From b727062e972102dd62e6e33637079c9be90cd959 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Tue, 24 Oct 2023 20:17:16 +0100 Subject: [PATCH] finish rules for v1 txs - clean up is needed --- consensus/src/bin/scan_chain.rs | 65 +---- consensus/src/block.rs | 1 + consensus/src/context.rs | 35 ++- consensus/src/lib.rs | 10 +- consensus/src/rpc.rs | 8 +- consensus/src/rpc/cache.rs | 15 +- consensus/src/transactions.rs | 65 ++++- consensus/src/transactions/inputs.rs | 28 +- consensus/src/transactions/outputs.rs | 8 +- consensus/src/transactions/ring.rs | 337 ++++++++++++------------ consensus/src/transactions/time_lock.rs | 72 ++--- database/src/types.rs | 12 +- 12 files changed, 365 insertions(+), 291 deletions(-) diff --git a/consensus/src/bin/scan_chain.rs b/consensus/src/bin/scan_chain.rs index 0502e7bb..6d1f051f 100644 --- a/consensus/src/bin/scan_chain.rs +++ b/consensus/src/bin/scan_chain.rs @@ -1,17 +1,11 @@ #![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::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Duration; -use rayon::prelude::*; use tower::{Service, ServiceExt}; -use tracing::instrument; use tracing::level_filters::LevelFilter; use cuprate_common::Network; @@ -53,7 +47,8 @@ where { 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); @@ -79,10 +74,6 @@ where let mut current_height = start_height; 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 { let next_batch_size = rpc_config.read().unwrap().block_batch_size(); @@ -96,56 +87,10 @@ where )), ); - let (DatabaseResponse::BlockBatchInRange(blocks), time_to_retrieve_batch) = - current_fut.await?? - else { + let (DatabaseResponse::BlockBatchInRange(blocks), _) = current_fut.await?? else { 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!( "Handling batch: {:?}, chain height: {}", current_height..(current_height + blocks.len() as u64), @@ -190,8 +135,6 @@ where cache.write().unwrap().save(&save_file)?; } } - - time_to_verify_last_batch = time_to_verify_batch.elapsed().as_millis(); } Ok(()) @@ -200,7 +143,7 @@ where #[tokio::main] async fn main() { tracing_subscriber::fmt() - .with_max_level(LevelFilter::DEBUG) + .with_max_level(LevelFilter::INFO) .init(); let network = Network::Mainnet; diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 98b929c4..ca15ed5d 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -132,6 +132,7 @@ where .oneshot(VerifyTxRequest::BatchSetupVerifyBlock { txs, current_chain_height: context.chain_height, + time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(), hf: context.current_hard_fork, }) .await? diff --git a/consensus/src/context.rs b/consensus/src/context.rs index 43f33270..5017c2e4 100644 --- a/consensus/src/context.rs +++ b/consensus/src/context.rs @@ -6,6 +6,7 @@ //! use std::{ + cmp::min, future::Future, ops::{Deref, DerefMut}, pin::Pin, @@ -17,7 +18,7 @@ use futures::FutureExt; use tokio::sync::RwLock; use tower::{Service, ServiceExt}; -use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse}; +use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse}; pub mod difficulty; mod hardforks; @@ -27,7 +28,7 @@ pub use difficulty::DifficultyCacheConfig; pub use hardforks::{HardFork, HardForkConfig}; pub use weight::BlockWeightsCacheConfig; -const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60; +const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; pub struct ContextConfig { hard_fork_cfg: HardForkConfig, @@ -143,11 +144,10 @@ pub struct BlockChainContext { pub median_weight_for_block_reward: usize, /// The amount of coins minted already. 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 /// [`BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW`] blocks. pub median_block_timestamp: Option, + top_block_timestamp: Option, /// The height of the chain. pub chain_height: u64, /// The top blocks hash @@ -157,6 +157,29 @@ pub struct 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 { self.effective_median_weight * 2 - 600 } @@ -227,9 +250,9 @@ impl Service for BlockChainContextService { median_long_term_weight: weight_cache.median_long_term_weight(), median_weight_for_block_reward: weight_cache.median_for_block_reward(¤t_hf), 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_timestamp(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW), + .median_timestamp(usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap()), chain_height: *chain_height, top_hash: *top_block_hash, current_hard_fork: current_hf, diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index b7e6ba5f..c08d6e57 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -31,18 +31,22 @@ where D: Database + Clone + Send + Sync + '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 block_svc = block::BlockVerifierService::new(context_svc.clone(), tx_svc.clone()); Ok((block_svc, tx_svc, context_svc_updater)) } +// TODO: split this enum up. #[derive(Debug, thiserror::Error)] pub enum ConsensusError { #[error("Miner transaction invalid: {0}")] MinerTransaction(&'static str), #[error("Transaction sig invalid: {0}")] TransactionSignatureInvalid(&'static str), + #[error("Transaction has too high output amount")] + TransactionOutputsTooMuch, #[error("Transaction inputs overflow")] TransactionInputsOverflow, #[error("Transaction outputs overflow")] @@ -111,6 +115,8 @@ pub enum DatabaseRequest { Outputs(HashMap>), NumberOutputsWithAmount(u64), + + CheckKIsNotSpent(HashSet<[u8; 32]>), #[cfg(feature = "binaries")] BlockBatchInRange(std::ops::Range), @@ -129,6 +135,8 @@ pub enum DatabaseResponse { Outputs(HashMap>), NumberOutputsWithAmount(usize), + CheckKIsNotSpent(bool), + #[cfg(feature = "binaries")] BlockBatchInRange( Vec<( diff --git a/consensus/src/rpc.rs b/consensus/src/rpc.rs index 9d1654d5..b593e225 100644 --- a/consensus/src/rpc.rs +++ b/consensus/src/rpc.rs @@ -295,7 +295,13 @@ impl tower::Service f } .instrument(span) .boxed(), - + DatabaseRequest::CheckKIsNotSpent(kis) => async move { + Ok(DatabaseResponse::CheckKIsNotSpent( + cache.read().unwrap().are_kis_spent(kis), + )) + } + .instrument(span) + .boxed(), DatabaseRequest::GeneratedCoins => async move { Ok(DatabaseResponse::GeneratedCoins( cache.read().unwrap().already_generated_coins, diff --git a/consensus/src/rpc/cache.rs b/consensus/src/rpc/cache.rs index ff2321df..d44b4ce9 100644 --- a/consensus/src/rpc/cache.rs +++ b/consensus/src/rpc/cache.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::io::Read; use std::path::Path; use std::{ @@ -7,7 +8,7 @@ use std::{ }; use bincode::{Decode, Encode}; -use monero_serai::transaction::{Timelock, Transaction}; +use monero_serai::transaction::{Input, Timelock, Transaction}; use tracing_subscriber::fmt::MakeWriter; use cuprate_common::Network; @@ -24,6 +25,7 @@ pub struct ScanningCache { // network: u8, numb_outs: HashMap, time_locked_out: HashMap<[u8; 32], u64>, + kis: HashSet<[u8; 32]>, pub already_generated_coins: u64, /// The height of the *next* block to scan. pub height: u64, @@ -67,12 +69,23 @@ impl ScanningCache { .outputs .iter() .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.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 { let time_lock = self.time_locked_out.get(tx).copied().unwrap_or(0); match time_lock { diff --git a/consensus/src/transactions.rs b/consensus/src/transactions.rs index fe719387..c5790ef7 100644 --- a/consensus/src/transactions.rs +++ b/consensus/src/transactions.rs @@ -19,7 +19,7 @@ mod inputs; pub(crate) mod outputs; mod ring; mod sigs; -//mod time_lock; +mod time_lock; #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum TxVersion { @@ -71,6 +71,7 @@ pub enum VerifyTxRequest { Block { txs: Vec>, current_chain_height: u64, + time_for_time_lock: u64, hf: HardFork, }, /// Batches the setup of [`TransactionVerificationData`] and verifies the transactions @@ -78,6 +79,7 @@ pub enum VerifyTxRequest { BatchSetupVerifyBlock { txs: Vec, current_chain_height: u64, + time_for_time_lock: u64, hf: HardFork, }, } @@ -123,14 +125,29 @@ where VerifyTxRequest::Block { txs, current_chain_height, + time_for_time_lock, 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 { txs, current_chain_height, + time_for_time_lock, hf, - } => batch_setup_verify_transactions_for_block(database, txs, current_chain_height, hf) - .boxed(), + } => batch_setup_verify_transactions_for_block( + database, + txs, + current_chain_height, + time_for_time_lock, + hf, + ) + .boxed(), } } } @@ -166,6 +183,7 @@ async fn batch_setup_verify_transactions_for_block( database: D, txs: Vec, current_chain_height: u64, + time_for_time_lock: u64, hf: HardFork, ) -> Result where @@ -180,7 +198,14 @@ where .await .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)) } @@ -189,6 +214,7 @@ async fn verify_transactions_for_block( database: D, txs: Vec>, current_chain_height: u64, + time_for_time_lock: u64, hf: HardFork, ) -> Result where @@ -202,7 +228,13 @@ where tokio::task::spawn_blocking(move || { 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( tx_verification_data: &TransactionVerificationData, current_chain_height: u64, + time_for_time_lock: u64, hf: HardFork, spent_kis: Arc>>, ) -> Result<(), ConsensusError> { - tracing::trace!( + tracing::debug!( "Verifying transaction: {}", 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!"), }; - 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 = outputs::check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?; @@ -242,6 +282,15 @@ fn verify_transaction_for_block( 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)?; Ok(()) diff --git a/consensus/src/transactions/inputs.rs b/consensus/src/transactions/inputs.rs index e5912a14..79426c7d 100644 --- a/consensus/src/transactions/inputs.rs +++ b/consensus/src/transactions/inputs.rs @@ -1,11 +1,11 @@ -use std::{ - cmp::{max, min, Ordering}, - collections::HashSet, - sync::Arc, -}; +//! # Inputs +//! +//! This module contains all consensus rules for non-miner transaction inputs, excluding time locks. +//! + +use std::{cmp::Ordering, collections::HashSet, sync::Arc}; use monero_serai::transaction::Input; -use tower::{Service, ServiceExt}; use crate::{ transactions::{ @@ -69,6 +69,7 @@ pub(crate) fn check_key_images( ) -> Result<(), ConsensusError> { match input { Input::ToKey { key_image, .. } => { + // this happens in monero-serai but we may as well duplicate the check. if !key_image.is_torsion_free() { return Err(ConsensusError::TransactionHasInvalidInput( "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. /// +/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-ring-members fn check_ring_members_unique(input: &Input, hf: &HardFork) -> Result<(), ConsensusError> { if hf >= &HardFork::V6 { 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> { let get_ki = |inp: &Input| match inp { 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( ring_member_info: &TxRingMembersInfo, current_chain_height: u64, @@ -170,7 +178,7 @@ fn check_10_block_lock( if hf >= &HardFork::V12 { if ring_member_info.youngest_used_out_height + 10 > current_chain_height { Err(ConsensusError::TransactionHasInvalidRing( - "tx has one ring member which is too younge", + "tx has one ring member which is too young", )) } else { Ok(()) @@ -203,6 +211,10 @@ fn sum_inputs_v1(inputs: &[Input]) -> Result { Ok(sum) } +/// Checks all input consensus rules. +/// +/// TODO: list rules. +/// pub fn check_inputs( inputs: &[Input], ring_member_info: &TxRingMembersInfo, @@ -219,6 +231,8 @@ pub fn check_inputs( if let Some(decoy_info) = &ring_member_info.decoy_info { check_decoy_info(decoy_info, hf)?; + } else { + assert_eq!(hf, &HardFork::V1); } for input in inputs { diff --git a/consensus/src/transactions/outputs.rs b/consensus/src/transactions/outputs.rs index dcdffb7f..67226da9 100644 --- a/consensus/src/transactions/outputs.rs +++ b/consensus/src/transactions/outputs.rs @@ -1,3 +1,7 @@ +//! # Outputs +//! +//! Consensus rules relating to non-miner transaction outputs + use std::sync::OnceLock; use monero_serai::transaction::Output; @@ -127,11 +131,11 @@ pub fn check_outputs( hf: &HardFork, tx_version: &TxVersion, ) -> Result { - check_output_types(outputs, &hf)?; + check_output_types(outputs, hf)?; check_output_keys(outputs)?; match tx_version { - TxVersion::RingSignatures => sum_outputs_v1(outputs, &hf), + TxVersion::RingSignatures => sum_outputs_v1(outputs, hf), _ => todo!("RingCT"), } } diff --git a/consensus/src/transactions/ring.rs b/consensus/src/transactions/ring.rs index bf2812cc..86b72c05 100644 --- a/consensus/src/transactions/ring.rs +++ b/consensus/src/transactions/ring.rs @@ -4,6 +4,8 @@ //! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied //! upon to do any. //! +//! The data collected by this module can be used to perform consensus checks. +//! use std::{ cmp::{max, min}, @@ -13,8 +15,8 @@ use std::{ use curve25519_dalek::EdwardsPoint; use monero_serai::{ - ringct::{mlsag::RingMatrix, RctType}, - transaction::{Input, Timelock, Transaction}, + ringct::RctType, + transaction::{Input, Timelock}, }; use tower::ServiceExt; @@ -23,168 +25,10 @@ use crate::{ 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. -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys -fn get_absolute_offsets(relative_offsets: &[u64]) -> Result, 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>, -) -> 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>), - /// TODO: - RingCT, -} - -impl Rings { - /// Builds the rings for the transaction inputs, from the given outputs. - pub fn new(outputs: Vec>, 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, - pub youngest_used_out_height: u64, - pub time_locked_outs: Vec, -} - -impl TxRingMembersInfo { - pub fn new( - used_outs: Vec>, - decoy_info: Option, - 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::>() - }) - .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>, - inputs: &[Input], -) -> Result>, 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::>()?) - } - _ => Err(ConsensusError::TransactionHasInvalidInput( - "input not ToKey", - )), - }) - .collect::>() -} - -/// Fills the `rings_member_info` field on the inputted [`TransactionVerificationData`] +/// This function batch gets all the ring members for the inputted transactions and fills in data about +/// them, like the youngest used out and the time locks. pub async fn batch_fill_ring_member_info( txs_verification_data: &[Arc], hf: &HardFork, @@ -230,6 +74,173 @@ pub async fn batch_fill_ring_member_info Result, 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>, +) -> 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>), + /// TODO: + RingCT, +} + +impl Rings { + /// Builds the rings for the transaction inputs, from the given outputs. + fn new(outputs: Vec>, 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, + pub youngest_used_out_height: u64, + pub time_locked_outs: Vec, +} + +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>, + decoy_info: Option, + 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::>() + }) + .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>, + inputs: &[Input], +) -> Result>, 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::>()?) + } + _ => Err(ConsensusError::TransactionHasInvalidInput( + "input not ToKey", + )), + }) + .collect::>() +} + /// A struct holding information about the inputs and their decoys. /// /// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html diff --git a/consensus/src/transactions/time_lock.rs b/consensus/src/transactions/time_lock.rs index 872dd6ec..8546842a 100644 --- a/consensus/src/transactions/time_lock.rs +++ b/consensus/src/transactions/time_lock.rs @@ -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 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. /// /// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time -pub fn output_unlocked( +fn output_unlocked( time_lock: &Timelock, - difficulty_cache: &DifficultyCache, current_chain_height: u64, + current_time_lock_timestamp: u64, hf: &HardFork, ) -> bool { match *time_lock { @@ -21,7 +48,7 @@ pub fn output_unlocked( check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height) } 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 } -/// 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. /// /// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp fn check_timestamp_time_lock( unlock_timestamp: u64, - difficulty_cache: &DifficultyCache, - current_chain_height: u64, + current_time_lock_timestamp: u64, hf: &HardFork, ) -> bool { - let timestamp = get_current_timestamp(difficulty_cache, current_chain_height, hf); - timestamp + hf.block_time().as_secs() >= unlock_timestamp + current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp } diff --git a/database/src/types.rs b/database/src/types.rs index f7052d64..a4ca8b8c 100644 --- a/database/src/types.rs +++ b/database/src/types.rs @@ -57,12 +57,12 @@ pub struct AltBlock { // ---- TRANSACTIONS ---- #[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. pub struct TransactionPruned { /// The transaction prefix. 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, } @@ -80,7 +80,7 @@ impl bincode::Decode for TransactionPruned { // Handle the prefix accordingly to its 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 { prefix, rct_signatures: RctSig { sig: None, p: None }, @@ -94,7 +94,7 @@ impl bincode::Decode for TransactionPruned { 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) .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); writer.write(&buf)?; 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 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); writer.write(&buf)?; }