diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index c02057b1..550f30d2 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -27,7 +27,8 @@ binaries = [ ] [dependencies] -hex = "0.4" +monero-consensus = {path = "./rules", features = ["rayon"]} + thiserror = "1" tower = {version = "0.4", features = ["util"]} tracing = "0.1" @@ -37,18 +38,19 @@ crypto-bigint = "0.5" curve25519-dalek = "4" randomx-rs = "1" -monero-serai = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"} +monero-serai = { workspace = true } multiexp = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"} dalek-ff-group = {git="https://github.com/cuprate/serai.git", rev = "4a5d860"} cuprate-common = {path = "../common"} -cryptonight-cuprate = {path = "../cryptonight"} rayon = "1" thread_local = "1.1.7" tokio = "1" tokio-util = "0.7" +hex = "0.4" + # used in binaries monero-wire = {path="../net/monero-wire", optional = true} monero-epee-bin-serde = {git = "https://github.com/monero-rs/monero-epee-bin-serde.git", rev = "e4a585a", optional = true} diff --git a/consensus/rules/Cargo.toml b/consensus/rules/Cargo.toml index 10681771..50952f78 100644 --- a/consensus/rules/Cargo.toml +++ b/consensus/rules/Cargo.toml @@ -9,11 +9,16 @@ proptest = ["dep:proptest", "dep:proptest-derive"] rayon = ["dep:rayon"] [dependencies] +cryptonight-cuprate = {path = "../../cryptonight"} +cuprate-common = {path = "../../common"} + monero-serai = { workspace = true } curve25519-dalek = { workspace = true } -tracing = { workspace = true } +hex = "0.4" +primitive-types = { version = "0.12.2", default-features = false } +tracing = { workspace = true } thiserror = { workspace = true } rayon = { workspace = true, optional = true } diff --git a/consensus/rules/src/blocks.rs b/consensus/rules/src/blocks.rs new file mode 100644 index 00000000..7f633d3c --- /dev/null +++ b/consensus/rules/src/blocks.rs @@ -0,0 +1,186 @@ +use monero_serai::block::Block; +use primitive_types::U256; + +use cryptonight_cuprate::*; + +use crate::{ + current_time, + hard_forks::HardForkError, + miner_tx::{check_miner_tx, MinerTxError}, + HardFork, +}; + +const BLOCK_SIZE_SANITY_LEEWAY: usize = 100; +const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum BlockError { + #[error("The blocks POW is invalid.")] + POWInvalid, + #[error("The block is too big.")] + TooLarge, + #[error("The block has too many transactions.")] + TooManyTxs, + #[error("The blocks previous ID is incorrect.")] + PreviousIDIncorrect, + #[error("The blocks timestamp is invalid.")] + TimeStampInvalid, + #[error("Hard-fork error: {0}")] + HardForkError(#[from] HardForkError), + #[error("Miner transaction error: {0}")] + MinerTxError(#[from] MinerTxError), +} + +/// Calculates the POW hash of this block. +pub fn calculate_pow_hash(buf: &[u8], height: u64, hf: &HardFork) -> Result<[u8; 32], BlockError> { + if height == 202612 { + return Ok( + hex::decode("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000") + .unwrap() + .try_into() + .unwrap(), + ); + } + + Ok(if hf < &HardFork::V7 { + cryptonight_hash_v0(buf) + } else if hf == &HardFork::V7 { + cryptonight_hash_v1(buf).map_err(|_| BlockError::POWInvalid)? + } else if hf < &HardFork::V10 { + cryptonight_hash_v2(buf) + } else if hf < &HardFork::V12 { + cryptonight_hash_r(buf, height) + } else { + todo!("RandomX") + }) +} + +/// Returns if the blocks POW hash is valid for the current difficulty. +/// +/// See: https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#checking-a-blocks-proof-of-work +pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> { + let int_hash = U256::from_little_endian(hash); + + let difficulty = U256::from(difficulty); + + if int_hash.checked_mul(difficulty).is_none() { + Err(BlockError::POWInvalid) + } else { + Ok(()) + } +} + +/// Sanity check on the block blob size. +/// +/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-size +fn block_size_sanity_check( + block_blob_len: usize, + effective_median: usize, +) -> Result<(), BlockError> { + if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY { + Err(BlockError::TooLarge) + } else { + Ok(()) + } +} + +/// Sanity check on number of txs in the block. +/// +/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#amount-of-transactions +fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> { + if number_none_miner_txs + 1 > 0x10000000 { + Err(BlockError::TooManyTxs) + } else { + Ok(()) + } +} + +/// Sanity check on the block weight. +/// +/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-siz +fn check_block_weight( + block_weight: usize, + median_for_block_reward: usize, +) -> Result<(), BlockError> { + if block_weight > median_for_block_reward * 2 { + Err(BlockError::TooLarge) + } else { + Ok(()) + } +} + +/// Verifies the previous id is the last blocks hash +/// +/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#previous-id +fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { + if &block.header.previous != top_hash { + Err(BlockError::PreviousIDIncorrect) + } else { + Ok(()) + } +} + +/// Checks the blocks timestamp is in the valid range. +/// +/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#timestamp +fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { + if block.header.timestamp < median_timestamp + || block.header.timestamp > current_time() + BLOCK_FUTURE_TIME_LIMIT + { + Err(BlockError::TimeStampInvalid) + } else { + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct ContextToVerifyBlock { + pub median_weight_for_block_reward: usize, + pub effective_median_weight: usize, + pub top_hash: [u8; 32], + pub median_block_timestamp: Option, + pub chain_height: u64, + pub current_hf: HardFork, + pub next_difficulty: u128, + pub already_generated_coins: u64, +} + +/// Checks the block is valid returning the blocks hard-fork vote and the amount of coins generated. +/// +/// Does not check the proof of work as that check is expensive and should be done last. +pub fn check_block( + block: &Block, + total_fees: u64, + block_weight: usize, + block_blob_len: usize, + block_chain_ctx: &ContextToVerifyBlock, +) -> Result<(HardFork, u64), BlockError> { + let (version, vote) = HardFork::from_block_header(&block.header)?; + + block_chain_ctx + .current_hf + .check_block_version_vote(&version, &vote)?; + + if let Some(median_timestamp) = block_chain_ctx.median_block_timestamp { + check_timestamp(block, median_timestamp)?; + } + + check_prev_id(block, &block_chain_ctx.top_hash)?; + + check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?; + block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?; + + check_amount_txs(block.txs.len())?; + + let generated_coins = check_miner_tx( + &block.miner_tx, + total_fees, + block_chain_ctx.chain_height, + block_weight, + block_chain_ctx.median_weight_for_block_reward, + block_chain_ctx.already_generated_coins, + &block_chain_ctx.current_hf, + )?; + + Ok((vote, generated_coins)) +} diff --git a/consensus/src/genesis.rs b/consensus/rules/src/genesis.rs similarity index 100% rename from consensus/src/genesis.rs rename to consensus/rules/src/genesis.rs diff --git a/consensus/rules/src/hard_forks.rs b/consensus/rules/src/hard_forks.rs index 81bb65ef..43faa6d7 100644 --- a/consensus/rules/src/hard_forks.rs +++ b/consensus/rules/src/hard_forks.rs @@ -6,6 +6,7 @@ //! This module also contains a [`HFVotes`] struct which keeps track of current blockchain voting, and //! has a method [`HFVotes::check_next_hard_fork`] to check if the next hard-fork should be activated. //! +use monero_serai::block::BlockHeader; use std::{ collections::VecDeque, fmt::{Display, Formatter}, @@ -103,7 +104,7 @@ impl HardFork { /// Returns the hard-fork for a blocks `major_version` field. /// /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote - pub fn from_version(version: &u8) -> Result { + pub fn from_version(version: u8) -> Result { Ok(match version { 1 => HardFork::V1, 2 => HardFork::V2, @@ -128,8 +129,8 @@ impl HardFork { /// Returns the hard-fork for a blocks `minor_version` (vote) field. /// /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote - pub fn from_vote(vote: &u8) -> HardFork { - if *vote == 0 { + pub fn from_vote(vote: u8) -> HardFork { + if vote == 0 { // A vote of 0 is interpreted as 1 as that's what Monero used to default to. return HardFork::V1; } @@ -137,9 +138,16 @@ impl HardFork { Self::from_version(vote).unwrap_or(HardFork::V16) } + pub fn from_block_header(header: &BlockHeader) -> Result<(HardFork, HardFork), HardForkError> { + Ok(( + HardFork::from_version(header.major_version)?, + HardFork::from_vote(header.minor_version), + )) + } + /// Returns the next hard-fork. pub fn next_fork(&self) -> Option { - HardFork::from_version(&(*self as u8 + 1)).ok() + HardFork::from_version(*self as u8 + 1).ok() } /// Returns the target block time for this hardfork. @@ -154,14 +162,15 @@ impl HardFork { /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote pub fn check_block_version_vote( - current_hf: &Self, + &self, version: &HardFork, vote: &HardFork, ) -> Result<(), HardForkError> { - if current_hf != version { + // self = current hf + if self != version { Err(HardForkError::VersionIncorrect)?; } - if current_hf < vote { + if self < vote { Err(HardForkError::VoteTooLow)?; } @@ -235,7 +244,7 @@ impl HFVotes { /// Checks if a future hard fork should be activated, returning the next hard-fork that should be /// activated. /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork + /// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#accepting-a-fork pub fn check_next_hard_fork( &self, current_hf: &HardFork, diff --git a/consensus/rules/src/lib.rs b/consensus/rules/src/lib.rs index 1f50dd88..5029aa6b 100644 --- a/consensus/rules/src/lib.rs +++ b/consensus/rules/src/lib.rs @@ -1,26 +1,25 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub mod blocks; mod decomposed_amount; +pub mod genesis; mod hard_forks; -mod miner_tx; -mod signatures; -mod transactions; +pub mod miner_tx; +pub mod signatures; +pub mod transactions; pub use decomposed_amount::is_decomposed_amount; pub use hard_forks::{HFVotes, HFsInfo, HardFork}; +pub use transactions::TxVersion; -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum TxVersion { - RingSignatures, - RingCT, -} - -impl TxVersion { - pub fn from_raw(version: u64) -> Option { - Some(match version { - 1 => TxVersion::RingSignatures, - 2 => TxVersion::RingCT, - _ => return None, - }) - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum ConsensusError { + #[error("Block error: {0}")] + Block(#[from] blocks::BlockError), + #[error("Transaction error: {0}")] + Transaction(#[from] transactions::TransactionError), + #[error("Signatures error: {0}")] + Signatures(#[from] signatures::SignatureError), } /// Checks that a point is canonical. @@ -36,6 +35,13 @@ fn check_point(point: &curve25519_dalek::edwards::CompressedEdwardsY) -> bool { .is_some() } +pub(crate) fn current_time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + #[cfg(feature = "rayon")] fn try_par_iter(t: T) -> T::Iter where diff --git a/consensus/rules/src/signatures/ring_signatures.rs b/consensus/rules/src/signatures/ring_signatures.rs index 4ef296e7..1a1fb1c4 100644 --- a/consensus/rules/src/signatures/ring_signatures.rs +++ b/consensus/rules/src/signatures/ring_signatures.rs @@ -12,7 +12,7 @@ use monero_serai::{ring_signatures::RingSignature, transaction::Input}; use rayon::prelude::*; use super::{Rings, SignatureError}; -use crate::par_iter; +use crate::try_par_iter; /// Verifies the ring signature. /// @@ -31,7 +31,7 @@ pub fn verify_inputs_signatures( return Err(SignatureError::MismatchSignatureSize); } - par_iter(inputs) + try_par_iter(inputs) .zip(rings) .zip(signatures) .try_for_each(|((input, ring), sig)| { diff --git a/consensus/rules/src/time_locks.rs b/consensus/rules/src/time_locks.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/consensus/rules/src/transactions.rs b/consensus/rules/src/transactions.rs index d8ab32da..b85707aa 100644 --- a/consensus/rules/src/transactions.rs +++ b/consensus/rules/src/transactions.rs @@ -2,13 +2,15 @@ use std::{cmp::Ordering, collections::HashSet, sync::Arc}; use monero_serai::transaction::{Input, Output, Timelock}; -use crate::{check_point, is_decomposed_amount, HardFork, TxVersion}; +use crate::{check_point, is_decomposed_amount, HardFork}; mod contextual_data; pub use contextual_data::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum TransactionError { + #[error("The transactions version is incorrect.")] + TransactionVersionInvalid, //-------------------------------------------------------- OUTPUTS #[error("Output is not a valid point.")] OutputNotValidPoint, @@ -20,6 +22,8 @@ pub enum TransactionError { AmountNotDecomposed, #[error("The transactions outputs overflow.")] OutputsOverflow, + #[error("The transactions outputs too much.")] + OutputsTooHigh, //-------------------------------------------------------- INPUTS #[error("One or more inputs don't have the expected number of decoys.")] InputDoesNotHaveExpectedNumbDecoys, @@ -45,6 +49,22 @@ pub enum TransactionError { RingMemberNotFound, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum TxVersion { + RingSignatures, + RingCT, +} + +impl TxVersion { + pub fn from_raw(version: u64) -> Option { + Some(match version { + 1 => TxVersion::RingSignatures, + 2 => TxVersion::RingCT, + _ => return None, + }) + } +} + //----------------------------------------------------------------------------------------------------------- OUTPUTS /// Checks the output keys are canonical points. @@ -426,3 +446,48 @@ pub fn check_inputs( _ => panic!("TODO: RCT"), } } + +/// Checks the version is in the allowed range. +/// +/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#version +pub fn check_tx_version( + decoy_info: &Option, + version: &TxVersion, + hf: &HardFork, +) -> Result<(), TransactionError> { + if let Some(decoy_info) = decoy_info { + let max = max_tx_version(hf); + if version > &max { + return Err(TransactionError::TransactionVersionInvalid); + } + + // TODO: Doc is wrong here + let min = min_tx_version(hf); + if version < &min && decoy_info.not_mixable != 0 { + return Err(TransactionError::TransactionVersionInvalid); + } + } else { + // This will only happen for hard-fork 1 when only RingSignatures are allowed. + if version != &TxVersion::RingSignatures { + return Err(TransactionError::TransactionVersionInvalid); + } + } + + Ok(()) +} + +fn max_tx_version(hf: &HardFork) -> TxVersion { + if hf <= &HardFork::V3 { + TxVersion::RingSignatures + } else { + TxVersion::RingCT + } +} + +fn min_tx_version(hf: &HardFork) -> TxVersion { + if hf >= &HardFork::V6 { + TxVersion::RingCT + } else { + TxVersion::RingSignatures + } +} diff --git a/consensus/rules/src/transactions/contextual_data.rs b/consensus/rules/src/transactions/contextual_data.rs index 07cf5a2b..5d0fd70c 100644 --- a/consensus/rules/src/transactions/contextual_data.rs +++ b/consensus/rules/src/transactions/contextual_data.rs @@ -11,10 +11,10 @@ use crate::{transactions::TransactionError, HardFork, TxVersion}; /// An already approved previous transaction output. #[derive(Debug)] pub struct OutputOnChain { - height: u64, - time_lock: Timelock, - key: EdwardsPoint, - mask: EdwardsPoint, + pub height: u64, + pub time_lock: Timelock, + pub key: EdwardsPoint, + pub mask: EdwardsPoint, } /// Gets the absolute offsets from the relative offsets. @@ -67,7 +67,7 @@ pub fn insert_ring_member_ids( /// 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>( +pub fn get_ring_members_for_inputs<'a>( outputs: &'a HashMap>, inputs: &[Input], ) -> Result>, TransactionError> { @@ -139,16 +139,18 @@ pub struct TxRingMembersInfo { pub decoy_info: Option, pub youngest_used_out_height: u64, pub time_locked_outs: Vec, + pub hf: HardFork, } impl TxRingMembersInfo { /// Construct a [`TxRingMembersInfo`] struct. /// /// The used outs must be all the ring members used in the transactions inputs. - fn new( + pub fn new( used_outs: Vec>, decoy_info: Option, tx_version: TxVersion, + hf: HardFork, ) -> TxRingMembersInfo { TxRingMembersInfo { youngest_used_out_height: used_outs @@ -175,6 +177,7 @@ impl TxRingMembersInfo { .collect::>() }) .collect(), + hf, rings: Rings::new(used_outs, tx_version), decoy_info, } @@ -216,7 +219,7 @@ impl DecoyInfo { /// pub fn new( inputs: &[Input], - outputs_with_amount: &[Option], + outputs_with_amount: &HashMap, hf: &HardFork, ) -> Result { let mut min_decoys = usize::MAX; @@ -226,10 +229,18 @@ impl DecoyInfo { let minimum_decoys = minimum_decoys(hf); - for (inp, outs_with_amt) in inputs.iter().zip(outputs_with_amount) { + for inp in inputs.iter() { match inp { - Input::ToKey { key_offsets, .. } => { - if let Some(outs_with_amt) = *outs_with_amt { + Input::ToKey { + key_offsets, + amount, + .. + } => { + if let Some(amount) = amount { + let outs_with_amt = *outputs_with_amount + .get(amount) + .expect("outputs_with_amount does not include needed amount."); + // https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs if outs_with_amt <= minimum_decoys { not_mixable += 1; diff --git a/consensus/src/bin/scan_chain.rs b/consensus/src/bin/scan_chain.rs index c74f11c5..5df30142 100644 --- a/consensus/src/bin/scan_chain.rs +++ b/consensus/src/bin/scan_chain.rs @@ -14,7 +14,7 @@ use tracing::level_filters::LevelFilter; use cuprate_common::Network; -use monero_consensus::{ +use cuprate_consensus::{ context::{ BlockChainContextRequest, BlockChainContextResponse, ContextConfig, UpdateBlockchainCacheData, @@ -170,42 +170,13 @@ where call_blocks(new_tx_chan, block_tx, start_height, chain_height, database).await }); - let (mut prepared_blocks_tx, mut prepared_blocks_rx) = mpsc::channel(3); - - let mut cloned_block_verifier = block_verifier.clone(); - tokio::spawn(async move { - while let Some(mut next_blocks) = incoming_blocks.next().await { - while !next_blocks.is_empty() { - tracing::info!( - "preparing next batch, number of blocks: {}", - next_blocks.len().min(150) - ); - - let res = cloned_block_verifier - .ready() - .await? - .call(VerifyBlockRequest::BatchSetup( - next_blocks.drain(0..next_blocks.len().min(150)).collect(), - )) - .await; - - prepared_blocks_tx.send(res).await.unwrap(); - } - } - - Result::<_, tower::BoxError>::Ok(()) - }); - - while let Some(prepared_blocks) = prepared_blocks_rx.next().await { - let VerifyBlockResponse::BatchSetup(prepared_blocks) = prepared_blocks? else { - panic!("block verifier sent incorrect response!"); - }; + while let Some(incoming_blocks) = incoming_blocks.next().await { let mut height = 0; - for block in prepared_blocks { + for block in incoming_blocks { let VerifyBlockResponse::MainChain(verified_block_info) = block_verifier .ready() .await? - .call(VerifyBlockRequest::MainChainPreparedBlock(block)) + .call(VerifyBlockRequest::MainChain(block)) .await? else { panic!("Block verifier sent incorrect response!"); @@ -219,13 +190,15 @@ where } update_cache_and_context(&cache, &mut ctx_svc, verified_block_info).await?; - } - tracing::info!( - "verified blocks: {:?}, chain height: {}", - 0..height, - chain_height - ); + if height % 200 == 0 { + tracing::info!( + "verified blocks: {:?}, chain height: {}", + 0..height, + chain_height + ); + } + } } Ok(()) diff --git a/consensus/src/bin/tx_pool.rs b/consensus/src/bin/tx_pool.rs index 516918a7..15efb881 100644 --- a/consensus/src/bin/tx_pool.rs +++ b/consensus/src/bin/tx_pool.rs @@ -15,13 +15,13 @@ use tower::{Service, ServiceExt}; use cuprate_common::tower_utils::InfallibleOneshotReceiver; -use monero_consensus::{ +use cuprate_consensus::{ context::{ BlockChainContext, BlockChainContextRequest, BlockChainContextResponse, RawBlockChainContext, }, transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, - ConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse, + ExtendedConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse, }; #[derive(Clone)] @@ -78,7 +78,7 @@ pub struct TxPool { impl TxPool where - TxV: Service + TxV: Service + Clone + Send + 'static, @@ -199,7 +199,7 @@ where .unwrap() .call(VerifyTxRequest::BatchSetup { txs: new_txs, - hf: current_ctx.current_hard_fork, + hf: current_ctx.current_hf, re_org_token: current_ctx.re_org_token.clone(), }) .await diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 61278196..2765cd10 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -7,22 +7,20 @@ use std::{ use futures::FutureExt; use monero_serai::{block::Block, transaction::Input}; -use rayon::prelude::*; use tower::{Service, ServiceExt}; +use monero_consensus::{ + blocks::{calculate_pow_hash, check_block, check_block_pow}, + ConsensusError, HardFork, +}; + use crate::{ context::{BlockChainContextRequest, BlockChainContextResponse}, helper::rayon_spawn_async, transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, - ConsensusError, HardFork, TxNotInPool, TxPoolRequest, TxPoolResponse, + ExtendedConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse, }; -mod checks; -mod hash_worker; -mod miner_tx; - -use hash_worker::calculate_pow_hash; - #[derive(Debug)] pub struct PrePreparedBlock { pub block: Block, @@ -53,9 +51,6 @@ pub struct VerifiedBlockInformation { pub enum VerifyBlockRequest { MainChain(Block), - - BatchSetup(Vec), - MainChainPreparedBlock(PrePreparedBlock), } pub enum VerifyBlockResponse { @@ -79,7 +74,7 @@ where + Clone + Send + 'static, - TxV: Service + TxV: Service + Clone + Send + 'static, @@ -112,7 +107,7 @@ where + 'static, C::Future: Send + 'static, - TxV: Service + TxV: Service + Clone + Send + 'static, @@ -125,7 +120,7 @@ where TxP::Future: Send + 'static, { type Response = VerifyBlockResponse; - type Error = ConsensusError; + type Error = ExtendedConsensusError; type Future = Pin> + Send + 'static>>; @@ -143,158 +138,18 @@ where VerifyBlockRequest::MainChain(block) => { verify_main_chain_block(block, context_svc, tx_verifier_svc, tx_pool).await } - VerifyBlockRequest::BatchSetup(blocks) => batch_prepare_block(blocks).await, - VerifyBlockRequest::MainChainPreparedBlock(block) => { - verify_prepared_main_chain_block(block, context_svc, tx_verifier_svc, tx_pool) - .await - } } } .boxed() } } -async fn batch_prepare_block(blocks: Vec) -> Result { - Ok(VerifyBlockResponse::BatchSetup( - rayon_spawn_async(move || { - blocks - .into_par_iter() - .map(prepare_block) - .collect::, _>>() - }) - .await?, - )) -} - -fn prepare_block(block: Block) -> Result { - let hf_version = HardFork::from_version(&block.header.major_version)?; - let hf_vote = HardFork::from_vote(&block.header.major_version); - - let height = match block.miner_tx.prefix.inputs.get(0) { - Some(Input::Gen(height)) => *height, - _ => { - return Err(ConsensusError::MinerTransaction( - "Input is not a miner input", - )) - } - }; - - tracing::debug!("preparing block: {}", height); - - Ok(PrePreparedBlock { - block_blob: block.serialize(), - block_hash: block.hash(), - pow_hash: calculate_pow_hash(&block.serialize_hashable(), height, &hf_version)?, - miner_tx_weight: block.miner_tx.weight(), - block, - hf_vote, - hf_version, - }) -} - -async fn verify_prepared_main_chain_block( - block: PrePreparedBlock, - context_svc: C, - tx_verifier_svc: TxV, - tx_pool: TxP, -) -> Result -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Send - + 'static, - C::Future: Send + 'static, - TxV: Service, - TxP: Service - + Clone - + Send - + 'static, -{ - tracing::debug!("getting blockchain context"); - let BlockChainContextResponse::Context(checked_context) = context_svc - .oneshot(BlockChainContextRequest::Get) - .await - .map_err(Into::::into)? - else { - panic!("Context service returned wrong response!"); - }; - - let context = checked_context.unchecked_blockchain_context().clone(); - - tracing::debug!("got blockchain context: {:?}", context); - - let txs = if !block.block.txs.is_empty() { - let TxPoolResponse::Transactions(txs) = tx_pool - .oneshot(TxPoolRequest::Transactions(block.block.txs.clone())) - .await?; - txs - } else { - vec![] - }; - - let block_weight = block.miner_tx_weight + txs.iter().map(|tx| tx.tx_weight).sum::(); - let total_fees = txs.iter().map(|tx| tx.fee).sum::(); - - if !txs.is_empty() { - tx_verifier_svc - .oneshot(VerifyTxRequest::Block { - txs: txs.clone(), - current_chain_height: context.chain_height, - time_for_time_lock: context.current_adjusted_timestamp_for_time_lock(), - hf: context.current_hard_fork, - re_org_token: context.re_org_token.clone(), - }) - .await?; - } - - let generated_coins = miner_tx::check_miner_tx( - &block.block.miner_tx, - total_fees, - context.chain_height, - block_weight, - context.median_weight_for_block_reward, - context.already_generated_coins, - &context.current_hard_fork, - )?; - - checks::block_size_sanity_check(block.block_blob.len(), context.effective_median_weight)?; - checks::block_weight_check(block_weight, context.median_weight_for_block_reward)?; - - checks::check_amount_txs(block.block.txs.len())?; - checks::check_prev_id(&block.block, &context.top_hash)?; - if let Some(median_timestamp) = context.median_block_timestamp { - // will only be None for the first 60 blocks - checks::check_timestamp(&block.block, median_timestamp)?; - } - - checks::check_block_pow(&block.pow_hash, context.next_difficulty)?; - - context - .current_hard_fork - .check_block_version_vote(&block.block.header)?; - - Ok(VerifyBlockResponse::MainChain(VerifiedBlockInformation { - block_hash: block.block_hash, - block: block.block, - txs, - pow_hash: block.pow_hash, - generated_coins, - weight: block_weight, - height: context.chain_height, - long_term_weight: context.next_block_long_term_weight(block_weight), - hf_vote: block.hf_vote, - cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty, - })) -} - async fn verify_main_chain_block( block: Block, context_svc: C, tx_verifier_svc: TxV, tx_pool: TxP, -) -> Result +) -> Result where C: Service< BlockChainContextRequest, @@ -303,7 +158,7 @@ where > + Send + 'static, C::Future: Send + 'static, - TxV: Service, + TxV: Service, TxP: Service + Clone + Send @@ -313,7 +168,7 @@ where let BlockChainContextResponse::Context(checked_context) = context_svc .oneshot(BlockChainContextRequest::Get) .await - .map_err(Into::::into)? + .map_err(Into::::into)? else { panic!("Context service returned wrong response!"); }; @@ -326,57 +181,39 @@ where .oneshot(TxPoolRequest::Transactions(block.txs.clone())) .await?; - let block_weight = block.miner_tx.weight() + txs.iter().map(|tx| tx.tx_weight).sum::(); - let total_fees = txs.iter().map(|tx| tx.fee).sum::(); - tx_verifier_svc .oneshot(VerifyTxRequest::Block { txs: txs.clone(), 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_hf, re_org_token: context.re_org_token.clone(), }) .await?; - let generated_coins = miner_tx::check_miner_tx( - &block.miner_tx, + let block_weight = block.miner_tx.weight() + txs.iter().map(|tx| tx.tx_weight).sum::(); + let total_fees = txs.iter().map(|tx| tx.fee).sum::(); + + let (hf_vote, generated_coins) = check_block( + &block, total_fees, - context.chain_height, block_weight, - context.median_weight_for_block_reward, - context.already_generated_coins, - &context.current_hard_fork, - )?; + block.serialize().len(), + &context.context_to_verify_block, + ) + .map_err(ConsensusError::Block)?; let hashing_blob = block.serialize_hashable(); - checks::block_size_sanity_check(block.serialize().len(), context.effective_median_weight)?; - checks::block_weight_check(block_weight, context.median_weight_for_block_reward)?; - - checks::check_amount_txs(block.txs.len())?; - checks::check_prev_id(&block, &context.top_hash)?; - if let Some(median_timestamp) = context.median_block_timestamp { - // will only be None for the first 60 blocks - checks::check_timestamp(&block, median_timestamp)?; - } - // do POW test last - let pow_hash = tokio::task::spawn_blocking(move || { - hash_worker::calculate_pow_hash( - &hashing_blob, - context.chain_height, - &context.current_hard_fork, - ) - }) - .await - .unwrap()?; + let chain_height = context.chain_height; + let current_hf = context.current_hf; + let pow_hash = + rayon_spawn_async(move || calculate_pow_hash(&hashing_blob, chain_height, ¤t_hf)) + .await + .map_err(ConsensusError::Block)?; - checks::check_block_pow(&pow_hash, context.next_difficulty)?; - - context - .current_hard_fork - .check_block_version_vote(&block.header)?; + check_block_pow(&pow_hash, context.next_difficulty).map_err(ConsensusError::Block)?; Ok(VerifyBlockResponse::MainChain(VerifiedBlockInformation { block_hash: block.hash(), @@ -387,7 +224,7 @@ where weight: block_weight, height: context.chain_height, long_term_weight: context.next_block_long_term_weight(block_weight), - hf_vote: HardFork::V1, + hf_vote, cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty, })) } diff --git a/consensus/src/block/checks.rs b/consensus/src/block/checks.rs deleted file mode 100644 index b12ddb1d..00000000 --- a/consensus/src/block/checks.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crypto_bigint::{CheckedMul, U256}; -use monero_serai::block::Block; - -use crate::{helper::current_time, ConsensusError}; - -const BLOCK_SIZE_SANITY_LEEWAY: usize = 100; -const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2; - -/// Returns if the blocks POW hash is valid for the current difficulty. -/// -/// See: https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#checking-a-blocks-proof-of-work -pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), ConsensusError> { - let int_hash = U256::from_le_slice(hash); - - let difficulty = U256::from_u128(difficulty); - - if int_hash.checked_mul(&difficulty).is_some().unwrap_u8() != 1 { - Err(ConsensusError::BlockPOWInvalid) - } else { - Ok(()) - } -} - -/// Sanity check on the block blob size. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-size -pub fn block_size_sanity_check( - block_blob_len: usize, - effective_median: usize, -) -> Result<(), ConsensusError> { - if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY { - Err(ConsensusError::BlockIsTooLarge) - } else { - Ok(()) - } -} - -/// Sanity check on number of txs in the block. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#amount-of-transactions -pub fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), ConsensusError> { - if number_none_miner_txs + 1 > 0x10000000 { - Err(ConsensusError::BlockIsTooLarge) - } else { - Ok(()) - } -} - -/// Sanity check on the block weight. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-siz -pub fn block_weight_check( - block_weight: usize, - median_for_block_reward: usize, -) -> Result<(), ConsensusError> { - if block_weight > median_for_block_reward * 2 { - Err(ConsensusError::BlockIsTooLarge) - } else { - Ok(()) - } -} - -/// Verifies the previous id is the last blocks hash -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#previous-id -pub fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), ConsensusError> { - if &block.header.previous != top_hash { - Err(ConsensusError::BlockIsNotApartOfChain) - } else { - Ok(()) - } -} - -/// Checks the blocks timestamp is in the valid range. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#timestamp -pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), ConsensusError> { - if block.header.timestamp < median_timestamp - || block.header.timestamp > current_time() + BLOCK_FUTURE_TIME_LIMIT - { - Err(ConsensusError::BlockTimestampInvalid) - } else { - Ok(()) - } -} diff --git a/consensus/src/block/hash_worker.rs b/consensus/src/block/hash_worker.rs deleted file mode 100644 index a3a0f769..00000000 --- a/consensus/src/block/hash_worker.rs +++ /dev/null @@ -1,33 +0,0 @@ -use cryptonight_cuprate::{ - cryptonight_hash_r, cryptonight_hash_v0, cryptonight_hash_v1, cryptonight_hash_v2, -}; - -use crate::{ConsensusError, HardFork}; - -/// Calcualtes the POW hash of this block. -pub fn calculate_pow_hash( - buf: &[u8], - height: u64, - hf: &HardFork, -) -> Result<[u8; 32], ConsensusError> { - if height == 202612 { - return Ok( - hex::decode("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000") - .unwrap() - .try_into() - .unwrap(), - ); - } - - Ok(if hf.in_range(&HardFork::V1, &HardFork::V7) { - cryptonight_hash_v0(buf) - } else if hf == &HardFork::V7 { - cryptonight_hash_v1(buf).map_err(|_| ConsensusError::BlockPOWInvalid)? - } else if hf.in_range(&HardFork::V8, &HardFork::V10) { - cryptonight_hash_v2(buf) - } else if hf.in_range(&HardFork::V10, &HardFork::V12) { - cryptonight_hash_r(buf, height) - } else { - todo!("RandomX") - }) -} diff --git a/consensus/src/block/miner_tx.rs b/consensus/src/block/miner_tx.rs deleted file mode 100644 index 55b3f78f..00000000 --- a/consensus/src/block/miner_tx.rs +++ /dev/null @@ -1,183 +0,0 @@ -use monero_serai::ringct::RctType; -use monero_serai::transaction::{Input, Output, Timelock, Transaction}; - -use crate::{ - transactions::{ - outputs::{check_output_types, is_decomposed_amount}, - TxVersion, - }, - ConsensusError, HardFork, -}; - -const MONEY_SUPPLY: u64 = u64::MAX; -const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11); - -const MINER_TX_TIME_LOCKED_BLOCKS: u64 = 60; - -fn calculate_base_reward(already_generated_coins: u64, hf: &HardFork) -> u64 { - let target_mins = hf.block_time().as_secs() / 60; - let emission_speed_factor = 20 - (target_mins - 1); - ((MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor) - .max(MINIMUM_REWARD_PER_MIN * target_mins) -} - -pub fn calculate_block_reward( - block_weight: usize, - median_bw: usize, - already_generated_coins: u64, - hf: &HardFork, -) -> u64 { - let base_reward: u128 = calculate_base_reward(already_generated_coins, hf).into(); - - if block_weight <= median_bw { - return base_reward.try_into().unwrap(); - } - - let multiplicand: u128 = ((2 * median_bw - block_weight) * block_weight) - .try_into() - .unwrap(); - let effective_median_bw: u128 = median_bw.try_into().unwrap(); - - (((base_reward * multiplicand) / effective_median_bw) / effective_median_bw) - .try_into() - .unwrap() -} - -/// Checks the miner transactions version. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#version -fn check_miner_tx_version(tx_version: &TxVersion, hf: &HardFork) -> Result<(), ConsensusError> { - // The TxVersion enum checks if the version is not 1 or 2 - if hf >= &HardFork::V12 && tx_version != &TxVersion::RingCT { - Err(ConsensusError::MinerTransaction("Version invalid")) - } else { - Ok(()) - } -} - -/// Checks the miner transactions inputs. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#input -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#height -fn check_inputs(inputs: &[Input], chain_height: u64) -> Result<(), ConsensusError> { - if inputs.len() != 1 { - return Err(ConsensusError::MinerTransaction( - "does not have exactly 1 input", - )); - } - - match &inputs[0] { - Input::Gen(height) => { - if height != &chain_height { - Err(ConsensusError::MinerTransaction( - "Height in input is not expected height", - )) - } else { - Ok(()) - } - } - _ => Err(ConsensusError::MinerTransaction("Input not of type Gen")), - } -} - -/// Checks the miner transaction has a correct time lock. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#unlock-time -fn check_time_lock(time_lock: &Timelock, chain_height: u64) -> Result<(), ConsensusError> { - match time_lock { - Timelock::Block(till_height) => { - if u64::try_from(*till_height).unwrap() != chain_height + MINER_TX_TIME_LOCKED_BLOCKS { - tracing::warn!("{}, {}", till_height, chain_height); - Err(ConsensusError::MinerTransaction( - "Time lock has invalid block height", - )) - } else { - Ok(()) - } - } - _ => Err(ConsensusError::MinerTransaction( - "Time lock is not a block height", - )), - } -} - -/// Sums the outputs checking for overflow. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts -fn sum_outputs(outputs: &[Output], hf: &HardFork) -> Result { - let mut sum: u64 = 0; - for out in outputs { - let amt = out.amount.unwrap_or(0); - if hf == &HardFork::V3 && !is_decomposed_amount(amt) { - return Err(ConsensusError::MinerTransaction( - "output amount is not decomposed", - )); - } - sum = sum - .checked_add(amt) - .ok_or(ConsensusError::MinerTransaction( - "outputs overflow when summed", - ))?; - } - Ok(sum) -} - -/// Checks the total outputs amount is correct returning the amount of coins collected by the miner. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#total-outputs -fn check_total_output_amt( - total_output: u64, - reward: u64, - fees: u64, - hf: &HardFork, -) -> Result { - if hf == &HardFork::V1 || hf >= &HardFork::V12 { - if total_output != reward + fees { - return Err(ConsensusError::MinerTransaction( - "miner transaction does not output correct amt", - )); - } - Ok(reward) - } else { - if total_output - fees > reward { - return Err(ConsensusError::MinerTransaction( - "miner transaction does not output correct amt", - )); - } - - if total_output > reward + fees { - return Err(ConsensusError::MinerTransaction( - "miner transaction does not output correct amt", - )); - } - Ok(total_output - fees) - } -} - -pub fn check_miner_tx( - tx: &Transaction, - total_fees: u64, - chain_height: u64, - block_weight: usize, - median_bw: usize, - already_generated_coins: u64, - hf: &HardFork, -) -> Result { - let tx_version = TxVersion::from_raw(tx.prefix.version)?; - check_miner_tx_version(&tx_version, hf)?; - - if hf >= &HardFork::V12 && tx.rct_signatures.rct_type() != RctType::Null { - return Err(ConsensusError::MinerTransaction("RctType is not null")); - } - - check_time_lock(&tx.prefix.timelock, chain_height)?; - - check_inputs(&tx.prefix.inputs, chain_height)?; - - check_output_types(&tx.prefix.outputs, hf)?; - - let reward = calculate_block_reward(block_weight, median_bw, already_generated_coins, hf); - let total_outs = sum_outputs(&tx.prefix.outputs, hf)?; - - check_total_output_amt(total_outs, reward, total_fees, hf) -} diff --git a/consensus/src/context.rs b/consensus/src/context.rs index 6ab98fda..fa94c5e0 100644 --- a/consensus/src/context.rs +++ b/consensus/src/context.rs @@ -20,8 +20,11 @@ use futures::{ use tower::{Service, ServiceExt}; use cuprate_common::tower_utils::InstaFuture; +use monero_consensus::{blocks::ContextToVerifyBlock, HardFork}; -use crate::{helper::current_time, ConsensusError, Database, DatabaseRequest, DatabaseResponse}; +use crate::{ + helper::current_time, Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError, +}; mod difficulty; mod hardforks; @@ -32,7 +35,7 @@ mod tests; mod tokens; pub use difficulty::DifficultyCacheConfig; -pub use hardforks::{HardFork, HardForkConfig}; +pub use hardforks::HardForkConfig; pub use tokens::*; pub use weight::BlockWeightsCacheConfig; @@ -69,7 +72,7 @@ pub async fn initialize_blockchain_context( + Send + Sync + 'static, - ConsensusError, + ExtendedConsensusError, > where D: Database + Clone + Send + Sync + 'static, @@ -140,30 +143,21 @@ where /// around. You should keep around [`BlockChainContext`] instead. #[derive(Debug, Clone)] pub struct RawBlockChainContext { - /// The next blocks difficulty. - pub next_difficulty: u128, /// The current cumulative difficulty. pub cumulative_difficulty: u128, - /// The current effective median block weight. - pub effective_median_weight: usize, - /// The median long term block weight. - median_long_term_weight: usize, - /// Median weight to use for block reward calculations. - pub median_weight_for_block_reward: usize, - /// The amount of coins minted already. - pub already_generated_coins: 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 - pub top_hash: [u8; 32], - /// The current hard fork. - pub current_hard_fork: HardFork, /// A token which is used to signal if a reorg has happened since creating the token. pub re_org_token: ReOrgToken, + pub context_to_verify_block: ContextToVerifyBlock, + /// The median long term block weight. + median_long_term_weight: usize, + top_block_timestamp: Option, +} + +impl std::ops::Deref for RawBlockChainContext { + type Target = ContextToVerifyBlock; + fn deref(&self) -> &Self::Target { + &self.context_to_verify_block + } } impl RawBlockChainContext { @@ -171,20 +165,19 @@ impl RawBlockChainContext { /// /// 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() { + if self.current_hf < 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() + + (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1) * self.current_hf.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(); + self.top_block_timestamp.unwrap() + self.current_hf.block_time().as_secs(); min(adjusted_median, adjusted_top_block) } @@ -200,7 +193,7 @@ impl RawBlockChainContext { pub fn next_block_long_term_weight(&self, block_weight: usize) -> usize { weight::calculate_block_long_term_weight( - &self.current_hard_fork, + &self.current_hf, block_weight, self.median_long_term_weight, ) @@ -346,21 +339,23 @@ impl Service for BlockChainContextService { InstaFuture::from(Ok(BlockChainContextResponse::Context(BlockChainContext { validity_token: current_validity_token.clone(), raw: RawBlockChainContext { - next_difficulty: difficulty_cache.next_difficulty(¤t_hf), + context_to_verify_block: ContextToVerifyBlock { + median_weight_for_block_reward: weight_cache + .median_for_block_reward(¤t_hf), + effective_median_weight: weight_cache + .effective_median_block_weight(¤t_hf), + top_hash: *top_block_hash, + median_block_timestamp: difficulty_cache.median_timestamp( + usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap(), + ), + chain_height: *chain_height, + current_hf, + next_difficulty: difficulty_cache.next_difficulty(¤t_hf), + already_generated_coins: *already_generated_coins, + }, cumulative_difficulty: difficulty_cache.cumulative_difficulty(), - effective_median_weight: weight_cache - .effective_median_block_weight(¤t_hf), 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, top_block_timestamp: difficulty_cache.top_block_timestamp(), - median_block_timestamp: difficulty_cache.median_timestamp( - usize::try_from(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW).unwrap(), - ), - chain_height: *chain_height, - top_hash: *top_block_hash, - current_hard_fork: current_hf, re_org_token: current_reorg_token.clone(), }, }))) diff --git a/consensus/src/context/difficulty.rs b/consensus/src/context/difficulty.rs index f8b380d3..075a3cc0 100644 --- a/consensus/src/context/difficulty.rs +++ b/consensus/src/context/difficulty.rs @@ -4,7 +4,7 @@ use tower::ServiceExt; use tracing::instrument; use crate::{ - helper::median, ConsensusError, Database, DatabaseRequest, DatabaseResponse, HardFork, + helper::median, Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError, HardFork, }; #[cfg(test)] @@ -74,7 +74,7 @@ impl DifficultyCache { chain_height: u64, config: DifficultyCacheConfig, database: D, - ) -> Result { + ) -> Result { tracing::info!("Initializing difficulty cache this may take a while."); let mut block_start = chain_height.saturating_sub(config.total_block_count()); @@ -212,7 +212,7 @@ fn get_window_start_and_end( async fn get_blocks_in_pow_info( database: D, block_heights: Range, -) -> Result<(VecDeque, VecDeque), ConsensusError> { +) -> Result<(VecDeque, VecDeque), ExtendedConsensusError> { tracing::info!("Getting blocks timestamps"); let DatabaseResponse::BlockExtendedHeaderInRange(ext_header) = database diff --git a/consensus/src/context/hardforks.rs b/consensus/src/context/hardforks.rs index 30bfffb0..8f11b942 100644 --- a/consensus/src/context/hardforks.rs +++ b/consensus/src/context/hardforks.rs @@ -1,260 +1,41 @@ -use std::{ - collections::VecDeque, - fmt::{Display, Formatter}, - ops::Range, - time::Duration, -}; +use std::ops::Range; -use monero_serai::block::BlockHeader; use tower::ServiceExt; use tracing::instrument; -use crate::{ConsensusError, Database, DatabaseRequest, DatabaseResponse}; +use monero_consensus::{HFVotes, HFsInfo, HardFork}; + +use crate::{Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError}; #[cfg(test)] pub(super) mod tests; // https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork const DEFAULT_WINDOW_SIZE: u64 = 10080; // supermajority window check length - a week -const BLOCK_TIME_V1: Duration = Duration::from_secs(60); -const BLOCK_TIME_V2: Duration = Duration::from_secs(120); - -const NUMB_OF_HARD_FORKS: usize = 16; - -/// Information about a given hard-fork. -#[derive(Debug, Clone, Copy)] -pub struct HFInfo { - height: u64, - threshold: u64, -} -impl HFInfo { - pub const fn new(height: u64, threshold: u64) -> HFInfo { - HFInfo { height, threshold } - } - - /// Returns the main-net hard-fork information. - /// - /// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#Mainnet-Hard-Forks - pub const fn main_net() -> [HFInfo; NUMB_OF_HARD_FORKS] { - [ - HFInfo::new(0, 0), - HFInfo::new(1009827, 0), - HFInfo::new(1141317, 0), - HFInfo::new(1220516, 0), - HFInfo::new(1288616, 0), - HFInfo::new(1400000, 0), - HFInfo::new(1546000, 0), - HFInfo::new(1685555, 0), - HFInfo::new(1686275, 0), - HFInfo::new(1788000, 0), - HFInfo::new(1788720, 0), - HFInfo::new(1978433, 0), - HFInfo::new(2210000, 0), - HFInfo::new(2210720, 0), - HFInfo::new(2688888, 0), - HFInfo::new(2689608, 0), - ] - } -} /// Configuration for hard-forks. /// #[derive(Debug, Clone)] pub struct HardForkConfig { /// The network we are on. - forks: [HFInfo; NUMB_OF_HARD_FORKS], + info: HFsInfo, /// The amount of votes we are taking into account to decide on a fork activation. window: u64, } impl HardForkConfig { - fn fork_info(&self, hf: &HardFork) -> HFInfo { - self.forks[*hf as usize - 1] - } - pub const fn main_net() -> HardForkConfig { Self { - forks: HFInfo::main_net(), + info: HFsInfo::main_net(), window: DEFAULT_WINDOW_SIZE, } } } -/// An identifier for every hard-fork Monero has had. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] -#[repr(u8)] -pub enum HardFork { - V1 = 1, - V2, - V3, - V4, - V5, - V6, - V7, - V8, - V9, - V10, - V11, - V12, - V13, - V14, - V15, - // remember to update from_vote! - V16, -} - -impl HardFork { - /// Returns the hard-fork for a blocks `major_version` field. - /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote - pub fn from_version(version: &u8) -> Result { - Ok(match version { - 1 => HardFork::V1, - 2 => HardFork::V2, - 3 => HardFork::V3, - 4 => HardFork::V4, - 5 => HardFork::V5, - 6 => HardFork::V6, - 7 => HardFork::V7, - 8 => HardFork::V8, - 9 => HardFork::V9, - 10 => HardFork::V10, - 11 => HardFork::V11, - 12 => HardFork::V12, - 13 => HardFork::V13, - 14 => HardFork::V14, - 15 => HardFork::V15, - 16 => HardFork::V16, - _ => { - return Err(ConsensusError::InvalidHardForkVersion( - "Version is not a known hard fork", - )) - } - }) - } - - /// Returns the hard-fork for a blocks `minor_version` (vote) field. - /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote - pub fn from_vote(vote: &u8) -> HardFork { - if *vote == 0 { - // A vote of 0 is interpreted as 1 as that's what Monero used to default to. - return HardFork::V1; - } - // This must default to the latest hard-fork! - Self::from_version(vote).unwrap_or(HardFork::V16) - } - - /// Returns the next hard-fork. - pub fn next_fork(&self) -> Option { - HardFork::from_version(&(*self as u8 + 1)).ok() - } - - /// Returns if the hard-fork is in range: - /// - /// start <= hf < end - pub fn in_range(&self, start: &HardFork, end: &HardFork) -> bool { - start <= self && self < end - } - - /// Returns the target block time for this hardfork. - pub fn block_time(&self) -> Duration { - match self { - HardFork::V1 => BLOCK_TIME_V1, - _ => BLOCK_TIME_V2, - } - } - - /// Checks a blocks version and vote, assuming that `self` is the current hard-fork. - /// - /// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote - pub fn check_block_version_vote( - &self, - block_header: &BlockHeader, - ) -> Result<(), ConsensusError> { - let version = HardFork::from_version(&block_header.major_version)?; - let vote = HardFork::from_vote(&block_header.minor_version); - - if self == &version && &vote >= self { - Ok(()) - } else { - Err(ConsensusError::InvalidHardForkVersion( - "Block version or vote incorrect", - )) - } - } -} - -/// A struct holding the current voting state of the blockchain. -#[derive(Debug, Clone)] -struct HFVotes { - votes: [u64; NUMB_OF_HARD_FORKS], - vote_list: VecDeque, - window_size: usize, -} - -impl Display for HFVotes { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("HFVotes") - .field("total", &self.total_votes()) - .field("V1", &self.votes_for_hf(&HardFork::V1)) - .field("V2", &self.votes_for_hf(&HardFork::V2)) - .field("V3", &self.votes_for_hf(&HardFork::V3)) - .field("V4", &self.votes_for_hf(&HardFork::V4)) - .field("V5", &self.votes_for_hf(&HardFork::V5)) - .field("V6", &self.votes_for_hf(&HardFork::V6)) - .field("V7", &self.votes_for_hf(&HardFork::V7)) - .field("V8", &self.votes_for_hf(&HardFork::V8)) - .field("V9", &self.votes_for_hf(&HardFork::V9)) - .field("V10", &self.votes_for_hf(&HardFork::V10)) - .field("V11", &self.votes_for_hf(&HardFork::V11)) - .field("V12", &self.votes_for_hf(&HardFork::V12)) - .field("V13", &self.votes_for_hf(&HardFork::V13)) - .field("V14", &self.votes_for_hf(&HardFork::V14)) - .field("V15", &self.votes_for_hf(&HardFork::V15)) - .field("V16", &self.votes_for_hf(&HardFork::V16)) - .finish() - } -} - -impl HFVotes { - pub fn new(window_size: usize) -> HFVotes { - HFVotes { - votes: [0; NUMB_OF_HARD_FORKS], - vote_list: VecDeque::with_capacity(window_size), - window_size, - } - } - - /// Add a vote for a hard-fork, this function removes votes outside of the window. - pub fn add_vote_for_hf(&mut self, hf: &HardFork) { - self.vote_list.push_back(*hf); - self.votes[*hf as usize - 1] += 1; - if self.vote_list.len() > self.window_size { - let hf = self.vote_list.pop_front().unwrap(); - self.votes[hf as usize - 1] -= 1; - } - } - - /// Returns the total votes for a hard-fork. - /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork - pub fn votes_for_hf(&self, hf: &HardFork) -> u64 { - self.votes[*hf as usize - 1..].iter().sum() - } - - /// Returns the total amount of votes being tracked - pub fn total_votes(&self) -> u64 { - self.votes.iter().sum() - } -} - /// A struct that keeps track of the current hard-fork and current votes. #[derive(Debug, Clone)] pub struct HardForkState { current_hardfork: HardFork, - next_hardfork: Option, config: HardForkConfig, votes: HFVotes, @@ -268,7 +49,7 @@ impl HardForkState { chain_height: u64, config: HardForkConfig, mut database: D, - ) -> Result { + ) -> Result { tracing::info!("Initializing hard-fork state this may take a while."); let block_start = chain_height.saturating_sub(config.window); @@ -297,12 +78,9 @@ impl HardForkState { let current_hardfork = ext_header.version; - let next_hardfork = current_hardfork.next_fork(); - let mut hfs = HardForkState { config, current_hardfork, - next_hardfork, votes, last_height: chain_height - 1, }; @@ -341,25 +119,16 @@ impl HardForkState { /// /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork fn check_set_new_hf(&mut self) { - while let Some(new_hf) = self.next_hardfork { - let hf_info = self.config.fork_info(&new_hf); - if self.last_height + 1 >= hf_info.height - && self.votes.votes_for_hf(&new_hf) - >= votes_needed(hf_info.threshold, self.config.window) - { - self.set_hf(new_hf); - } else { - return; - } + if let Some(next_fork) = self.votes.check_next_hard_fork( + &self.current_hardfork, + self.last_height + 1, + self.config.window, + &self.config.info, + ) { + self.current_hardfork = next_fork; } } - /// Sets a new hard-fork. - fn set_hf(&mut self, new_hf: HardFork) { - self.next_hardfork = new_hf.next_fork(); - self.current_hardfork = new_hf; - } - pub fn current_hardfork(&self) -> HardFork { self.current_hardfork } @@ -377,7 +146,7 @@ async fn get_votes_in_range( database: D, block_heights: Range, window_size: usize, -) -> Result { +) -> Result { let mut votes = HFVotes::new(window_size); let DatabaseResponse::BlockExtendedHeaderInRange(vote_list) = database diff --git a/consensus/src/context/weight.rs b/consensus/src/context/weight.rs index d6e5b7ff..5af1796d 100644 --- a/consensus/src/context/weight.rs +++ b/consensus/src/context/weight.rs @@ -18,7 +18,7 @@ use tracing::instrument; use crate::{ helper::{median, rayon_spawn_async}, - ConsensusError, Database, DatabaseRequest, DatabaseResponse, HardFork, + Database, DatabaseRequest, DatabaseResponse, ExtendedConsensusError, HardFork, }; #[cfg(test)] @@ -37,7 +37,7 @@ const LONG_TERM_WINDOW: u64 = 100000; pub fn penalty_free_zone(hf: &HardFork) -> usize { if hf == &HardFork::V1 { PENALTY_FREE_ZONE_1 - } else if hf.in_range(&HardFork::V2, &HardFork::V5) { + } else if hf >= &HardFork::V2 && hf < &HardFork::V5 { PENALTY_FREE_ZONE_2 } else { PENALTY_FREE_ZONE_5 @@ -98,7 +98,7 @@ impl BlockWeightsCache { chain_height: u64, config: BlockWeightsCacheConfig, database: D, - ) -> Result { + ) -> Result { tracing::info!("Initializing weight cache this may take a while."); let long_term_weights = get_long_term_weight_in_range( @@ -230,7 +230,7 @@ impl BlockWeightsCache { /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/reward.html#calculating-block-reward pub fn median_for_block_reward(&self, hf: &HardFork) -> usize { - if hf.in_range(&HardFork::V1, &HardFork::V12) { + if hf < &HardFork::V12 { self.median_short_term_weight() } else { self.effective_median_block_weight(hf) @@ -244,13 +244,13 @@ fn calculate_effective_median_block_weight( median_short_term_weight: usize, median_long_term_weight: usize, ) -> usize { - if hf.in_range(&HardFork::V1, &HardFork::V10) { + if hf < &HardFork::V10 { return median_short_term_weight.max(penalty_free_zone(hf)); } let long_term_median = median_long_term_weight.max(PENALTY_FREE_ZONE_5); let short_term_median = median_short_term_weight; - let effective_median = if hf.in_range(&HardFork::V10, &HardFork::V15) { + let effective_median = if hf >= &HardFork::V10 && hf < &HardFork::V15 { min( max(PENALTY_FREE_ZONE_5, short_term_median), 50 * long_term_median, @@ -270,14 +270,14 @@ pub fn calculate_block_long_term_weight( block_weight: usize, long_term_median: usize, ) -> usize { - if hf.in_range(&HardFork::V1, &HardFork::V10) { + if hf < &HardFork::V10 { return block_weight; } let long_term_median = max(penalty_free_zone(hf), long_term_median); let (short_term_constraint, adjusted_block_weight) = - if hf.in_range(&HardFork::V10, &HardFork::V15) { + if hf >= &HardFork::V10 && hf < &HardFork::V15 { let stc = long_term_median + long_term_median * 2 / 5; (stc, block_weight) } else { @@ -292,7 +292,7 @@ pub fn calculate_block_long_term_weight( async fn get_blocks_weight_in_range( range: Range, database: D, -) -> Result, ConsensusError> { +) -> Result, ExtendedConsensusError> { tracing::info!("getting block weights."); let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database @@ -312,7 +312,7 @@ async fn get_blocks_weight_in_range( async fn get_long_term_weight_in_range( range: Range, database: D, -) -> Result, ConsensusError> { +) -> Result, ExtendedConsensusError> { tracing::info!("getting block long term weights."); let DatabaseResponse::BlockExtendedHeaderInRange(ext_headers) = database diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 2e04ddcf..0bc7cc2b 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -4,10 +4,11 @@ use std::{ sync::Arc, }; +use monero_consensus::{transactions::OutputOnChain, ConsensusError, HardFork}; + //mod batch_verifier; pub mod block; pub mod context; -pub mod genesis; mod helper; #[cfg(feature = "binaries")] pub mod rpc; @@ -20,10 +21,20 @@ pub use block::{ }; pub use context::{ initialize_blockchain_context, BlockChainContext, BlockChainContextRequest, - BlockChainContextResponse, ContextConfig, HardFork, + BlockChainContextResponse, ContextConfig, }; pub use transactions::{VerifyTxRequest, VerifyTxResponse}; +#[derive(Debug, thiserror::Error)] +pub enum ExtendedConsensusError { + #[error("{0}")] + ConErr(#[from] monero_consensus::ConsensusError), + #[error("Database error: {0}")] + DBErr(#[from] tower::BoxError), + #[error("Needed transaction is not in pool")] + TxPErr(#[from] TxNotInPool), +} + // TODO: instead of (ab)using generic returns return the acc type pub async fn initialize_verifier( database: D, @@ -34,8 +45,8 @@ pub async fn initialize_verifier( impl tower::Service< VerifyBlockRequest, Response = VerifyBlockResponse, - Error = ConsensusError, - Future = impl Future> + Error = ExtendedConsensusError, + Future = impl Future> + Send + 'static, > + Clone @@ -44,8 +55,10 @@ pub async fn initialize_verifier( impl tower::Service< VerifyTxRequest, Response = VerifyTxResponse, - Error = ConsensusError, - Future = impl Future> + Send + 'static, + Error = ExtendedConsensusError, + Future = impl Future> + + Send + + 'static, > + Clone + Send + 'static, @@ -76,43 +89,6 @@ where Ok((block_svc, tx_svc)) } -// 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")] - TransactionOutputsOverflow, - #[error("Transaction has an invalid output: {0}")] - TransactionInvalidOutput(&'static str), - #[error("Transaction has an invalid version")] - TransactionVersionInvalid, - #[error("Transaction an invalid input: {0}")] - TransactionHasInvalidInput(&'static str), - #[error("Transaction has invalid ring: {0}")] - TransactionHasInvalidRing(&'static str), - #[error("Block has an invalid proof of work")] - BlockPOWInvalid, - #[error("Block has a timestamp outside of the valid range")] - BlockTimestampInvalid, - #[error("Block is too large")] - BlockIsTooLarge, - #[error("Invalid hard fork version: {0}")] - InvalidHardForkVersion(&'static str), - #[error("The block has a different previous hash than expected")] - BlockIsNotApartOfChain, - #[error("One or more transaction is not in the transaction pool")] - TxNotInPool(#[from] TxNotInPool), - #[error("Database error: {0}")] - Database(#[from] tower::BoxError), -} - pub trait Database: tower::Service { @@ -123,14 +99,6 @@ impl>), - NumberOutputsWithAmount(u64), + NumberOutputsWithAmount(Vec), CheckKIsNotSpent(HashSet<[u8; 32]>), @@ -173,7 +141,7 @@ pub enum DatabaseResponse { GeneratedCoins(u64), Outputs(HashMap>), - NumberOutputsWithAmount(usize), + NumberOutputsWithAmount(HashMap), /// returns true if key images are spent CheckKIsNotSpent(bool), diff --git a/consensus/src/rpc.rs b/consensus/src/rpc.rs index af9db585..e8f8bffa 100644 --- a/consensus/src/rpc.rs +++ b/consensus/src/rpc.rs @@ -149,7 +149,7 @@ where .boxed(), DatabaseRequest::NumberOutputsWithAmount(amt) => async move { Ok(DatabaseResponse::NumberOutputsWithAmount( - cache.read().await.numb_outs(amt), + cache.read().await.numb_outs(&amt), )) } .boxed(), diff --git a/consensus/src/rpc/cache.rs b/consensus/src/rpc/cache.rs index a8715167..bd162a3b 100644 --- a/consensus/src/rpc/cache.rs +++ b/consensus/src/rpc/cache.rs @@ -115,8 +115,11 @@ impl ScanningCache { self.numb_outs.values().sum() } - pub fn numb_outs(&self, amount: u64) -> usize { - *self.numb_outs.get(&amount).unwrap_or(&0) + pub fn numb_outs(&self, amounts: &[u64]) -> HashMap { + amounts + .iter() + .map(|amount| (*amount, *self.numb_outs.get(amount).unwrap_or(&0))) + .collect() } pub fn add_outs(&mut self, amount: u64, count: usize) { @@ -130,7 +133,7 @@ impl ScanningCache { impl Display for ScanningCache { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let rct_outs = self.numb_outs(0); + let rct_outs = *self.numb_outs(&[0]).get(&0).unwrap(); let total_outs = self.total_outs(); f.debug_struct("Cache") diff --git a/consensus/src/rpc/connection.rs b/consensus/src/rpc/connection.rs index 5f2eed98..d310def7 100644 --- a/consensus/src/rpc/connection.rs +++ b/consensus/src/rpc/connection.rs @@ -131,9 +131,9 @@ impl RpcConnection { }; Ok(ExtendedBlockHeader { - version: HardFork::from_version(&info.major_version) + version: HardFork::from_version(info.major_version) .expect("previously checked block has incorrect version"), - vote: HardFork::from_vote(&info.minor_version), + vote: HardFork::from_vote(info.minor_version), timestamp: info.timestamp, cumulative_difficulty: u128_from_low_high( info.cumulative_difficulty, @@ -167,9 +167,9 @@ impl RpcConnection { res.headers .into_iter() .map(|info| ExtendedBlockHeader { - version: HardFork::from_version(&info.major_version) + version: HardFork::from_version(info.major_version) .expect("previously checked block has incorrect version"), - vote: HardFork::from_vote(&info.minor_version), + vote: HardFork::from_vote(info.minor_version), timestamp: info.timestamp, cumulative_difficulty: u128_from_low_high( info.cumulative_difficulty, diff --git a/consensus/src/rpc/discover.rs b/consensus/src/rpc/discover.rs index a442fca0..173be85c 100644 --- a/consensus/src/rpc/discover.rs +++ b/consensus/src/rpc/discover.rs @@ -19,7 +19,7 @@ use super::{ async fn check_rpc(addr: String, cache: Arc>) -> Option { tracing::debug!("Sending request to node."); - let con = HttpRpc::new_custom_timeout(addr.clone(), Duration::from_secs(u64::MAX)) + let con = HttpRpc::with_custom_timeout(addr.clone(), Duration::from_secs(u64::MAX)) .await .ok()?; let (tx, rx) = mpsc::channel(0); diff --git a/consensus/src/transactions.rs b/consensus/src/transactions.rs index 73e12c54..917294d1 100644 --- a/consensus/src/transactions.rs +++ b/consensus/src/transactions.rs @@ -13,32 +13,21 @@ use rayon::prelude::*; use tower::{Service, ServiceExt}; use tracing::instrument; +use monero_consensus::{ + signatures::verify_contextual_signatures, + transactions::{ + check_all_time_locks, check_inputs, check_outputs, check_tx_version, TransactionError, + TxRingMembersInfo, + }, + ConsensusError, HardFork, TxVersion, +}; + use crate::{ - context::ReOrgToken, helper::rayon_spawn_async, ConsensusError, Database, DatabaseRequest, - DatabaseResponse, HardFork, + context::ReOrgToken, helper::rayon_spawn_async, Database, DatabaseRequest, DatabaseResponse, + ExtendedConsensusError, }; mod contextual_data; -mod inputs; -pub(crate) mod outputs; -mod sigs; -mod time_lock; - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum TxVersion { - RingSignatures, - RingCT, -} - -impl TxVersion { - pub fn from_raw(version: u64) -> Result { - match version { - 1 => Ok(TxVersion::RingSignatures), - 2 => Ok(TxVersion::RingCT), - _ => Err(ConsensusError::TransactionVersionInvalid), - } - } -} /// Data needed to verify a transaction. /// @@ -52,7 +41,7 @@ pub struct TransactionVerificationData { pub tx_hash: [u8; 32], /// We put this behind a mutex as the information is not constant and is based of past outputs idxs /// which could change on re-orgs. - rings_member_info: std::sync::Mutex>, + rings_member_info: std::sync::Mutex>, } impl TransactionVerificationData { @@ -63,7 +52,8 @@ impl TransactionVerificationData { tx_weight: tx.weight(), fee: tx.rct_signatures.base.fee, rings_member_info: std::sync::Mutex::new(None), - version: TxVersion::from_raw(tx.prefix.version)?, + version: TxVersion::from_raw(tx.prefix.version) + .ok_or(TransactionError::TransactionVersionInvalid)?, tx, }) } @@ -113,7 +103,7 @@ where D::Future: Send + 'static, { type Response = VerifyTxResponse; - type Error = ConsensusError; + type Error = ExtendedConsensusError; type Future = Pin> + Send + 'static>>; @@ -154,7 +144,7 @@ async fn batch_setup_transactions( txs: Vec, hf: HardFork, re_org_token: ReOrgToken, -) -> Result +) -> Result where D: Database + Clone + Sync + Send + 'static, { @@ -179,7 +169,7 @@ async fn verify_transactions_for_block( time_for_time_lock: u64, hf: HardFork, re_org_token: ReOrgToken, -) -> Result +) -> Result where D: Database + Clone + Sync + Send + 'static, { @@ -215,9 +205,7 @@ where }; if kis_spent { - return Err(ConsensusError::TransactionHasInvalidInput( - "One or more key image spent!", - )); + Err(ConsensusError::Transaction(TransactionError::KeyImageSpent))?; } Ok(VerifyTxResponse::Ok) @@ -243,21 +231,20 @@ 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.0.decoy_info, tx_version, &hf)?; - time_lock::check_all_time_locks( - &rings_member_info.time_locked_outs, + check_all_time_locks( + &rings_member_info.0.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)?; + let sum_outputs = check_outputs(&tx_verification_data.tx.prefix.outputs, &hf, tx_version)?; - let sum_inputs = inputs::check_inputs( - &tx_verification_data.tx.prefix.inputs, - rings_member_info, + let sum_inputs = check_inputs( + tx_verification_data.tx.prefix.inputs.as_slice(), + &rings_member_info.0, current_chain_height, &hf, tx_version, @@ -266,59 +253,14 @@ fn verify_transaction_for_block( if tx_version == &TxVersion::RingSignatures { if sum_outputs >= sum_inputs { - return Err(ConsensusError::TransactionOutputsTooMuch); + Err(TransactionError::OutputsTooHigh)?; } // 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)?; + verify_contextual_signatures(&tx_verification_data.tx, &rings_member_info.0.rings)?; Ok(()) } - -/// Checks the version is in the allowed range. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#version -fn check_tx_version( - decoy_info: &Option, - version: &TxVersion, - hf: &HardFork, -) -> Result<(), ConsensusError> { - if let Some(decoy_info) = decoy_info { - let max = max_tx_version(hf); - if version > &max { - return Err(ConsensusError::TransactionVersionInvalid); - } - - // TODO: Doc is wrong here - let min = min_tx_version(hf); - if version < &min && decoy_info.not_mixable != 0 { - return Err(ConsensusError::TransactionVersionInvalid); - } - } else { - // This will only happen for hard-fork 1 when only RingSignatures are allowed. - if version != &TxVersion::RingSignatures { - return Err(ConsensusError::TransactionVersionInvalid); - } - } - - Ok(()) -} - -fn max_tx_version(hf: &HardFork) -> TxVersion { - if hf <= &HardFork::V3 { - TxVersion::RingSignatures - } else { - TxVersion::RingCT - } -} - -fn min_tx_version(hf: &HardFork) -> TxVersion { - if hf >= &HardFork::V6 { - TxVersion::RingCT - } else { - TxVersion::RingSignatures - } -} diff --git a/consensus/src/transactions/contextual_data.rs b/consensus/src/transactions/contextual_data.rs index a55b6aef..05c534cc 100644 --- a/consensus/src/transactions/contextual_data.rs +++ b/consensus/src/transactions/contextual_data.rs @@ -11,30 +11,30 @@ //! Because this data is unique for *every* transaction and the context service is just for blockchain state data. //! -use std::ops::Deref; -use std::{ - cmp::{max, min}, - collections::{HashMap, HashSet}, - sync::Arc, -}; +use std::collections::HashSet; +use std::{collections::HashMap, ops::Deref, sync::Arc}; -use curve25519_dalek::EdwardsPoint; -use monero_serai::transaction::{Input, Timelock}; +use monero_serai::transaction::Input; use tower::ServiceExt; -use crate::{ - context::ReOrgToken, transactions::TransactionVerificationData, ConsensusError, Database, - DatabaseRequest, DatabaseResponse, HardFork, OutputOnChain, +use monero_consensus::{ + transactions::{ + get_ring_members_for_inputs, insert_ring_member_ids, DecoyInfo, TxRingMembersInfo, + }, + ConsensusError, HardFork, }; -use super::TxVersion; +use crate::{ + context::ReOrgToken, transactions::TransactionVerificationData, Database, DatabaseRequest, + DatabaseResponse, ExtendedConsensusError, +}; pub async fn batch_refresh_ring_member_info( txs_verification_data: &[Arc], hf: &HardFork, re_org_token: ReOrgToken, - database: D, -) -> Result<(), ConsensusError> { + mut database: D, +) -> Result<(), ExtendedConsensusError> { let (txs_needing_full_refresh, txs_needing_partial_refresh) = ring_member_info_needing_refresh(txs_verification_data, hf); @@ -48,10 +48,40 @@ pub async fn batch_refresh_ring_member_info amount.unwrap_or(0), + _ => 0, + }) + .collect::>() + }) + .collect::>(); + + let DatabaseResponse::NumberOutputsWithAmount(outputs_with_amount) = database + .ready() + .await? + .call(DatabaseRequest::NumberOutputsWithAmount( + unique_input_amounts.into_iter().collect(), + )) + .await? + else { + panic!("Database sent incorrect response!") + }; + for tx_v_data in txs_needing_partial_refresh { let decoy_info = if hf != &HardFork::V1 { // this data is only needed after hard-fork 1. - Some(DecoyInfo::new(&tx_v_data.tx.prefix.inputs, hf, database.clone()).await?) + Some( + DecoyInfo::new(&tx_v_data.tx.prefix.inputs, &outputs_with_amount, hf) + .map_err(ConsensusError::Transaction)?, + ) } else { None }; @@ -64,6 +94,7 @@ pub async fn batch_refresh_ring_member_info Result<(), ConsensusError> { +) -> Result<(), ExtendedConsensusError> { let mut output_ids = HashMap::new(); for tx_v_data in txs_verification_data.iter() { - insert_ring_member_ids(&tx_v_data.tx.prefix.inputs, &mut output_ids)?; + insert_ring_member_ids(&tx_v_data.tx.prefix.inputs, &mut output_ids) + .map_err(ConsensusError::Transaction)?; } let DatabaseResponse::Outputs(outputs) = database @@ -146,322 +179,38 @@ 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_default() - .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)] -#[non_exhaustive] -pub enum Rings { - /// Legacy, pre-ringCT, rings. - Legacy(Vec>), - // RingCT rings, (outkey, mask). - RingCT(Vec>), -} - -impl Rings { - /// Builds the rings for the transaction inputs, from the given outputs. - fn new(outputs: Vec>, tx_version: TxVersion) -> Rings { - match tx_version { - TxVersion::RingSignatures => Rings::Legacy( - outputs - .into_iter() - .map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect()) - .collect(), - ), - TxVersion::RingCT => Rings::RingCT( - outputs - .into_iter() - .map(|inp_outs| { - inp_outs - .into_iter() - .map(|out| [out.key, out.mask]) - .collect() - }) - .collect(), - ), - } - } -} - -/// 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, - /// A token used to check if a re org has happened since getting this data. - re_org_token: ReOrgToken, - /// The hard-fork this data was retrived for. - hf: HardFork, -} - -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, - tx_version: TxVersion, - hf: HardFork, - re_org_token: ReOrgToken, - ) -> 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, tx_version), - re_org_token, - decoy_info, - hf, - } - } -} - -/// 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. This data can vary by block so -/// this data needs to be retrieved after every change in the blockchain. -/// -/// This data *does not* need to be refreshed if one of these are true: -/// - The input amounts are *ALL* 0 -/// - The top block hash is the same as when this data was retrieved. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html -#[derive(Debug)] -pub struct DecoyInfo { - /// The number of inputs that have enough outputs on the chain to mix with. - pub mixable: usize, - /// The number of inputs that don't have enough outputs on the chain to mix with. - pub not_mixable: usize, - /// The minimum amount of decoys used in the transaction. - pub min_decoys: usize, - /// The maximum amount of decoys used in the transaction. - pub max_decoys: usize, -} - -impl DecoyInfo { - /// Creates a new [`DecoyInfo`] struct relating to the passed in inputs. - /// - /// Do not rely on this function to do consensus checks! - /// - pub async fn new( - inputs: &[Input], - hf: &HardFork, - mut database: D, - ) -> Result { - let mut min_decoys = usize::MAX; - let mut max_decoys = usize::MIN; - let mut mixable = 0; - let mut not_mixable = 0; - - let minimum_decoys = minimum_decoys(hf); - - for inp in inputs { - match inp { - Input::ToKey { - amount, - key_offsets, - .. - } => { - if let Some(amt) = *amount { - let DatabaseResponse::NumberOutputsWithAmount(numb_of_outs) = database - .ready() - .await? - .call(DatabaseRequest::NumberOutputsWithAmount(amt)) - .await? - else { - panic!("Database sent incorrect response!"); - }; - - // https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs - if numb_of_outs <= minimum_decoys && amt != 0 { - not_mixable += 1; - } else { - mixable += 1; - } - } else { - // ringCT amounts are always mixable. - mixable += 1; - } - - let numb_decoys = key_offsets - .len() - .checked_sub(1) - .ok_or(ConsensusError::TransactionHasInvalidRing("ring is empty"))?; - // https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-and-maximum-decoys-used - min_decoys = min(min_decoys, numb_decoys); - max_decoys = max(max_decoys, numb_decoys); - } - _ => { - return Err(ConsensusError::TransactionHasInvalidInput( - "input not ToKey", - )) - } - } - } - - Ok(DecoyInfo { - mixable, - not_mixable, - min_decoys, - max_decoys, - }) - } -} - -/// Returns the minimum amount of decoys for a hard-fork. -/// **There are exceptions to this always being the minimum decoys** -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-amount-of-decoys -pub(crate) fn minimum_decoys(hf: &HardFork) -> usize { - use HardFork as HF; - match hf { - HF::V1 => panic!("hard-fork 1 does not use these rules!"), - HF::V2 | HF::V3 | HF::V4 | HF::V5 => 2, - HF::V6 => 4, - HF::V7 => 6, - HF::V8 | HF::V9 | HF::V10 | HF::V11 | HF::V12 | HF::V13 | HF::V14 => 10, - HF::V15 | HF::V16 => 15, - } -} diff --git a/consensus/src/transactions/inputs.rs b/consensus/src/transactions/inputs.rs deleted file mode 100644 index 253eec17..00000000 --- a/consensus/src/transactions/inputs.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! # 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 crate::{ - transactions::{ - contextual_data::{minimum_decoys, DecoyInfo, TxRingMembersInfo}, - TxVersion, - }, - ConsensusError, HardFork, -}; - -/// Checks the decoys are allowed. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#minimum-decoys -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#equal-number-of-decoys -fn check_decoy_info(decoy_info: &DecoyInfo, hf: &HardFork) -> Result<(), ConsensusError> { - if hf == &HardFork::V15 { - // Hard-fork 15 allows both v14 and v16 rules - return check_decoy_info(decoy_info, &HardFork::V14) - .or_else(|_| check_decoy_info(decoy_info, &HardFork::V16)); - } - - let current_minimum_decoys = minimum_decoys(hf); - - if decoy_info.min_decoys < current_minimum_decoys { - if decoy_info.not_mixable == 0 { - return Err(ConsensusError::TransactionHasInvalidRing( - "input does not have enough decoys", - )); - } - if decoy_info.mixable > 1 { - return Err(ConsensusError::TransactionHasInvalidInput( - "more than one mixable input with unmixable inputs", - )); - } - } - - if hf >= &HardFork::V8 && decoy_info.min_decoys != current_minimum_decoys { - return Err(ConsensusError::TransactionHasInvalidRing( - "one ring does not have the minimum number of decoys", - )); - } - - if hf >= &HardFork::V12 && decoy_info.min_decoys != decoy_info.max_decoys { - return Err(ConsensusError::TransactionHasInvalidRing( - "rings do not have the same number of members", - )); - } - - Ok(()) -} - -/// Checks the inputs key images for torsion and for duplicates in the transaction. -/// -/// The `spent_kis` parameter is not meant to be a complete list of key images, just a list of related transactions -/// key images, for example transactions in a block. The chain will be checked for duplicates later. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#unique-key-image -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#torsion-free-key-image -pub(crate) fn check_key_images( - input: &Input, - spent_kis: &mut HashSet<[u8; 32]>, -) -> 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", - )); - } - if !spent_kis.insert(key_image.compress().to_bytes()) { - return Err(ConsensusError::TransactionHasInvalidInput( - "key image already spent", - )); - } - } - _ => { - return Err(ConsensusError::TransactionHasInvalidInput( - "Input not ToKey", - )) - } - } - - Ok(()) -} - -/// Checks that the input is of type [`Input::ToKey`] aka txin_to_key. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#input-type -fn check_input_type(input: &Input) -> Result<(), ConsensusError> { - match input { - Input::ToKey { .. } => Ok(()), - _ => Err(ConsensusError::TransactionHasInvalidInput( - "Input not ToKey", - )), - } -} - -/// Checks that the input has decoys. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys -fn check_input_has_decoys(input: &Input) -> Result<(), ConsensusError> { - match input { - Input::ToKey { key_offsets, .. } => { - if key_offsets.is_empty() { - Err(ConsensusError::TransactionHasInvalidRing("No ring members")) - } else { - Ok(()) - } - } - _ => panic!("Input not ToKey"), - } -} - -/// 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 { - Input::ToKey { key_offsets, .. } => key_offsets.iter().skip(1).try_for_each(|offset| { - if *offset == 0 { - Err(ConsensusError::TransactionHasInvalidRing( - "duplicate ring member", - )) - } else { - Ok(()) - } - }), - _ => panic!("Only ToKey is allowed this should have already been checked!"), - } - } else { - Ok(()) - } -} - -/// 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(), - _ => panic!("Only ToKey is allowed this should have already been checked!"), - }; - - if hf >= &HardFork::V7 { - for inps in inputs.windows(2) { - match get_ki(&inps[0]).cmp(&get_ki(&inps[1])) { - Ordering::Less => (), - _ => { - return Err(ConsensusError::TransactionHasInvalidInput( - "Inputs not ordered by key image!", - )) - } - } - } - Ok(()) - } else { - Ok(()) - } -} - -/// 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, - hf: &HardFork, -) -> Result<(), ConsensusError> { - 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 young", - )) - } else { - Ok(()) - } - } else { - Ok(()) - } -} - -/// Sums the inputs checking for overflow. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#inputs-and-outputs-must-not-overflow -fn sum_inputs_v1(inputs: &[Input]) -> Result { - let mut sum: u64 = 0; - for inp in inputs { - match inp { - Input::ToKey { amount, .. } => { - sum = sum - .checked_add(amount.unwrap_or(0)) - .ok_or(ConsensusError::TransactionInputsOverflow)?; - } - _ => { - return Err(ConsensusError::TransactionHasInvalidInput( - "input not ToKey", - )) - } - } - } - - Ok(sum) -} - -/// Checks all input consensus rules. -/// -/// TODO: list rules. -/// -pub fn check_inputs( - inputs: &[Input], - ring_member_info: &TxRingMembersInfo, - current_chain_height: u64, - hf: &HardFork, - tx_version: &TxVersion, - spent_kis: Arc>>, -) -> Result { - if inputs.is_empty() { - return Err(ConsensusError::TransactionHasInvalidInput("no inputs")); - } - - check_10_block_lock(ring_member_info, current_chain_height, hf)?; - - 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 { - check_input_type(input)?; - check_input_has_decoys(input)?; - - check_ring_members_unique(input, hf)?; - - let mut spent_kis_lock = spent_kis.lock().unwrap(); - check_key_images(input, &mut spent_kis_lock)?; - } - - check_inputs_sorted(inputs, hf)?; - - match tx_version { - TxVersion::RingSignatures => sum_inputs_v1(inputs), - _ => panic!("TODO: RCT"), - } -} diff --git a/consensus/src/transactions/outputs.rs b/consensus/src/transactions/outputs.rs deleted file mode 100644 index 67226da9..00000000 --- a/consensus/src/transactions/outputs.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! # Outputs -//! -//! Consensus rules relating to non-miner transaction outputs - -use std::sync::OnceLock; - -use monero_serai::transaction::Output; - -use crate::{helper::check_point, transactions::TxVersion, ConsensusError, HardFork}; - -/// Decomposed amount table. -/// -/// TODO: manually list each amount -static DECOMPOSED_AMOUNTS: OnceLock<[u64; 172]> = OnceLock::new(); - -pub(crate) fn decomposed_amounts() -> &'static [u64; 172] { - DECOMPOSED_AMOUNTS.get_or_init(|| { - let mut amounts = [1; 172]; - for zeros in 0..19 { - for digit in 1..10 { - amounts[zeros * 9 + digit - 1] = - (digit * 10_usize.pow(zeros as u32)).try_into().unwrap(); - } - } - amounts[171] = 10000000000000000000; - amounts - }) -} - -/// Checks the output keys are canonical points. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#output-keys-canonical -fn check_output_keys(outputs: &[Output]) -> Result<(), ConsensusError> { - for out in outputs { - if !check_point(&out.key) { - return Err(ConsensusError::TransactionInvalidOutput( - "outputs one time key is not a valid point", - )); - } - } - - Ok(()) -} - -/// Checks the output types are allowed. -/// -/// This is also used during miner-tx verification. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#output-type -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-type -pub(crate) fn check_output_types(outputs: &[Output], hf: &HardFork) -> Result<(), ConsensusError> { - if hf == &HardFork::V15 { - for outs in outputs.windows(2) { - if outs[0].view_tag.is_some() != outs[0].view_tag.is_some() { - return Err(ConsensusError::TransactionInvalidOutput( - "v15 output must have one output type", - )); - } - } - return Ok(()); - } - - for out in outputs { - if hf <= &HardFork::V14 && out.view_tag.is_some() { - return Err(ConsensusError::TransactionInvalidOutput( - "tagged output used before allowed", - )); - } else if hf >= &HardFork::V16 && out.view_tag.is_none() { - return Err(ConsensusError::TransactionInvalidOutput( - "output does not have a view tag", - )); - } - } - Ok(()) -} - -/// Checks that an output amount is decomposed. -/// -/// This is also used during miner tx verification. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts -pub(crate) fn is_decomposed_amount(amount: u64) -> bool { - decomposed_amounts().binary_search(&amount).is_ok() -} - -/// Checks the outputs amount for version 1 txs. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount -fn check_output_amount_v1(amount: u64, hf: &HardFork) -> Result<(), ConsensusError> { - if amount == 0 { - return Err(ConsensusError::TransactionInvalidOutput( - "zero amount output for v1 tx", - )); - } - - if hf >= &HardFork::V2 && !is_decomposed_amount(amount) { - return Err(ConsensusError::TransactionInvalidOutput( - "v1 tx does not have decomposed amount", - )); - } - - Ok(()) -} - -/// Sums the outputs, checking for overflow and other consensus rules. -/// -/// Should only be used on v1 transactions. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#inputs-and-outputs-must-not-overflow -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount -fn sum_outputs_v1(outputs: &[Output], hf: &HardFork) -> Result { - let mut sum: u64 = 0; - - for out in outputs { - let raw_amount = out.amount.unwrap_or(0); - - check_output_amount_v1(raw_amount, hf)?; - - sum = sum - .checked_add(raw_amount) - .ok_or(ConsensusError::TransactionOutputsOverflow)?; - } - - Ok(sum) -} - -/// Checks the outputs against all output consensus rules, returning the sum of the output amounts. -pub fn check_outputs( - outputs: &[Output], - hf: &HardFork, - tx_version: &TxVersion, -) -> Result { - check_output_types(outputs, hf)?; - check_output_keys(outputs)?; - - match tx_version { - TxVersion::RingSignatures => sum_outputs_v1(outputs, hf), - _ => todo!("RingCT"), - } -} diff --git a/consensus/src/transactions/sigs.rs b/consensus/src/transactions/sigs.rs deleted file mode 100644 index 78fad295..00000000 --- a/consensus/src/transactions/sigs.rs +++ /dev/null @@ -1,17 +0,0 @@ -use monero_serai::transaction::Transaction; - -use crate::{transactions::contextual_data::Rings, ConsensusError}; - -mod ring_sigs; - -pub fn verify_signatures(tx: &Transaction, rings: &Rings) -> Result<(), ConsensusError> { - match rings { - Rings::Legacy(_) => ring_sigs::verify_inputs_signatures( - &tx.prefix.inputs, - &tx.signatures, - rings, - &tx.signature_hash(), - ), - _ => panic!("TODO: RCT"), - } -} diff --git a/consensus/src/transactions/sigs/ring_sigs.rs b/consensus/src/transactions/sigs/ring_sigs.rs deleted file mode 100644 index 41958971..00000000 --- a/consensus/src/transactions/sigs/ring_sigs.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Version 1 ring signature verification. -//! -//! Some checks have to be done at deserialization or with data we don't have so we can't do them here, those checks are: -//! https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#signatures-must-be-canonical -//! this happens at deserialization in monero-serai. -//! https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#amount-of-signatures-in-a-ring -//! and this happens during ring signature verification in monero-serai. -//! -use monero_serai::{ring_signatures::RingSignature, transaction::Input}; -use rayon::prelude::*; - -use super::Rings; -use crate::ConsensusError; - -/// Verifies the ring signature. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#the-ring-signature-must-be-valid -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#amount-of-ring-signatures -pub fn verify_inputs_signatures( - inputs: &[Input], - signatures: &[RingSignature], - rings: &Rings, - tx_sig_hash: &[u8; 32], -) -> Result<(), ConsensusError> { - match rings { - Rings::Legacy(rings) => { - // rings.len() != inputs.len() can't happen but check any way. - if signatures.len() != inputs.len() || rings.len() != inputs.len() { - return Err(ConsensusError::TransactionSignatureInvalid( - "number of ring sigs != inputs", - )); - } - - inputs - .par_iter() - .zip(rings) - .zip(signatures) - .try_for_each(|((input, ring), sig)| { - let Input::ToKey { key_image, .. } = input else { - panic!("How did we build a ring with no decoys?"); - }; - - if !sig.verify(tx_sig_hash, ring, key_image) { - return Err(ConsensusError::TransactionSignatureInvalid( - "Invalid ring signature", - )); - } - Ok(()) - })?; - } - _ => panic!("tried to verify v1 tx with a non v1 ring"), - } - Ok(()) -} diff --git a/consensus/src/transactions/time_lock.rs b/consensus/src/transactions/time_lock.rs deleted file mode 100644 index 48d1177d..00000000 --- a/consensus/src/transactions/time_lock.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! # Time Locks -//! -//! This module contains the checks for time locks, using the `check_all_time_locks` function. -//! -use monero_serai::transaction::Timelock; - -use crate::{ConsensusError, HardFork}; - -/// 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 -fn output_unlocked( - time_lock: &Timelock, - current_chain_height: u64, - current_time_lock_timestamp: u64, - hf: &HardFork, -) -> bool { - match *time_lock { - Timelock::None => true, - Timelock::Block(unlock_height) => { - check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height) - } - Timelock::Time(unlock_time) => { - check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf) - } - } -} - -/// 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#block-height -fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool { - // current_chain_height = 1 + top height - unlock_height <= current_chain_height -} - -/// /// -/// 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, - current_time_lock_timestamp: u64, - hf: &HardFork, -) -> bool { - current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp -}