finish rules for v1 txs - clean up is needed

This commit is contained in:
Boog900 2023-10-24 20:17:16 +01:00
parent 9b7f778f60
commit b727062e97
No known key found for this signature in database
GPG key ID: 5401367FB7302004
12 changed files with 365 additions and 291 deletions

View file

@ -1,17 +1,11 @@
#![cfg(feature = "binaries")] #![cfg(feature = "binaries")]
use futures::Sink;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::io::Read;
use std::ops::Range; use std::ops::Range;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use rayon::prelude::*;
use tower::{Service, ServiceExt}; use tower::{Service, ServiceExt};
use tracing::instrument;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use cuprate_common::Network; use cuprate_common::Network;
@ -53,7 +47,8 @@ where
{ {
tracing::info!("Beginning chain scan"); tracing::info!("Beginning chain scan");
let chain_height = 3_000_000; // TODO: when we implement all rules use the RPCs chain height, for now we don't check v2 txs.
let chain_height = 1288616;
tracing::info!("scanning to chain height: {}", chain_height); tracing::info!("scanning to chain height: {}", chain_height);
@ -79,10 +74,6 @@ where
let mut current_height = start_height; let mut current_height = start_height;
let mut next_batch_start_height = start_height + batch_size; let mut next_batch_start_height = start_height + batch_size;
let mut time_to_verify_last_batch: u128 = 0;
let mut batches_till_check_batch_size: u64 = 2;
while next_batch_start_height < chain_height { while next_batch_start_height < chain_height {
let next_batch_size = rpc_config.read().unwrap().block_batch_size(); let next_batch_size = rpc_config.read().unwrap().block_batch_size();
@ -96,56 +87,10 @@ where
)), )),
); );
let (DatabaseResponse::BlockBatchInRange(blocks), time_to_retrieve_batch) = let (DatabaseResponse::BlockBatchInRange(blocks), _) = current_fut.await?? else {
current_fut.await??
else {
panic!("Database sent incorrect response!"); panic!("Database sent incorrect response!");
}; };
let time_to_verify_batch = std::time::Instant::now();
let time_to_retrieve_batch = time_to_retrieve_batch.as_millis();
/*
if time_to_retrieve_batch > time_to_verify_last_batch + 2000
&& batches_till_check_batch_size == 0
{
batches_till_check_batch_size = 3;
let mut conf = rpc_config.write().unwrap();
tracing::info!(
"Decreasing batch size time to verify last batch: {}, time_to_retrieve_batch: {}",
time_to_verify_last_batch,
time_to_retrieve_batch
);
conf.max_blocks_per_node = (conf.max_blocks_per_node
* time_to_verify_last_batch as u64
/ (time_to_retrieve_batch as u64))
.max(10_u64)
.min(MAX_BLOCKS_IN_RANGE);
tracing::info!("Decreasing batch size to: {}", conf.max_blocks_per_node);
} else if time_to_retrieve_batch + 2000 < time_to_verify_last_batch
&& batches_till_check_batch_size == 0
{
batches_till_check_batch_size = 3;
let mut conf = rpc_config.write().unwrap();
tracing::info!(
"Increasing batch size time to verify last batch: {}, time_to_retrieve_batch: {}",
time_to_verify_last_batch,
time_to_retrieve_batch
);
conf.max_blocks_per_node = (conf.max_blocks_per_node
* (time_to_verify_last_batch as u64)
/ time_to_retrieve_batch.max(1) as u64)
.max(30_u64)
.min(MAX_BLOCKS_IN_RANGE);
tracing::info!("Increasing batch size to: {}", conf.max_blocks_per_node);
} else {
batches_till_check_batch_size = batches_till_check_batch_size.saturating_sub(1);
}
*/
tracing::info!( tracing::info!(
"Handling batch: {:?}, chain height: {}", "Handling batch: {:?}, chain height: {}",
current_height..(current_height + blocks.len() as u64), current_height..(current_height + blocks.len() as u64),
@ -190,8 +135,6 @@ where
cache.write().unwrap().save(&save_file)?; cache.write().unwrap().save(&save_file)?;
} }
} }
time_to_verify_last_batch = time_to_verify_batch.elapsed().as_millis();
} }
Ok(()) Ok(())
@ -200,7 +143,7 @@ where
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_max_level(LevelFilter::DEBUG) .with_max_level(LevelFilter::INFO)
.init(); .init();
let network = Network::Mainnet; let network = Network::Mainnet;

View file

@ -132,6 +132,7 @@ where
.oneshot(VerifyTxRequest::BatchSetupVerifyBlock { .oneshot(VerifyTxRequest::BatchSetupVerifyBlock {
txs, txs,
current_chain_height: context.chain_height, current_chain_height: context.chain_height,
time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(),
hf: context.current_hard_fork, hf: context.current_hard_fork,
}) })
.await? .await?

View file

@ -6,6 +6,7 @@
//! //!
use std::{ use std::{
cmp::min,
future::Future, future::Future,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
pin::Pin, pin::Pin,
@ -17,7 +18,7 @@ use futures::FutureExt;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower::{Service, ServiceExt}; use tower::{Service, ServiceExt};
use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse}; use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse};
pub mod difficulty; pub mod difficulty;
mod hardforks; mod hardforks;
@ -27,7 +28,7 @@ pub use difficulty::DifficultyCacheConfig;
pub use hardforks::{HardFork, HardForkConfig}; pub use hardforks::{HardFork, HardForkConfig};
pub use weight::BlockWeightsCacheConfig; pub use weight::BlockWeightsCacheConfig;
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60; const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60;
pub struct ContextConfig { pub struct ContextConfig {
hard_fork_cfg: HardForkConfig, hard_fork_cfg: HardForkConfig,
@ -143,11 +144,10 @@ pub struct BlockChainContext {
pub median_weight_for_block_reward: usize, pub median_weight_for_block_reward: usize,
/// The amount of coins minted already. /// The amount of coins minted already.
pub already_generated_coins: u64, pub already_generated_coins: u64,
/// Timestamp to use to check time locked outputs.
pub time_lock_timestamp: u64,
/// The median timestamp over the last [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks, will be None if there aren't /// The median timestamp over the last [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks, will be None if there aren't
/// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks. /// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks.
pub median_block_timestamp: Option<u64>, pub median_block_timestamp: Option<u64>,
top_block_timestamp: Option<u64>,
/// The height of the chain. /// The height of the chain.
pub chain_height: u64, pub chain_height: u64,
/// The top blocks hash /// The top blocks hash
@ -157,6 +157,29 @@ pub struct BlockChainContext {
} }
impl BlockChainContext { impl BlockChainContext {
/// Returns the timestamp the should be used when checking locked outputs.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
pub fn current_adjusted_timestamp_for_time_lock(&self) -> u64 {
if self.current_hard_fork < HardFork::V13 || self.median_block_timestamp.is_none() {
current_time()
} else {
// This is safe as we just checked if this was None.
let median = self.median_block_timestamp.unwrap();
let adjusted_median = median
+ (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1)
* self.current_hard_fork.block_time().as_secs()
/ 2;
// This is safe as we just checked if the median was None and this will only be none for genesis and the first block.
let adjusted_top_block =
self.top_block_timestamp.unwrap() + self.current_hard_fork.block_time().as_secs();
min(adjusted_median, adjusted_top_block)
}
}
pub fn block_blob_size_limit(&self) -> usize { pub fn block_blob_size_limit(&self) -> usize {
self.effective_median_weight * 2 - 600 self.effective_median_weight * 2 - 600
} }
@ -227,9 +250,9 @@ impl Service<BlockChainContextRequest> for BlockChainContextService {
median_long_term_weight: weight_cache.median_long_term_weight(), median_long_term_weight: weight_cache.median_long_term_weight(),
median_weight_for_block_reward: weight_cache.median_for_block_reward(&current_hf), median_weight_for_block_reward: weight_cache.median_for_block_reward(&current_hf),
already_generated_coins: *already_generated_coins, already_generated_coins: *already_generated_coins,
time_lock_timestamp: 0, //TODO: top_block_timestamp: difficulty_cache.top_block_timestamp(),
median_block_timestamp: difficulty_cache median_block_timestamp: difficulty_cache
.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW), .median_timestamp(usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap()),
chain_height: *chain_height, chain_height: *chain_height,
top_hash: *top_block_hash, top_hash: *top_block_hash,
current_hard_fork: current_hf, current_hard_fork: current_hf,

View file

@ -31,18 +31,22 @@ where
D: Database + Clone + Send + Sync + 'static, D: Database + Clone + Send + Sync + 'static,
D::Future: Send + 'static, D::Future: Send + 'static,
{ {
let (context_svc, context_svc_updater) = context::initialize_blockchain_context(cfg, database.clone()).await?; let (context_svc, context_svc_updater) =
context::initialize_blockchain_context(cfg, database.clone()).await?;
let tx_svc = transactions::TxVerifierService::new(database); let tx_svc = transactions::TxVerifierService::new(database);
let block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone()); let block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone());
Ok((block_svc, tx_svc, context_svc_updater)) Ok((block_svc, tx_svc, context_svc_updater))
} }
// TODO: split this enum up.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ConsensusError { pub enum ConsensusError {
#[error("Miner transaction invalid: {0}")] #[error("Miner transaction invalid: {0}")]
MinerTransaction(&'static str), MinerTransaction(&'static str),
#[error("Transaction sig invalid: {0}")] #[error("Transaction sig invalid: {0}")]
TransactionSignatureInvalid(&'static str), TransactionSignatureInvalid(&'static str),
#[error("Transaction has too high output amount")]
TransactionOutputsTooMuch,
#[error("Transaction inputs overflow")] #[error("Transaction inputs overflow")]
TransactionInputsOverflow, TransactionInputsOverflow,
#[error("Transaction outputs overflow")] #[error("Transaction outputs overflow")]
@ -112,6 +116,8 @@ pub enum DatabaseRequest {
Outputs(HashMap<u64, HashSet<u64>>), Outputs(HashMap<u64, HashSet<u64>>),
NumberOutputsWithAmount(u64), NumberOutputsWithAmount(u64),
CheckKIsNotSpent(HashSet<[u8; 32]>),
#[cfg(feature = "binaries")] #[cfg(feature = "binaries")]
BlockBatchInRange(std::ops::Range<u64>), BlockBatchInRange(std::ops::Range<u64>),
} }
@ -129,6 +135,8 @@ pub enum DatabaseResponse {
Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>), Outputs(HashMap<u64, HashMap<u64, OutputOnChain>>),
NumberOutputsWithAmount(usize), NumberOutputsWithAmount(usize),
CheckKIsNotSpent(bool),
#[cfg(feature = "binaries")] #[cfg(feature = "binaries")]
BlockBatchInRange( BlockBatchInRange(
Vec<( Vec<(

View file

@ -295,7 +295,13 @@ impl<R: RpcConnection + Send + Sync + 'static> tower::Service<DatabaseRequest> f
} }
.instrument(span) .instrument(span)
.boxed(), .boxed(),
DatabaseRequest::CheckKIsNotSpent(kis) => async move {
Ok(DatabaseResponse::CheckKIsNotSpent(
cache.read().unwrap().are_kis_spent(kis),
))
}
.instrument(span)
.boxed(),
DatabaseRequest::GeneratedCoins => async move { DatabaseRequest::GeneratedCoins => async move {
Ok(DatabaseResponse::GeneratedCoins( Ok(DatabaseResponse::GeneratedCoins(
cache.read().unwrap().already_generated_coins, cache.read().unwrap().already_generated_coins,

View file

@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use std::{ use std::{
@ -7,7 +8,7 @@ use std::{
}; };
use bincode::{Decode, Encode}; use bincode::{Decode, Encode};
use monero_serai::transaction::{Timelock, Transaction}; use monero_serai::transaction::{Input, Timelock, Transaction};
use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::fmt::MakeWriter;
use cuprate_common::Network; use cuprate_common::Network;
@ -24,6 +25,7 @@ pub struct ScanningCache {
// network: u8, // network: u8,
numb_outs: HashMap<u64, u64>, numb_outs: HashMap<u64, u64>,
time_locked_out: HashMap<[u8; 32], u64>, time_locked_out: HashMap<[u8; 32], u64>,
kis: HashSet<[u8; 32]>,
pub already_generated_coins: u64, pub already_generated_coins: u64,
/// The height of the *next* block to scan. /// The height of the *next* block to scan.
pub height: u64, pub height: u64,
@ -67,12 +69,23 @@ impl ScanningCache {
.outputs .outputs
.iter() .iter()
.for_each(|out| self.add_outs(out.amount.unwrap_or(0), 1)); .for_each(|out| self.add_outs(out.amount.unwrap_or(0), 1));
tx.tx.prefix.inputs.iter().for_each(|inp| match inp {
Input::ToKey { key_image, .. } => {
assert!(self.kis.insert(key_image.compress().to_bytes()))
}
_ => unreachable!(),
})
}); });
self.already_generated_coins = self.already_generated_coins.saturating_add(generated_coins); self.already_generated_coins = self.already_generated_coins.saturating_add(generated_coins);
self.height += 1; self.height += 1;
} }
pub fn are_kis_spent(&self, kis: HashSet<[u8; 32]>) -> bool {
self.kis.is_disjoint(&kis)
}
pub fn outputs_time_lock(&self, tx: &[u8; 32]) -> Timelock { pub fn outputs_time_lock(&self, tx: &[u8; 32]) -> Timelock {
let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0); let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0);
match time_lock { match time_lock {

View file

@ -19,7 +19,7 @@ mod inputs;
pub(crate) mod outputs; pub(crate) mod outputs;
mod ring; mod ring;
mod sigs; mod sigs;
//mod time_lock; mod time_lock;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum TxVersion { pub enum TxVersion {
@ -71,6 +71,7 @@ pub enum VerifyTxRequest {
Block { Block {
txs: Vec<Arc<TransactionVerificationData>>, txs: Vec<Arc<TransactionVerificationData>>,
current_chain_height: u64, current_chain_height: u64,
time_for_time_lock: u64,
hf: HardFork, hf: HardFork,
}, },
/// Batches the setup of [`TransactionVerificationData`] and verifies the transactions /// Batches the setup of [`TransactionVerificationData`] and verifies the transactions
@ -78,6 +79,7 @@ pub enum VerifyTxRequest {
BatchSetupVerifyBlock { BatchSetupVerifyBlock {
txs: Vec<Transaction>, txs: Vec<Transaction>,
current_chain_height: u64, current_chain_height: u64,
time_for_time_lock: u64,
hf: HardFork, hf: HardFork,
}, },
} }
@ -123,13 +125,28 @@ where
VerifyTxRequest::Block { VerifyTxRequest::Block {
txs, txs,
current_chain_height, current_chain_height,
time_for_time_lock,
hf, hf,
} => verify_transactions_for_block(database, txs, current_chain_height, hf).boxed(), } => verify_transactions_for_block(
database,
txs,
current_chain_height,
time_for_time_lock,
hf,
)
.boxed(),
VerifyTxRequest::BatchSetupVerifyBlock { VerifyTxRequest::BatchSetupVerifyBlock {
txs, txs,
current_chain_height, current_chain_height,
time_for_time_lock,
hf, hf,
} => batch_setup_verify_transactions_for_block(database, txs, current_chain_height, hf) } => batch_setup_verify_transactions_for_block(
database,
txs,
current_chain_height,
time_for_time_lock,
hf,
)
.boxed(), .boxed(),
} }
} }
@ -166,6 +183,7 @@ async fn batch_setup_verify_transactions_for_block<D>(
database: D, database: D,
txs: Vec<Transaction>, txs: Vec<Transaction>,
current_chain_height: u64, current_chain_height: u64,
time_for_time_lock: u64,
hf: HardFork, hf: HardFork,
) -> Result<VerifyTxResponse, ConsensusError> ) -> Result<VerifyTxResponse, ConsensusError>
where where
@ -180,7 +198,14 @@ where
.await .await
.unwrap()?; .unwrap()?;
verify_transactions_for_block(database, txs.clone(), current_chain_height, hf).await?; verify_transactions_for_block(
database,
txs.clone(),
current_chain_height,
time_for_time_lock,
hf,
)
.await?;
Ok(VerifyTxResponse::BatchSetupOk(txs)) Ok(VerifyTxResponse::BatchSetupOk(txs))
} }
@ -189,6 +214,7 @@ async fn verify_transactions_for_block<D>(
database: D, database: D,
txs: Vec<Arc<TransactionVerificationData>>, txs: Vec<Arc<TransactionVerificationData>>,
current_chain_height: u64, current_chain_height: u64,
time_for_time_lock: u64,
hf: HardFork, hf: HardFork,
) -> Result<VerifyTxResponse, ConsensusError> ) -> Result<VerifyTxResponse, ConsensusError>
where where
@ -202,7 +228,13 @@ where
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
txs.par_iter().try_for_each(|tx| { txs.par_iter().try_for_each(|tx| {
verify_transaction_for_block(tx, current_chain_height, hf, spent_kis.clone()) verify_transaction_for_block(
tx,
current_chain_height,
time_for_time_lock,
hf,
spent_kis.clone(),
)
}) })
}); });
@ -212,10 +244,11 @@ where
fn verify_transaction_for_block( fn verify_transaction_for_block(
tx_verification_data: &TransactionVerificationData, tx_verification_data: &TransactionVerificationData,
current_chain_height: u64, current_chain_height: u64,
time_for_time_lock: u64,
hf: HardFork, hf: HardFork,
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>, spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
) -> Result<(), ConsensusError> { ) -> Result<(), ConsensusError> {
tracing::trace!( tracing::debug!(
"Verifying transaction: {}", "Verifying transaction: {}",
hex::encode(tx_verification_data.tx_hash) hex::encode(tx_verification_data.tx_hash)
); );
@ -228,7 +261,14 @@ fn verify_transaction_for_block(
None => panic!("rings_member_info needs to be set to be able to verify!"), None => panic!("rings_member_info needs to be set to be able to verify!"),
}; };
check_tx_version(&rings_member_info.decoy_info, &tx_version, &hf)?; check_tx_version(&rings_member_info.decoy_info, tx_version, &hf)?;
time_lock::check_all_time_locks(
&rings_member_info.time_locked_outs,
current_chain_height,
time_for_time_lock,
&hf,
)?;
let sum_outputs = let sum_outputs =
outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?; outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?;
@ -242,6 +282,15 @@ fn verify_transaction_for_block(
spent_kis, spent_kis,
)?; )?;
if tx_version == &TxVersion::RingSignatures {
if sum_outputs >= sum_inputs {
return Err(ConsensusError::TransactionOutputsTooMuch);
}
// check that monero-serai is calculating the correct value here, why can't we just use this
// value? because we don't have this when we create the object.
assert_eq!(tx_verification_data.fee, sum_inputs - sum_outputs);
}
sigs::verify_signatures(&tx_verification_data.tx, &rings_member_info.rings)?; sigs::verify_signatures(&tx_verification_data.tx, &rings_member_info.rings)?;
Ok(()) Ok(())

View file

@ -1,11 +1,11 @@
use std::{ //! # Inputs
cmp::{max, min, Ordering}, //!
collections::HashSet, //! This module contains all consensus rules for non-miner transaction inputs, excluding time locks.
sync::Arc, //!
};
use std::{cmp::Ordering, collections::HashSet, sync::Arc};
use monero_serai::transaction::Input; use monero_serai::transaction::Input;
use tower::{Service, ServiceExt};
use crate::{ use crate::{
transactions::{ transactions::{
@ -69,6 +69,7 @@ pub(crate) fn check_key_images(
) -> Result<(), ConsensusError> { ) -> Result<(), ConsensusError> {
match input { match input {
Input::ToKey { key_image, .. } => { Input::ToKey { key_image, .. } => {
// this happens in monero-serai but we may as well duplicate the check.
if !key_image.is_torsion_free() { if !key_image.is_torsion_free() {
return Err(ConsensusError::TransactionHasInvalidInput( return Err(ConsensusError::TransactionHasInvalidInput(
"key image has torsion", "key image has torsion",
@ -120,6 +121,7 @@ fn check_input_has_decoys(input: &Input) -> Result<(), ConsensusError> {
/// Checks that the ring members for the input are unique after hard-fork 6. /// Checks that the ring members for the input are unique after hard-fork 6.
/// ///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-ring-members
fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> { fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> {
if hf >= &HardFork::V6 { if hf >= &HardFork::V6 {
match input { match input {
@ -139,6 +141,9 @@ fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), Consens
} }
} }
/// Checks that from hf 7 the inputs are sorted by key image.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#sorted-inputs
fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusError> { fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusError> {
let get_ki = |inp: &Input| match inp { let get_ki = |inp: &Input| match inp {
Input::ToKey { key_image, .. } => key_image.compress().to_bytes(), Input::ToKey { key_image, .. } => key_image.compress().to_bytes(),
@ -162,6 +167,9 @@ fn check_inputs_sorted(inputs: &[Input], hf: &HardFork) -> Result<(), ConsensusE
} }
} }
/// Checks the youngest output is at least 10 blocks old.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#10-block-lock
fn check_10_block_lock( fn check_10_block_lock(
ring_member_info: &TxRingMembersInfo, ring_member_info: &TxRingMembersInfo,
current_chain_height: u64, current_chain_height: u64,
@ -170,7 +178,7 @@ fn check_10_block_lock(
if hf >= &HardFork::V12 { if hf >= &HardFork::V12 {
if ring_member_info.youngest_used_out_height + 10 > current_chain_height { if ring_member_info.youngest_used_out_height + 10 > current_chain_height {
Err(ConsensusError::TransactionHasInvalidRing( Err(ConsensusError::TransactionHasInvalidRing(
"tx has one ring member which is too younge", "tx has one ring member which is too young",
)) ))
} else { } else {
Ok(()) Ok(())
@ -203,6 +211,10 @@ fn sum_inputs_v1(inputs: &[Input]) -> Result<u64, ConsensusError> {
Ok(sum) Ok(sum)
} }
/// Checks all input consensus rules.
///
/// TODO: list rules.
///
pub fn check_inputs( pub fn check_inputs(
inputs: &[Input], inputs: &[Input],
ring_member_info: &TxRingMembersInfo, ring_member_info: &TxRingMembersInfo,
@ -219,6 +231,8 @@ pub fn check_inputs(
if let Some(decoy_info) = &ring_member_info.decoy_info { if let Some(decoy_info) = &ring_member_info.decoy_info {
check_decoy_info(decoy_info, hf)?; check_decoy_info(decoy_info, hf)?;
} else {
assert_eq!(hf, &HardFork::V1);
} }
for input in inputs { for input in inputs {

View file

@ -1,3 +1,7 @@
//! # Outputs
//!
//! Consensus rules relating to non-miner transaction outputs
use std::sync::OnceLock; use std::sync::OnceLock;
use monero_serai::transaction::Output; use monero_serai::transaction::Output;
@ -127,11 +131,11 @@ pub fn check_outputs(
hf: &HardFork, hf: &HardFork,
tx_version: &TxVersion, tx_version: &TxVersion,
) -> Result<u64, ConsensusError> { ) -> Result<u64, ConsensusError> {
check_output_types(outputs, &hf)?; check_output_types(outputs, hf)?;
check_output_keys(outputs)?; check_output_keys(outputs)?;
match tx_version { match tx_version {
TxVersion::RingSignatures => sum_outputs_v1(outputs, &hf), TxVersion::RingSignatures => sum_outputs_v1(outputs, hf),
_ => todo!("RingCT"), _ => todo!("RingCT"),
} }
} }

View file

@ -4,6 +4,8 @@
//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied //! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
//! upon to do any. //! upon to do any.
//! //!
//! The data collected by this module can be used to perform consensus checks.
//!
use std::{ use std::{
cmp::{max, min}, cmp::{max, min},
@ -13,8 +15,8 @@ use std::{
use curve25519_dalek::EdwardsPoint; use curve25519_dalek::EdwardsPoint;
use monero_serai::{ use monero_serai::{
ringct::{mlsag::RingMatrix, RctType}, ringct::RctType,
transaction::{Input, Timelock, Transaction}, transaction::{Input, Timelock},
}; };
use tower::ServiceExt; use tower::ServiceExt;
@ -23,168 +25,10 @@ use crate::{
DatabaseResponse, HardFork, OutputOnChain, DatabaseResponse, HardFork, OutputOnChain,
}; };
/// Gets the absolute offsets from the relative offsets. /// Fills the `rings_member_info` field on the inputted [`TransactionVerificationData`].
/// ///
/// This function will return an error if the relative offsets are empty. /// This function batch gets all the ring members for the inputted transactions and fills in data about
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys /// them, like the youngest used out and the time locks.
fn get_absolute_offsets(relative_offsets: &[u64]) -> Result<Vec<u64>, ConsensusError> {
if relative_offsets.is_empty() {
return Err(ConsensusError::TransactionHasInvalidRing(
"ring has no members",
));
}
let mut offsets = Vec::with_capacity(relative_offsets.len());
offsets.push(relative_offsets[0]);
for i in 1..relative_offsets.len() {
offsets.push(offsets[i - 1] + relative_offsets[i]);
}
Ok(offsets)
}
/// Inserts the outputs that are needed to verify the transaction inputs into the provided HashMap.
///
/// This will error if the inputs are empty
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#no-empty-inputs
///
pub fn insert_ring_member_ids(
inputs: &[Input],
output_ids: &mut HashMap<u64, HashSet<u64>>,
) -> Result<(), ConsensusError> {
if inputs.is_empty() {
return Err(ConsensusError::TransactionHasInvalidInput(
"transaction has no inputs",
));
}
for input in inputs {
match input {
Input::ToKey {
amount,
key_offsets,
..
} => output_ids
.entry(amount.unwrap_or(0))
.or_insert_with(HashSet::new)
.extend(get_absolute_offsets(key_offsets)?),
// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#input-type
_ => {
return Err(ConsensusError::TransactionHasInvalidInput(
"input not ToKey",
))
}
}
}
Ok(())
}
/// Represents the ring members of all the inputs.
#[derive(Debug)]
pub enum Rings {
/// Legacy, pre-ringCT, ring.
Legacy(Vec<Vec<EdwardsPoint>>),
/// TODO:
RingCT,
}
impl Rings {
/// Builds the rings for the transaction inputs, from the given outputs.
pub fn new(outputs: Vec<Vec<&OutputOnChain>>, rct_type: RctType) -> Rings {
match rct_type {
RctType::Null => Rings::Legacy(
outputs
.into_iter()
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
.collect(),
),
_ => todo!("RingCT"),
}
}
}
/// Information on the outputs the transaction is is referencing for inputs (ring members).
#[derive(Debug)]
pub struct TxRingMembersInfo {
pub rings: Rings,
/// Information on the structure of the decoys, will be [`None`] for txs before [`HardFork::V1`]
pub decoy_info: Option<DecoyInfo>,
pub youngest_used_out_height: u64,
pub time_locked_outs: Vec<Timelock>,
}
impl TxRingMembersInfo {
pub fn new(
used_outs: Vec<Vec<&OutputOnChain>>,
decoy_info: Option<DecoyInfo>,
rct_type: RctType,
) -> TxRingMembersInfo {
TxRingMembersInfo {
youngest_used_out_height: used_outs
.iter()
.map(|inp_outs| {
inp_outs
.iter()
.map(|out| out.height)
.max()
.expect("Input must have ring members")
})
.max()
.expect("Tx must have inputs"),
time_locked_outs: used_outs
.iter()
.flat_map(|inp_outs| {
inp_outs
.iter()
.filter_map(|out| match out.time_lock {
Timelock::None => None,
lock => Some(lock),
})
.collect::<Vec<_>>()
})
.collect(),
rings: Rings::new(used_outs, rct_type),
decoy_info,
}
}
}
/// Get the ring members for the inputs from the outputs on the chain.
fn get_ring_members_for_inputs<'a>(
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
inputs: &[Input],
) -> Result<Vec<Vec<&'a OutputOnChain>>, ConsensusError> {
inputs
.iter()
.map(|inp| match inp {
Input::ToKey {
amount,
key_offsets,
..
} => {
let offsets = get_absolute_offsets(key_offsets)?;
Ok(offsets
.iter()
.map(|offset| {
// get the hashmap for this amount.
outputs
.get(&amount.unwrap_or(0))
// get output at the index from the amount hashmap.
.and_then(|amount_map| amount_map.get(offset))
.ok_or(ConsensusError::TransactionHasInvalidRing(
"ring member not in database",
))
})
.collect::<Result<_, ConsensusError>>()?)
}
_ => Err(ConsensusError::TransactionHasInvalidInput(
"input not ToKey",
)),
})
.collect::<Result<_, ConsensusError>>()
}
/// Fills the `rings_member_info` field on the inputted [`TransactionVerificationData`]
pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'static>( pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'static>(
txs_verification_data: &[Arc<TransactionVerificationData>], txs_verification_data: &[Arc<TransactionVerificationData>],
hf: &HardFork, hf: &HardFork,
@ -230,6 +74,173 @@ pub async fn batch_fill_ring_member_info<D: Database + Clone + Send + Sync + 'st
Ok(()) Ok(())
} }
/// Gets the absolute offsets from the relative offsets.
///
/// This function will return an error if the relative offsets are empty.
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys
fn get_absolute_offsets(relative_offsets: &[u64]) -> Result<Vec<u64>, ConsensusError> {
if relative_offsets.is_empty() {
return Err(ConsensusError::TransactionHasInvalidRing(
"ring has no members",
));
}
let mut offsets = Vec::with_capacity(relative_offsets.len());
offsets.push(relative_offsets[0]);
for i in 1..relative_offsets.len() {
offsets.push(offsets[i - 1] + relative_offsets[i]);
}
Ok(offsets)
}
/// Inserts the output IDs that are needed to verify the transaction inputs into the provided HashMap.
///
/// This will error if the inputs are empty
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#no-empty-inputs
///
fn insert_ring_member_ids(
inputs: &[Input],
output_ids: &mut HashMap<u64, HashSet<u64>>,
) -> Result<(), ConsensusError> {
if inputs.is_empty() {
return Err(ConsensusError::TransactionHasInvalidInput(
"transaction has no inputs",
));
}
for input in inputs {
match input {
Input::ToKey {
amount,
key_offsets,
..
} => output_ids
.entry(amount.unwrap_or(0))
.or_insert_with(HashSet::new)
.extend(get_absolute_offsets(key_offsets)?),
// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#input-type
_ => {
return Err(ConsensusError::TransactionHasInvalidInput(
"input not ToKey",
))
}
}
}
Ok(())
}
/// Represents the ring members of all the inputs.
#[derive(Debug)]
pub enum Rings {
/// Legacy, pre-ringCT, rings.
Legacy(Vec<Vec<EdwardsPoint>>),
/// TODO:
RingCT,
}
impl Rings {
/// Builds the rings for the transaction inputs, from the given outputs.
fn new(outputs: Vec<Vec<&OutputOnChain>>, rct_type: RctType) -> Rings {
match rct_type {
RctType::Null => Rings::Legacy(
outputs
.into_iter()
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
.collect(),
),
_ => todo!("RingCT"),
}
}
}
/// Information on the outputs the transaction is is referencing for inputs (ring members).
#[derive(Debug)]
pub struct TxRingMembersInfo {
pub rings: Rings,
/// Information on the structure of the decoys, will be [`None`] for txs before [`HardFork::V1`]
pub decoy_info: Option<DecoyInfo>,
pub youngest_used_out_height: u64,
pub time_locked_outs: Vec<Timelock>,
}
impl TxRingMembersInfo {
/// Construct a [`TxRingMembersInfo`] struct.
///
/// The used outs must be all the ring members used in the transactions inputs.
fn new(
used_outs: Vec<Vec<&OutputOnChain>>,
decoy_info: Option<DecoyInfo>,
rct_type: RctType,
) -> TxRingMembersInfo {
TxRingMembersInfo {
youngest_used_out_height: used_outs
.iter()
.map(|inp_outs| {
inp_outs
.iter()
// the output with the highest height is the youngest
.map(|out| out.height)
.max()
.expect("Input must have ring members")
})
.max()
.expect("Tx must have inputs"),
time_locked_outs: used_outs
.iter()
.flat_map(|inp_outs| {
inp_outs
.iter()
.filter_map(|out| match out.time_lock {
Timelock::None => None,
lock => Some(lock),
})
.collect::<Vec<_>>()
})
.collect(),
rings: Rings::new(used_outs, rct_type),
decoy_info,
}
}
}
/// Get the ring members for the inputs from the outputs on the chain.
///
/// Will error if `outputs` does not contain the outputs needed.
fn get_ring_members_for_inputs<'a>(
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
inputs: &[Input],
) -> Result<Vec<Vec<&'a OutputOnChain>>, ConsensusError> {
inputs
.iter()
.map(|inp| match inp {
Input::ToKey {
amount,
key_offsets,
..
} => {
let offsets = get_absolute_offsets(key_offsets)?;
Ok(offsets
.iter()
.map(|offset| {
// get the hashmap for this amount.
outputs
.get(&amount.unwrap_or(0))
// get output at the index from the amount hashmap.
.and_then(|amount_map| amount_map.get(offset))
.ok_or(ConsensusError::TransactionHasInvalidRing(
"ring member not in database",
))
})
.collect::<Result<_, ConsensusError>>()?)
}
_ => Err(ConsensusError::TransactionHasInvalidInput(
"input not ToKey",
)),
})
.collect::<Result<_, ConsensusError>>()
}
/// A struct holding information about the inputs and their decoys. /// A struct holding information about the inputs and their decoys.
/// ///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html /// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html

View file

@ -1,18 +1,45 @@
use std::cmp::min; //! # Time Locks
//!
//! This module contains the checks for time locks, using the `check_all_time_locks` function.
//!
use monero_serai::transaction::Timelock; use monero_serai::transaction::Timelock;
use crate::{context::difficulty::DifficultyCache, helper::current_time, HardFork}; use crate::{ConsensusError, HardFork};
const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; /// Checks all the time locks are unlocked.
///
/// `current_time_lock_timestamp` must be: https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
pub fn check_all_time_locks(
time_locks: &[Timelock],
current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> Result<(), ConsensusError> {
time_locks.iter().try_for_each(|time_lock| {
if !output_unlocked(
time_lock,
current_chain_height,
current_time_lock_timestamp,
hf,
) {
Err(ConsensusError::TransactionHasInvalidRing(
"One or more ring members locked",
))
} else {
Ok(())
}
})
}
/// Checks if an outputs unlock time has passed. /// Checks if an outputs unlock time has passed.
/// ///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time /// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
pub fn output_unlocked( fn output_unlocked(
time_lock: &Timelock, time_lock: &Timelock,
difficulty_cache: &DifficultyCache,
current_chain_height: u64, current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork, hf: &HardFork,
) -> bool { ) -> bool {
match *time_lock { match *time_lock {
@ -21,7 +48,7 @@ pub fn output_unlocked(
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height) check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
} }
Timelock::Time(unlock_time) => { Timelock::Time(unlock_time) => {
check_timestamp_time_lock(unlock_time, difficulty_cache, current_chain_height, hf) check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
} }
} }
} }
@ -34,39 +61,14 @@ fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool
unlock_height >= current_chain_height unlock_height >= current_chain_height
} }
/// Returns the timestamp the should be used when checking locked outputs. /// ///
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
fn get_current_timestamp(
difficulty_cache: &DifficultyCache,
current_chain_height: u64,
hf: &HardFork,
) -> u64 {
if hf < &HardFork::V13 || current_chain_height < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
current_time()
} else {
let median = difficulty_cache
.median_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW.try_into().unwrap());
let adjusted_median =
median + (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * hf.block_time().as_secs() / 2;
// This is safe as we just check we don't have less than 60 blocks in the chain.
let adjusted_top_block =
difficulty_cache.top_block_timestamp().unwrap() + hf.block_time().as_secs();
min(adjusted_median, adjusted_top_block)
}
}
/// Returns if a locked output, which uses a block height, can be spend. /// Returns if a locked output, which uses a block height, can be spend.
/// ///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp /// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
fn check_timestamp_time_lock( fn check_timestamp_time_lock(
unlock_timestamp: u64, unlock_timestamp: u64,
difficulty_cache: &DifficultyCache, current_time_lock_timestamp: u64,
current_chain_height: u64,
hf: &HardFork, hf: &HardFork,
) -> bool { ) -> bool {
let timestamp = get_current_timestamp(difficulty_cache, current_chain_height, hf); current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
timestamp + hf.block_time().as_secs() >= unlock_timestamp
} }

View file

@ -57,12 +57,12 @@ pub struct AltBlock {
// ---- TRANSACTIONS ---- // ---- TRANSACTIONS ----
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// [`TransactionPruned`] is, as its name suggest, the pruned part of a transaction, which is the Transaction Prefix and its RingCT signatures. /// [`TransactionPruned`] is, as its name suggest, the pruned part of a transaction, which is the Transaction Prefix and its RingCT ring.
/// This struct is used in the [`crate::table::txsprefix`] table. /// This struct is used in the [`crate::table::txsprefix`] table.
pub struct TransactionPruned { pub struct TransactionPruned {
/// The transaction prefix. /// The transaction prefix.
pub prefix: TransactionPrefix, pub prefix: TransactionPrefix,
/// The RingCT signatures, will only contain the 'sig' field. /// The RingCT ring, will only contain the 'sig' field.
pub rct_signatures: RctSig, pub rct_signatures: RctSig,
} }
@ -80,7 +80,7 @@ impl bincode::Decode for TransactionPruned {
// Handle the prefix accordingly to its version // Handle the prefix accordingly to its version
match *prefix.version { match *prefix.version {
// First transaction format, Pre-RingCT, so the signatures are None // First transaction format, Pre-RingCT, so the ring are None
1 => Ok(TransactionPruned { 1 => Ok(TransactionPruned {
prefix, prefix,
rct_signatures: RctSig { sig: None, p: None }, rct_signatures: RctSig { sig: None, p: None },
@ -94,7 +94,7 @@ impl bincode::Decode for TransactionPruned {
rct_signatures, rct_signatures,
}); });
} }
// Otherwise get the RingCT signatures for the tx inputs // Otherwise get the RingCT ring for the tx inputs
if let Some(sig) = RctSigBase::consensus_decode(&mut r, inputs, outputs) if let Some(sig) = RctSigBase::consensus_decode(&mut r, inputs, outputs)
.map_err(|_| bincode::error::DecodeError::Other("Monero-rs decoding failed"))? .map_err(|_| bincode::error::DecodeError::Other("Monero-rs decoding failed"))?
{ {
@ -123,10 +123,10 @@ impl bincode::Encode for TransactionPruned {
let buf = monero::consensus::serialize(&self.prefix); let buf = monero::consensus::serialize(&self.prefix);
writer.write(&buf)?; writer.write(&buf)?;
match *self.prefix.version { match *self.prefix.version {
1 => {} // First transaction format, Pre-RingCT, so the there is no Rct signatures to add 1 => {} // First transaction format, Pre-RingCT, so the there is no Rct ring to add
_ => { _ => {
if let Some(sig) = &self.rct_signatures.sig { if let Some(sig) = &self.rct_signatures.sig {
// If there is signatures then we append it at the end // If there is ring then we append it at the end
let buf = monero::consensus::serialize(sig); let buf = monero::consensus::serialize(sig);
writer.write(&buf)?; writer.write(&buf)?;
} }