diff --git a/Cargo.lock b/Cargo.lock index c35deecb..39458962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,6 +573,7 @@ dependencies = [ "crypto-bigint", "cuprate-cryptonight", "cuprate-helper", + "cuprate-types", "curve25519-dalek", "hex", "hex-literal", @@ -860,7 +861,10 @@ dependencies = [ "cuprate-fixed-bytes", "curve25519-dalek", "monero-serai", + "proptest", + "proptest-derive", "serde", + "thiserror", ] [[package]] diff --git a/consensus/fast-sync/src/fast_sync.rs b/consensus/fast-sync/src/fast_sync.rs index b42ae642..35fa6742 100644 --- a/consensus/fast-sync/src/fast_sync.rs +++ b/consensus/fast-sync/src/fast_sync.rs @@ -16,7 +16,7 @@ use tower::{Service, ServiceExt}; use cuprate_consensus::{ context::{BlockChainContextRequest, BlockChainContextResponse}, - transactions::TransactionVerificationData, + transactions::new_tx_verification_data, }; use cuprate_consensus_rules::{miner_tx::MinerTxError, ConsensusError}; use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; @@ -257,7 +257,7 @@ where .remove(tx) .ok_or(FastSyncError::TxsIncludedWithBlockIncorrect)?; - let data = TransactionVerificationData::new(tx)?; + let data = new_tx_verification_data(tx)?; verified_txs.push(VerifiedTransactionInformation { tx_blob: data.tx_blob, tx_weight: data.tx_weight, diff --git a/consensus/rules/Cargo.toml b/consensus/rules/Cargo.toml index 311bcc95..2cf03e39 100644 --- a/consensus/rules/Cargo.toml +++ b/consensus/rules/Cargo.toml @@ -7,11 +7,12 @@ authors = ["Boog900"] [features] default = [] -proptest = ["dep:proptest", "dep:proptest-derive"] +proptest = ["dep:proptest", "dep:proptest-derive", "cuprate-types/proptest"] rayon = ["dep:rayon"] [dependencies] cuprate-helper = { path = "../../helper", default-features = false, features = ["std"] } +cuprate-types = { path = "../../types", default-features = false } cuprate-cryptonight = {path = "../../cryptonight"} monero-serai = { workspace = true, features = ["std"] } diff --git a/consensus/rules/src/blocks.rs b/consensus/rules/src/blocks.rs index c36f68b8..e118e9a3 100644 --- a/consensus/rules/src/blocks.rs +++ b/consensus/rules/src/blocks.rs @@ -6,7 +6,7 @@ use monero_serai::block::Block; use cuprate_cryptonight::*; use crate::{ - current_unix_timestamp, + check_block_version_vote, current_unix_timestamp, hard_forks::HardForkError, miner_tx::{check_miner_tx, MinerTxError}, HardFork, @@ -249,11 +249,10 @@ pub fn check_block( block_blob_len: usize, block_chain_ctx: &ContextToVerifyBlock, ) -> Result<(HardFork, u64), BlockError> { - let (version, vote) = HardFork::from_block_header(&block.header)?; + let (version, vote) = + HardFork::from_block_header(&block.header).map_err(|_| HardForkError::HardForkUnknown)?; - block_chain_ctx - .current_hf - .check_block_version_vote(&version, &vote)?; + check_block_version_vote(&block_chain_ctx.current_hf, &version, &vote)?; if let Some(median_timestamp) = block_chain_ctx.median_block_timestamp { check_timestamp(block, median_timestamp)?; diff --git a/consensus/rules/src/hard_forks.rs b/consensus/rules/src/hard_forks.rs index 6b983149..4f786e49 100644 --- a/consensus/rules/src/hard_forks.rs +++ b/consensus/rules/src/hard_forks.rs @@ -1,40 +1,37 @@ //! # Hard-Forks //! -//! Monero use hard-forks to update it's protocol, this module contains a [`HardFork`] enum which is -//! an identifier for every current hard-fork. -//! -//! This module also contains a [`HFVotes`] struct which keeps track of current blockchain voting, and -//! has a method [`HFVotes::current_fork`] to check if the next hard-fork should be activated. -//! -use monero_serai::block::BlockHeader; +//! Monero use hard-forks to update it's protocol, this module contains a [`HFVotes`] struct which +//! keeps track of current blockchain voting, and has a method [`HFVotes::current_fork`] to check +//! if the next hard-fork should be activated. use std::{ collections::VecDeque, fmt::{Display, Formatter}, - time::Duration, }; +pub use cuprate_types::{HardFork, HardForkError}; + #[cfg(test)] mod tests; -/// Target block time for hf 1. -/// -/// ref: -const BLOCK_TIME_V1: Duration = Duration::from_secs(60); -/// Target block time from v2. -/// -/// ref: -const BLOCK_TIME_V2: Duration = Duration::from_secs(120); - pub const NUMB_OF_HARD_FORKS: usize = 16; -#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] -pub enum HardForkError { - #[error("The hard-fork is unknown")] - HardForkUnknown, - #[error("The block is on an incorrect hard-fork")] - VersionIncorrect, - #[error("The block's vote is for a previous hard-fork")] - VoteTooLow, +/// Checks a blocks version and vote, assuming that `hf` is the current hard-fork. +/// +/// ref: +pub fn check_block_version_vote( + hf: &HardFork, + version: &HardFork, + vote: &HardFork, +) -> Result<(), HardForkError> { + // self = current hf + if hf != version { + Err(HardForkError::VersionIncorrect)?; + } + if hf > vote { + Err(HardForkError::VoteTooLow)?; + } + + Ok(()) } /// Information about a given hard-fork. @@ -135,113 +132,6 @@ impl HFsInfo { } } -/// An identifier for every hard-fork Monero has had. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] -#[cfg_attr(any(feature = "proptest", 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. - /// - /// - #[inline] - 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(HardForkError::HardForkUnknown), - }) - } - - /// Returns the hard-fork for a blocks `minor_version` (vote) field. - /// - /// - #[inline] - 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) - } - - #[inline] - pub fn from_block_header(header: &BlockHeader) -> Result<(HardFork, HardFork), HardForkError> { - Ok(( - HardFork::from_version(header.hardfork_version)?, - HardFork::from_vote(header.hardfork_signal), - )) - } - - /// Returns the next hard-fork. - pub fn next_fork(&self) -> Option { - HardFork::from_version(*self as u8 + 1).ok() - } - - /// Returns the target block time for this hardfork. - /// - /// ref: - 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. - /// - /// ref: - pub fn check_block_version_vote( - &self, - version: &HardFork, - vote: &HardFork, - ) -> Result<(), HardForkError> { - // self = current hf - if self != version { - Err(HardForkError::VersionIncorrect)?; - } - if self > vote { - Err(HardForkError::VoteTooLow)?; - } - - Ok(()) - } -} - /// A struct holding the current voting state of the blockchain. #[derive(Debug, Clone, Eq, PartialEq)] pub struct HFVotes { diff --git a/consensus/rules/src/lib.rs b/consensus/rules/src/lib.rs index 3106cbbc..a5f8800a 100644 --- a/consensus/rules/src/lib.rs +++ b/consensus/rules/src/lib.rs @@ -9,7 +9,7 @@ pub mod miner_tx; pub mod transactions; pub use decomposed_amount::is_decomposed_amount; -pub use hard_forks::{HFVotes, HFsInfo, HardFork}; +pub use hard_forks::{check_block_version_vote, HFVotes, HFsInfo, HardFork}; pub use transactions::TxVersion; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] diff --git a/consensus/rules/src/miner_tx.rs b/consensus/rules/src/miner_tx.rs index e4927e39..663c95ef 100644 --- a/consensus/rules/src/miner_tx.rs +++ b/consensus/rules/src/miner_tx.rs @@ -1,6 +1,8 @@ use monero_serai::transaction::{Input, Output, Timelock, Transaction}; -use crate::{is_decomposed_amount, transactions::check_output_types, HardFork, TxVersion}; +use cuprate_types::TxVersion; + +use crate::{is_decomposed_amount, transactions::check_output_types, HardFork}; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum MinerTxError { diff --git a/consensus/rules/src/transactions.rs b/consensus/rules/src/transactions.rs index 5a0676b0..9c6ad51a 100644 --- a/consensus/rules/src/transactions.rs +++ b/consensus/rules/src/transactions.rs @@ -1,8 +1,11 @@ use std::cmp::Ordering; -use monero_serai::ringct::RctType; +use monero_serai::{ + ringct::RctType, + transaction::{Input, Output, Timelock, Transaction}, +}; -use monero_serai::transaction::{Input, Output, Timelock, Transaction}; +pub use cuprate_types::TxVersion; use crate::{ batch_verifier::BatchVerifier, blocks::penalty_free_zone, check_point_canonically_encoded, @@ -75,31 +78,6 @@ pub enum TransactionError { RingCTError(#[from] RingCTError), } -/// An enum representing all valid Monero transaction versions. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum TxVersion { - /// Legacy ring signatures. - RingSignatures, - /// RingCT - RingCT, -} - -impl TxVersion { - /// Converts a `raw` version value to a [`TxVersion`]. - /// - /// This will return `None` on invalid values. - /// - /// ref: - /// && - pub fn from_raw(version: u8) -> Option { - Some(match version { - 1 => TxVersion::RingSignatures, - 2 => TxVersion::RingCT, - _ => return None, - }) - } -} - //----------------------------------------------------------------------------------------------------------- OUTPUTS /// Checks the output keys are canonically encoded points. diff --git a/consensus/src/block.rs b/consensus/src/block.rs index f5aac5ed..e785a6b7 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -16,20 +16,22 @@ use tower::{Service, ServiceExt}; use cuprate_helper::asynch::rayon_spawn_async; use cuprate_types::{ - AltBlockInformation, VerifiedBlockInformation, VerifiedTransactionInformation, + AltBlockInformation, TransactionVerificationData, VerifiedBlockInformation, + VerifiedTransactionInformation, }; use cuprate_consensus_rules::{ blocks::{ calculate_pow_hash, check_block, check_block_pow, randomx_seed_height, BlockError, RandomX, }, + hard_forks::HardForkError, miner_tx::MinerTxError, ConsensusError, HardFork, }; use crate::{ context::{BlockChainContextRequest, BlockChainContextResponse, RawBlockChainContext}, - transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, + transactions::{VerifyTxRequest, VerifyTxResponse}, Database, ExtendedConsensusError, }; @@ -71,8 +73,8 @@ impl PreparedBlockExPow { /// - Hard-fork values are invalid /// - Miner transaction is missing a miner input pub fn new(block: Block) -> Result { - let (hf_version, hf_vote) = - HardFork::from_block_header(&block.header).map_err(BlockError::HardForkError)?; + let (hf_version, hf_vote) = HardFork::from_block_header(&block.header) + .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?; let Some(Input::Gen(height)) = block.miner_transaction.prefix().inputs.first() else { Err(ConsensusError::Block(BlockError::MinerTxError( @@ -125,8 +127,8 @@ impl PreparedBlock { block: Block, randomx_vm: Option<&R>, ) -> Result { - let (hf_version, hf_vote) = - HardFork::from_block_header(&block.header).map_err(BlockError::HardForkError)?; + let (hf_version, hf_vote) = HardFork::from_block_header(&block.header) + .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?; let [Input::Gen(height)] = &block.miner_transaction.prefix().inputs[..] else { Err(ConsensusError::Block(BlockError::MinerTxError( diff --git a/consensus/src/block/alt_block.rs b/consensus/src/block/alt_block.rs index 89440834..513697e9 100644 --- a/consensus/src/block/alt_block.rs +++ b/consensus/src/block/alt_block.rs @@ -15,7 +15,10 @@ use cuprate_consensus_rules::{ ConsensusError, }; use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::{AltBlockInformation, Chain, ChainId, VerifiedTransactionInformation}; +use cuprate_types::{ + AltBlockInformation, Chain, ChainId, TransactionVerificationData, + VerifiedTransactionInformation, +}; use crate::{ block::{free::pull_ordered_transactions, PreparedBlock}, @@ -25,7 +28,6 @@ use crate::{ weight::{self, BlockWeightsCache}, AltChainContextCache, AltChainRequestToken, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW, }, - transactions::TransactionVerificationData, BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, VerifyBlockResponse, }; diff --git a/consensus/src/block/batch_prepare.rs b/consensus/src/block/batch_prepare.rs index 64d1ccb5..9974d6d1 100644 --- a/consensus/src/block/batch_prepare.rs +++ b/consensus/src/block/batch_prepare.rs @@ -16,7 +16,7 @@ use cuprate_helper::asynch::rayon_spawn_async; use crate::{ block::{free::pull_ordered_transactions, PreparedBlock, PreparedBlockExPow}, context::rx_vms::RandomXVM, - transactions::TransactionVerificationData, + transactions::new_tx_verification_data, BlockChainContextRequest, BlockChainContextResponse, ExtendedConsensusError, VerifyBlockResponse, }; @@ -185,7 +185,7 @@ where let txs = txs .into_par_iter() .map(|tx| { - let tx = TransactionVerificationData::new(tx)?; + let tx = new_tx_verification_data(tx)?; Ok::<_, ConsensusError>((tx.tx_hash, tx)) }) .collect::, _>>()?; diff --git a/consensus/src/block/free.rs b/consensus/src/block/free.rs index 46698e51..e122374d 100644 --- a/consensus/src/block/free.rs +++ b/consensus/src/block/free.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use monero_serai::block::Block; -use crate::{transactions::TransactionVerificationData, ExtendedConsensusError}; +use cuprate_types::TransactionVerificationData; + +use crate::ExtendedConsensusError; /// Returns a list of transactions, pulled from `txs` in the order they are in the [`Block`]. /// diff --git a/consensus/src/context/hardforks.rs b/consensus/src/context/hardforks.rs index 057e1c34..682933d1 100644 --- a/consensus/src/context/hardforks.rs +++ b/consensus/src/context/hardforks.rs @@ -95,8 +95,7 @@ impl HardForkState { panic!("Database sent incorrect response!"); }; - let current_hardfork = - HardFork::from_version(ext_header.version).expect("Stored block has invalid hardfork"); + let current_hardfork = ext_header.version; let mut hfs = HardForkState { config, diff --git a/consensus/src/tests/mock_db.rs b/consensus/src/tests/mock_db.rs index b1383781..a260cf03 100644 --- a/consensus/src/tests/mock_db.rs +++ b/consensus/src/tests/mock_db.rs @@ -61,8 +61,8 @@ pub struct DummyBlockExtendedHeader { impl From for ExtendedBlockHeader { fn from(value: DummyBlockExtendedHeader) -> Self { ExtendedBlockHeader { - version: value.version.unwrap_or(HardFork::V1) as u8, - vote: value.vote.unwrap_or(HardFork::V1) as u8, + version: value.version.unwrap_or(HardFork::V1), + vote: value.vote.unwrap_or(HardFork::V1).as_u8(), timestamp: value.timestamp.unwrap_or_default(), cumulative_difficulty: value.cumulative_difficulty.unwrap_or_default(), block_weight: value.block_weight.unwrap_or_default(), diff --git a/consensus/src/transactions.rs b/consensus/src/transactions.rs index 978407ec..91de67cd 100644 --- a/consensus/src/transactions.rs +++ b/consensus/src/transactions.rs @@ -7,7 +7,7 @@ use std::{ future::Future, ops::Deref, pin::Pin, - sync::{Arc, Mutex as StdMutex}, + sync::Arc, task::{Context, Poll}, }; @@ -22,10 +22,13 @@ use cuprate_consensus_rules::{ check_decoy_info, check_transaction_contextual, check_transaction_semantic, output_unlocked, TransactionError, }, - ConsensusError, HardFork, TxVersion, + ConsensusError, HardFork, }; use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse}, + CachedVerificationState, TransactionVerificationData, TxVersion, +}; use crate::{ batch_verifier::MultiThreadedBatchVerifier, @@ -36,6 +39,8 @@ use crate::{ pub mod contextual_data; mod free; +pub use free::new_tx_verification_data; + /// A struct representing the type of validation that needs to be completed for this transaction. #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum VerificationNeeded { @@ -45,79 +50,6 @@ enum VerificationNeeded { Contextual, } -/// Represents if a transaction has been fully validated and under what conditions -/// the transaction is valid in the future. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum CachedVerificationState { - /// The transaction has not been validated. - NotVerified, - /// The transaction is valid* if the block represented by this hash is in the blockchain and the [`HardFork`] - /// is the same. - /// - /// *V1 transactions require checks on their ring-length even if this hash is in the blockchain. - ValidAtHashAndHF([u8; 32], HardFork), - /// The transaction is valid* if the block represented by this hash is in the blockchain _and_ this - /// given time lock is unlocked. The time lock here will represent the youngest used time based lock - /// (If the transaction uses any time based time locks). This is because time locks are not monotonic - /// so unlocked outputs could become re-locked. - /// - /// *V1 transactions require checks on their ring-length even if this hash is in the blockchain. - ValidAtHashAndHFWithTimeBasedLock([u8; 32], HardFork, Timelock), -} - -impl CachedVerificationState { - /// Returns the block hash this is valid for if in state [`CachedVerificationState::ValidAtHashAndHF`] or [`CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock`]. - fn verified_at_block_hash(&self) -> Option<[u8; 32]> { - match self { - CachedVerificationState::NotVerified => None, - CachedVerificationState::ValidAtHashAndHF(hash, _) - | CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock(hash, _, _) => Some(*hash), - } - } -} - -/// Data needed to verify a transaction. -#[derive(Debug)] -pub struct TransactionVerificationData { - /// The transaction we are verifying - pub tx: Transaction, - /// The [`TxVersion`] of this tx. - pub version: TxVersion, - /// The serialised transaction. - pub tx_blob: Vec, - /// The weight of the transaction. - pub tx_weight: usize, - /// The fee this transaction has paid. - pub fee: u64, - /// The hash of this transaction. - pub tx_hash: [u8; 32], - /// The verification state of this transaction. - pub cached_verification_state: StdMutex, -} - -impl TransactionVerificationData { - /// Creates a new [`TransactionVerificationData`] from the given [`Transaction`]. - pub fn new(tx: Transaction) -> Result { - let tx_hash = tx.hash(); - let tx_blob = tx.serialize(); - - let tx_weight = free::tx_weight(&tx, &tx_blob); - - let fee = free::tx_fee(&tx)?; - - Ok(TransactionVerificationData { - tx_hash, - tx_blob, - tx_weight, - fee, - cached_verification_state: StdMutex::new(CachedVerificationState::NotVerified), - version: TxVersion::from_raw(tx.version()) - .ok_or(TransactionError::TransactionVersionInvalid)?, - tx, - }) - } -} - /// A request to verify a transaction. pub enum VerifyTxRequest { /// Verifies a batch of prepared txs. @@ -252,7 +184,7 @@ where tracing::debug!(parent: &span, "prepping transactions for verification."); let txs = rayon_spawn_async(|| { txs.into_par_iter() - .map(|tx| TransactionVerificationData::new(tx).map(Arc::new)) + .map(|tx| new_tx_verification_data(tx).map(Arc::new)) .collect::, _>>() }) .await?; @@ -399,7 +331,7 @@ fn transactions_needing_verification( .push((tx.clone(), VerificationNeeded::SemanticAndContextual)); continue; } - CachedVerificationState::ValidAtHashAndHF(hash, hf) => { + CachedVerificationState::ValidAtHashAndHF { block_hash, hf } => { if current_hf != hf { drop(guard); full_validation_transactions @@ -407,13 +339,17 @@ fn transactions_needing_verification( continue; } - if !hashes_in_main_chain.contains(hash) { + if !hashes_in_main_chain.contains(block_hash) { drop(guard); full_validation_transactions.push((tx.clone(), VerificationNeeded::Contextual)); continue; } } - CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock(hash, hf, lock) => { + CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock { + block_hash, + hf, + time_lock, + } => { if current_hf != hf { drop(guard); full_validation_transactions @@ -421,14 +357,14 @@ fn transactions_needing_verification( continue; } - if !hashes_in_main_chain.contains(hash) { + if !hashes_in_main_chain.contains(block_hash) { drop(guard); full_validation_transactions.push((tx.clone(), VerificationNeeded::Contextual)); continue; } // If the time lock is still locked then the transaction is invalid. - if !output_unlocked(lock, current_chain_height, time_for_time_lock, hf) { + if !output_unlocked(time_lock, current_chain_height, time_for_time_lock, hf) { return Err(ConsensusError::Transaction( TransactionError::OneOrMoreRingMembersLocked, )); @@ -517,10 +453,15 @@ where txs.iter() .zip(txs_ring_member_info) .for_each(|((tx, _), ring)| { - if ring.time_locked_outs.is_empty() { - *tx.cached_verification_state.lock().unwrap() = - CachedVerificationState::ValidAtHashAndHF(top_hash, hf); + *tx.cached_verification_state.lock().unwrap() = if ring.time_locked_outs.is_empty() + { + // no outputs with time-locks used. + CachedVerificationState::ValidAtHashAndHF { + block_hash: top_hash, + hf, + } } else { + // an output with a time-lock was used, check if it was time-based. let youngest_timebased_lock = ring .time_locked_outs .iter() @@ -530,16 +471,20 @@ where }) .min(); - *tx.cached_verification_state.lock().unwrap() = - if let Some(time) = youngest_timebased_lock { - CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock( - top_hash, - hf, - Timelock::Time(time), - ) - } else { - CachedVerificationState::ValidAtHashAndHF(top_hash, hf) - }; + if let Some(time) = youngest_timebased_lock { + // time-based lock used. + CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock { + block_hash: top_hash, + hf, + time_lock: Timelock::Time(time), + } + } else { + // no time-based locked output was used. + CachedVerificationState::ValidAtHashAndHF { + block_hash: top_hash, + hf, + } + } } }); diff --git a/consensus/src/transactions/free.rs b/consensus/src/transactions/free.rs index 5ffd16e8..02c52358 100644 --- a/consensus/src/transactions/free.rs +++ b/consensus/src/transactions/free.rs @@ -1,9 +1,40 @@ +use std::sync::Mutex as StdMutex; + use monero_serai::{ ringct::{bulletproofs::Bulletproof, RctType}, transaction::{Input, Transaction}, }; -use cuprate_consensus_rules::transactions::TransactionError; +use cuprate_consensus_rules::{transactions::TransactionError, ConsensusError}; +use cuprate_types::{CachedVerificationState, TransactionVerificationData, TxVersion}; + +/// Creates a new [`TransactionVerificationData`] from a [`Transaction`]. +/// +/// # Errors +/// +/// This function will return [`Err`] if the transaction is malformed, although returning [`Ok`] does +/// not necessarily mean the tx is correctly formed. +pub fn new_tx_verification_data( + tx: Transaction, +) -> Result { + let tx_hash = tx.hash(); + let tx_blob = tx.serialize(); + + let tx_weight = tx_weight(&tx, &tx_blob); + + let fee = tx_fee(&tx)?; + + Ok(TransactionVerificationData { + tx_hash, + version: TxVersion::from_raw(tx.version()) + .ok_or(TransactionError::TransactionVersionInvalid)?, + tx_blob, + tx_weight, + fee, + cached_verification_state: StdMutex::new(CachedVerificationState::NotVerified), + tx, + }) +} /// Calculates the weight of a [`Transaction`]. /// diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 4d358f41..de955c85 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -8,7 +8,7 @@ use cuprate_database::{ RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, }; use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; -use cuprate_types::{ExtendedBlockHeader, VerifiedBlockInformation}; +use cuprate_types::{ExtendedBlockHeader, HardFork, VerifiedBlockInformation}; use crate::{ ops::{ @@ -182,6 +182,7 @@ pub fn get_block_extended_header( /// Same as [`get_block_extended_header`] but with a [`BlockHeight`]. #[doc = doc_error!()] +#[allow(clippy::missing_panics_doc)] // The panic is only possible with a corrupt DB #[inline] pub fn get_block_extended_header_from_height( block_height: &BlockHeight, @@ -200,7 +201,8 @@ pub fn get_block_extended_header_from_height( #[allow(clippy::cast_possible_truncation)] Ok(ExtendedBlockHeader { cumulative_difficulty, - version: block.header.hardfork_version, + version: HardFork::from_version(block.header.hardfork_version) + .expect("Stored block must have a valid hard-fork"), vote: block.header.hardfork_signal, timestamp: block.header.timestamp, block_weight: block_info.weight as usize, @@ -369,7 +371,7 @@ mod test { let b1 = block_header_from_hash; let b2 = block; assert_eq!(b1, block_header_from_height); - assert_eq!(b1.version, b2.block.header.hardfork_version); + assert_eq!(b1.version.as_u8(), b2.block.header.hardfork_version); assert_eq!(b1.vote, b2.block.header.hardfork_signal); assert_eq!(b1.timestamp, b2.block.header.timestamp); assert_eq!(b1.cumulative_difficulty, b2.cumulative_difficulty); diff --git a/types/Cargo.toml b/types/Cargo.toml index 99fa978b..4c31cfc0 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -13,6 +13,7 @@ default = ["blockchain", "epee", "serde"] blockchain = [] epee = ["dep:cuprate-epee-encoding"] serde = ["dep:serde"] +proptest = ["dep:proptest", "dep:proptest-derive"] [dependencies] cuprate-epee-encoding = { path = "../net/epee-encoding", optional = true } @@ -23,5 +24,9 @@ curve25519-dalek = { workspace = true } monero-serai = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } borsh = { workspace = true, optional = true } +thiserror = { workspace = true } + +proptest = { workspace = true, optional = true } +proptest-derive = { workspace = true, optional = true } [dev-dependencies] \ No newline at end of file diff --git a/types/README.md b/types/README.md index 876931fd..6dd2388f 100644 --- a/types/README.md +++ b/types/README.md @@ -9,3 +9,4 @@ This crate is a kitchen-sink for data types that are shared across Cuprate. | `blockchain` | Enables the `blockchain` module, containing the blockchain database request/response types | `serde` | Enables `serde` on types where applicable | `epee` | Enables `cuprate-epee-encoding` on types where applicable +| `proptest` | Enables `proptest::arbitrary::Arbitrary` on some types diff --git a/types/src/hard_fork.rs b/types/src/hard_fork.rs new file mode 100644 index 00000000..412448e2 --- /dev/null +++ b/types/src/hard_fork.rs @@ -0,0 +1,131 @@ +//! The [`HardFork`] type. +use std::time::Duration; + +use monero_serai::block::BlockHeader; + +/// Target block time for hf 1. +/// +/// ref: +const BLOCK_TIME_V1: Duration = Duration::from_secs(60); +/// Target block time from v2. +/// +/// ref: +const BLOCK_TIME_V2: Duration = Duration::from_secs(120); + +/// An error working with a [`HardFork`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +pub enum HardForkError { + /// The raw-HF value is not a valid [`HardFork`]. + #[error("The hard-fork is unknown")] + HardForkUnknown, + /// The [`HardFork`] version is incorrect. + #[error("The block is on an incorrect hard-fork")] + VersionIncorrect, + /// The block's [`HardFork`] vote was below the current [`HardFork`]. + #[error("The block's vote is for a previous hard-fork")] + VoteTooLow, +} + +/// An identifier for every hard-fork Monero has had. +#[allow(missing_docs)] +#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +#[cfg_attr(any(feature = "proptest"), derive(proptest_derive::Arbitrary))] +#[repr(u8)] +pub enum HardFork { + #[default] + 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 [`BlockHeader::hardfork_version`] field. + /// + /// ref: + /// + /// # Errors + /// + /// Will return [`Err`] if the version is not a valid [`HardFork`]. + #[inline] + pub const fn from_version(version: u8) -> Result { + Ok(match version { + 1 => Self::V1, + 2 => Self::V2, + 3 => Self::V3, + 4 => Self::V4, + 5 => Self::V5, + 6 => Self::V6, + 7 => Self::V7, + 8 => Self::V8, + 9 => Self::V9, + 10 => Self::V10, + 11 => Self::V11, + 12 => Self::V12, + 13 => Self::V13, + 14 => Self::V14, + 15 => Self::V15, + 16 => Self::V16, + _ => return Err(HardForkError::HardForkUnknown), + }) + } + + /// Returns the hard-fork for a blocks [`BlockHeader::hardfork_signal`] (vote) field. + /// + /// + #[inline] + pub fn from_vote(vote: u8) -> Self { + if vote == 0 { + // A vote of 0 is interpreted as 1 as that's what Monero used to default to. + return Self::V1; + } + // This must default to the latest hard-fork! + Self::from_version(vote).unwrap_or(Self::V16) + } + + /// Returns the [`HardFork`] version and vote from this block header. + /// + /// # Errors + /// + /// Will return [`Err`] if the [`BlockHeader::hardfork_version`] is not a valid [`HardFork`]. + #[inline] + pub fn from_block_header(header: &BlockHeader) -> Result<(Self, Self), HardForkError> { + Ok(( + Self::from_version(header.hardfork_version)?, + Self::from_vote(header.hardfork_signal), + )) + } + + /// Returns the raw hard-fork value, as it would appear in [`BlockHeader::hardfork_version`]. + pub const fn as_u8(&self) -> u8 { + *self as u8 + } + + /// Returns the next hard-fork. + pub fn next_fork(&self) -> Option { + Self::from_version(*self as u8 + 1).ok() + } + + /// Returns the target block time for this hardfork. + /// + /// ref: + pub const fn block_time(&self) -> Duration { + match self { + Self::V1 => BLOCK_TIME_V1, + _ => BLOCK_TIME_V2, + } + } +} diff --git a/types/src/lib.rs b/types/src/lib.rs index bcf6a45d..d70f4c31 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -80,9 +80,15 @@ // Documentation for each module is located in the respective file. mod block_complete_entry; +mod hard_fork; +mod transaction_verification_data; mod types; pub use block_complete_entry::{BlockCompleteEntry, PrunedTxBlobEntry, TransactionBlobs}; +pub use hard_fork::{HardFork, HardForkError}; +pub use transaction_verification_data::{ + CachedVerificationState, TransactionVerificationData, TxVersion, +}; pub use types::{ AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation, VerifiedTransactionInformation, @@ -91,5 +97,4 @@ pub use types::{ //---------------------------------------------------------------------------------------------------- Feature-gated #[cfg(feature = "blockchain")] pub mod blockchain; - //---------------------------------------------------------------------------------------------------- Private diff --git a/types/src/transaction_verification_data.rs b/types/src/transaction_verification_data.rs new file mode 100644 index 00000000..68e17b81 --- /dev/null +++ b/types/src/transaction_verification_data.rs @@ -0,0 +1,94 @@ +//! Contains [`TransactionVerificationData`] and the related types. + +use std::sync::Mutex; + +use monero_serai::transaction::{Timelock, Transaction}; + +use crate::HardFork; + +/// An enum representing all valid Monero transaction versions. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum TxVersion { + /// Legacy ring signatures. + RingSignatures, + /// Ring-CT + RingCT, +} + +impl TxVersion { + /// Converts a `raw` version value to a [`TxVersion`]. + /// + /// This will return `None` on invalid values. + /// + /// ref: + /// && + pub const fn from_raw(version: u8) -> Option { + Some(match version { + 1 => Self::RingSignatures, + 2 => Self::RingCT, + _ => return None, + }) + } +} + +/// Represents if a transaction has been fully validated and under what conditions +/// the transaction is valid in the future. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CachedVerificationState { + /// The transaction has not been validated. + NotVerified, + /// The transaction is valid* if the block represented by this hash is in the blockchain and the [`HardFork`] + /// is the same. + /// + /// *V1 transactions require checks on their ring-length even if this hash is in the blockchain. + ValidAtHashAndHF { + /// The block hash that was in the chain when this transaction was validated. + block_hash: [u8; 32], + /// The hf this transaction was validated against. + hf: HardFork, + }, + /// The transaction is valid* if the block represented by this hash is in the blockchain _and_ this + /// given time lock is unlocked. The time lock here will represent the youngest used time based lock + /// (If the transaction uses any time based time locks). This is because time locks are not monotonic + /// so unlocked outputs could become re-locked. + /// + /// *V1 transactions require checks on their ring-length even if this hash is in the blockchain. + ValidAtHashAndHFWithTimeBasedLock { + /// The block hash that was in the chain when this transaction was validated. + block_hash: [u8; 32], + /// The hf this transaction was validated against. + hf: HardFork, + /// The youngest used time based lock. + time_lock: Timelock, + }, +} + +impl CachedVerificationState { + /// Returns the block hash this is valid for if in state [`CachedVerificationState::ValidAtHashAndHF`] or [`CachedVerificationState::ValidAtHashAndHFWithTimeBasedLock`]. + pub const fn verified_at_block_hash(&self) -> Option<[u8; 32]> { + match self { + Self::NotVerified => None, + Self::ValidAtHashAndHF { block_hash, .. } + | Self::ValidAtHashAndHFWithTimeBasedLock { block_hash, .. } => Some(*block_hash), + } + } +} + +/// Data needed to verify a transaction. +#[derive(Debug)] +pub struct TransactionVerificationData { + /// The transaction we are verifying + pub tx: Transaction, + /// The [`TxVersion`] of this tx. + pub version: TxVersion, + /// The serialised transaction. + pub tx_blob: Vec, + /// The weight of the transaction. + pub tx_weight: usize, + /// The fee this transaction has paid. + pub fee: u64, + /// The hash of this transaction. + pub tx_hash: [u8; 32], + /// The verification state of this transaction. + pub cached_verification_state: Mutex, +} diff --git a/types/src/types.rs b/types/src/types.rs index a4a7135f..4b6e2e12 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -7,6 +7,8 @@ use monero_serai::{ transaction::{Timelock, Transaction}, }; +use crate::HardFork; + //---------------------------------------------------------------------------------------------------- ExtendedBlockHeader /// Extended header data of a block. /// @@ -15,13 +17,11 @@ use monero_serai::{ pub struct ExtendedBlockHeader { /// The block's major version. /// - /// This can also be represented with `cuprate_consensus::HardFork`. - /// /// This is the same value as [`monero_serai::block::BlockHeader::hardfork_version`]. - pub version: u8, + pub version: HardFork, /// The block's hard-fork vote. /// - /// This can also be represented with `cuprate_consensus::HardFork`. + /// This can't be represented with [`HardFork`] as raw-votes can be out of the range of [`HardFork`]s. /// /// This is the same value as [`monero_serai::block::BlockHeader::hardfork_signal`]. pub vote: u8,