From 64eeb89a1a43aa61c0e7687ec70f78dbd340e59c Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Sun, 24 Nov 2024 16:23:38 +0200 Subject: [PATCH 1/5] Enable payloads for non coinbase transactions (#591) * Enable payloads for non coinbase transactions * Add payload hash to sighash * test reflects enabling payload * Enhance benchmarking: add payload size variations Refactored `mock_tx` to `mock_tx_with_payload` to support custom payload sizes. Introduced new benchmark function `benchmark_check_scripts_with_payload` to test performance with varying payload sizes. Commented out the old benchmark function to focus on payload-based tests. * Enhance script checking benchmarks Added benchmarks to evaluate script checking performance with varying payload sizes and input counts. This helps in understanding the impact of transaction payload size on validation and the relationship between input count and payload processing overhead. * Add new test case for transaction hashing and refactor code This commit introduces a new test case to verify that transaction IDs and hashes change with payload modifications. Additionally, code readability and consistency are improved by refactoring multi-line expressions into single lines where appropriate. * Add payload activation test for transactions This commit introduces a new integration test to validate the enforcement of payload activation rules at a specified DAA score. The test ensures that transactions with large payloads are rejected before activation and accepted afterward, maintaining consensus integrity. * style: fmt * test: add test that checks that payload change reflects sighash * rename test * Don't ever skip utxo_free_tx_validation * lints --------- Co-authored-by: max143672 --- consensus/benches/check_scripts.rs | 56 +++++-- consensus/core/src/config/params.rs | 13 ++ consensus/core/src/hashing/sighash.rs | 61 ++++++-- consensus/core/src/hashing/tx.rs | 7 + consensus/src/consensus/services.rs | 1 + .../body_validation_in_context.rs | 29 ++-- .../processes/transaction_validator/mod.rs | 4 + .../tx_validation_in_isolation.rs | 11 +- .../tx_validation_not_utxo_related.rs | 12 ++ simpa/Cargo.toml | 5 + simpa/src/main.rs | 4 + simpa/src/simulator/miner.rs | 8 +- simpa/src/simulator/network.rs | 2 + .../src/consensus_integration_tests.rs | 142 +++++++++++++++++- 14 files changed, 297 insertions(+), 58 deletions(-) diff --git a/consensus/benches/check_scripts.rs b/consensus/benches/check_scripts.rs index a451eec650..5d13c43d8e 100644 --- a/consensus/benches/check_scripts.rs +++ b/consensus/benches/check_scripts.rs @@ -13,8 +13,10 @@ use kaspa_utils::iter::parallelism_in_power_steps; use rand::{thread_rng, Rng}; use secp256k1::Keypair; -// You may need to add more detailed mocks depending on your actual code. -fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec) { +fn mock_tx_with_payload(inputs_count: usize, non_uniq_signatures: usize, payload_size: usize) -> (Transaction, Vec) { + let mut payload = vec![0u8; payload_size]; + thread_rng().fill(&mut payload[..]); + let reused_values = SigHashReusedValuesUnsync::new(); let dummy_prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1); let mut tx = Transaction::new( @@ -24,10 +26,11 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec 0, SubnetworkId::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 0, - vec![], + payload, ); let mut utxos = vec![]; let mut kps = vec![]; + for _ in 0..inputs_count - non_uniq_signatures { let kp = Keypair::new(secp256k1::SECP256K1, &mut thread_rng()); tx.inputs.push(TransactionInput { previous_outpoint: dummy_prev_out, signature_script: vec![], sequence: 0, sig_op_count: 1 }); @@ -40,6 +43,7 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec }); kps.push(kp); } + for _ in 0..non_uniq_signatures { let kp = kps.last().unwrap(); tx.inputs.push(TransactionInput { previous_outpoint: dummy_prev_out, signature_script: vec![], sequence: 0, sig_op_count: 1 }); @@ -51,14 +55,15 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec is_coinbase: false, }); } + for (i, kp) in kps.iter().enumerate().take(inputs_count - non_uniq_signatures) { let mut_tx = MutableTransaction::with_entries(&tx, utxos.clone()); let sig_hash = calc_schnorr_signature_hash(&mut_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values); let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); let sig: [u8; 64] = *kp.sign_schnorr(msg).as_ref(); - // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); } + let length = tx.inputs.len(); for i in (inputs_count - non_uniq_signatures)..length { let kp = kps.last().unwrap(); @@ -66,16 +71,16 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec let sig_hash = calc_schnorr_signature_hash(&mut_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values); let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); let sig: [u8; 64] = *kp.sign_schnorr(msg).as_ref(); - // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); } + (tx, utxos) } fn benchmark_check_scripts(c: &mut Criterion) { for inputs_count in [100, 50, 25, 10, 5, 2] { for non_uniq_signatures in [0, inputs_count / 2] { - let (tx, utxos) = mock_tx(inputs_count, non_uniq_signatures); + let (tx, utxos) = mock_tx_with_payload(inputs_count, non_uniq_signatures, 0); let mut group = c.benchmark_group(format!("inputs: {inputs_count}, non uniq: {non_uniq_signatures}")); group.sampling_mode(SamplingMode::Flat); @@ -97,12 +102,10 @@ fn benchmark_check_scripts(c: &mut Criterion) { }) }); - // Iterate powers of two up to available parallelism for i in parallelism_in_power_steps() { if inputs_count >= i { group.bench_function(format!("rayon, custom thread pool, thread count {i}"), |b| { let tx = MutableTransaction::with_entries(tx.clone(), utxos.clone()); - // Create a custom thread pool with the specified number of threads let pool = rayon::ThreadPoolBuilder::new().num_threads(i).build().unwrap(); let cache = Cache::new(inputs_count as u64); b.iter(|| { @@ -117,11 +120,44 @@ fn benchmark_check_scripts(c: &mut Criterion) { } } +/// Benchmarks script checking performance with different payload sizes and input counts. +/// +/// This benchmark evaluates the performance impact of transaction payload size +/// on script validation, testing multiple scenarios: +/// +/// * Payload sizes: 0KB, 16KB, 32KB, 64KB, 128KB +/// * Input counts: 1, 2, 10, 50 transactions +/// +/// The benchmark helps understand: +/// 1. How payload size affects validation performance +/// 2. The relationship between input count and payload processing overhead +fn benchmark_check_scripts_with_payload(c: &mut Criterion) { + let payload_sizes = [0, 16_384, 32_768, 65_536, 131_072]; // 0, 16KB, 32KB, 64KB, 128KB + let input_counts = [1, 2, 10, 50]; + let non_uniq_signatures = 0; + + for inputs_count in input_counts { + for &payload_size in &payload_sizes { + let (tx, utxos) = mock_tx_with_payload(inputs_count, non_uniq_signatures, payload_size); + let mut group = c.benchmark_group(format!("script_check/inputs_{}/payload_{}_kb", inputs_count, payload_size / 1024)); + group.sampling_mode(SamplingMode::Flat); + + group.bench_function("parallel_validation", |b| { + let tx = MutableTransaction::with_entries(tx.clone(), utxos.clone()); + let cache = Cache::new(inputs_count as u64); + b.iter(|| { + cache.clear(); + check_scripts_par_iter(black_box(&cache), black_box(&tx.as_verifiable()), false).unwrap(); + }) + }); + } + } +} + criterion_group! { name = benches; - // This can be any expression that returns a `Criterion` object. config = Criterion::default().with_output_color(true).measurement_time(std::time::Duration::new(20, 0)); - targets = benchmark_check_scripts + targets = benchmark_check_scripts, benchmark_check_scripts_with_payload } criterion_main!(benches); diff --git a/consensus/core/src/config/params.rs b/consensus/core/src/config/params.rs index 8cab11c92d..e5da18c256 100644 --- a/consensus/core/src/config/params.rs +++ b/consensus/core/src/config/params.rs @@ -130,6 +130,9 @@ pub struct Params { pub skip_proof_of_work: bool, pub max_block_level: BlockLevel, pub pruning_proof_m: u64, + + /// Activation rules for when to enable using the payload field in transactions + pub payload_activation: ForkActivation, } fn unix_now() -> u64 { @@ -406,6 +409,8 @@ pub const MAINNET_PARAMS: Params = Params { skip_proof_of_work: false, max_block_level: 225, pruning_proof_m: 1000, + + payload_activation: ForkActivation::never(), }; pub const TESTNET_PARAMS: Params = Params { @@ -469,6 +474,8 @@ pub const TESTNET_PARAMS: Params = Params { skip_proof_of_work: false, max_block_level: 250, pruning_proof_m: 1000, + + payload_activation: ForkActivation::never(), }; pub const TESTNET11_PARAMS: Params = Params { @@ -530,6 +537,8 @@ pub const TESTNET11_PARAMS: Params = Params { skip_proof_of_work: false, max_block_level: 250, + + payload_activation: ForkActivation::never(), }; pub const SIMNET_PARAMS: Params = Params { @@ -584,6 +593,8 @@ pub const SIMNET_PARAMS: Params = Params { skip_proof_of_work: true, // For simnet only, PoW can be simulated by default max_block_level: 250, + + payload_activation: ForkActivation::never(), }; pub const DEVNET_PARAMS: Params = Params { @@ -641,4 +652,6 @@ pub const DEVNET_PARAMS: Params = Params { skip_proof_of_work: false, max_block_level: 250, pruning_proof_m: 1000, + + payload_activation: ForkActivation::never(), }; diff --git a/consensus/core/src/hashing/sighash.rs b/consensus/core/src/hashing/sighash.rs index e6c7ad4dd0..05645356dd 100644 --- a/consensus/core/src/hashing/sighash.rs +++ b/consensus/core/src/hashing/sighash.rs @@ -3,10 +3,7 @@ use kaspa_hashes::{Hash, Hasher, HasherBase, TransactionSigningHash, Transaction use std::cell::Cell; use std::sync::Arc; -use crate::{ - subnets::SUBNETWORK_ID_NATIVE, - tx::{ScriptPublicKey, Transaction, TransactionOutpoint, TransactionOutput, VerifiableTransaction}, -}; +use crate::tx::{ScriptPublicKey, Transaction, TransactionOutpoint, TransactionOutput, VerifiableTransaction}; use super::{sighash_type::SigHashType, HasherExtensions}; @@ -19,6 +16,7 @@ pub struct SigHashReusedValuesUnsync { sequences_hash: Cell>, sig_op_counts_hash: Cell>, outputs_hash: Cell>, + payload_hash: Cell>, } impl SigHashReusedValuesUnsync { @@ -33,6 +31,7 @@ pub struct SigHashReusedValuesSync { sequences_hash: ArcSwapOption, sig_op_counts_hash: ArcSwapOption, outputs_hash: ArcSwapOption, + payload_hash: ArcSwapOption, } impl SigHashReusedValuesSync { @@ -46,6 +45,7 @@ pub trait SigHashReusedValues { fn sequences_hash(&self, set: impl Fn() -> Hash) -> Hash; fn sig_op_counts_hash(&self, set: impl Fn() -> Hash) -> Hash; fn outputs_hash(&self, set: impl Fn() -> Hash) -> Hash; + fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash; } impl SigHashReusedValues for Arc { @@ -64,6 +64,10 @@ impl SigHashReusedValues for Arc { fn outputs_hash(&self, set: impl Fn() -> Hash) -> Hash { self.as_ref().outputs_hash(set) } + + fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash { + self.as_ref().outputs_hash(set) + } } impl SigHashReusedValues for SigHashReusedValuesUnsync { @@ -98,6 +102,14 @@ impl SigHashReusedValues for SigHashReusedValuesUnsync { hash }) } + + fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash { + self.payload_hash.get().unwrap_or_else(|| { + let hash = set(); + self.payload_hash.set(Some(hash)); + hash + }) + } } impl SigHashReusedValues for SigHashReusedValuesSync { @@ -136,6 +148,15 @@ impl SigHashReusedValues for SigHashReusedValuesSync { self.outputs_hash.rcu(|_| Arc::new(hash)); hash } + + fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash { + if let Some(value) = self.payload_hash.load().as_ref() { + return **value; + } + let hash = set(); + self.payload_hash.rcu(|_| Arc::new(hash)); + hash + } } pub fn previous_outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &impl SigHashReusedValues) -> Hash { @@ -182,17 +203,17 @@ pub fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_value reused_values.sig_op_counts_hash(hash) } -pub fn payload_hash(tx: &Transaction) -> Hash { - if tx.subnetwork_id == SUBNETWORK_ID_NATIVE { - return ZERO_HASH; - } +pub fn payload_hash(tx: &Transaction, reused_values: &impl SigHashReusedValues) -> Hash { + let hash = || { + if tx.subnetwork_id.is_native() && tx.payload.is_empty() { + return ZERO_HASH; + } - // TODO: Right now this branch will never be executed, since payload is disabled - // for all non coinbase transactions. Once payload is enabled, the payload hash - // should be cached to make it cost O(1) instead of O(tx.inputs.len()). - let mut hasher = TransactionSigningHash::new(); - hasher.write_var_bytes(&tx.payload); - hasher.finalize() + let mut hasher = TransactionSigningHash::new(); + hasher.write_var_bytes(&tx.payload); + hasher.finalize() + }; + reused_values.payload_hash(hash) } pub fn outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &impl SigHashReusedValues, input_index: usize) -> Hash { @@ -260,7 +281,7 @@ pub fn calc_schnorr_signature_hash( .write_u64(tx.lock_time) .update(&tx.subnetwork_id) .write_u64(tx.gas) - .update(payload_hash(tx)) + .update(payload_hash(tx, reused_values)) .write_u8(hash_type.to_u8()); hasher.finalize() } @@ -285,7 +306,7 @@ mod tests { use crate::{ hashing::sighash_type::{SIG_HASH_ALL, SIG_HASH_ANY_ONE_CAN_PAY, SIG_HASH_NONE, SIG_HASH_SINGLE}, - subnets::SubnetworkId, + subnets::{SubnetworkId, SUBNETWORK_ID_NATIVE}, tx::{PopulatedTransaction, Transaction, TransactionId, TransactionInput, UtxoEntry}, }; @@ -608,6 +629,14 @@ mod tests { action: ModifyAction::NoAction, expected_hash: "846689131fb08b77f83af1d3901076732ef09d3f8fdff945be89aa4300562e5f", // should change the hash }, + TestVector { + name: "native-all-0-modify-payload", + populated_tx: &native_populated_tx, + hash_type: SIG_HASH_ALL, + input_index: 0, + action: ModifyAction::Payload, + expected_hash: "72ea6c2871e0f44499f1c2b556f265d9424bfea67cca9cb343b4b040ead65525", // should change the hash + }, // subnetwork transaction TestVector { name: "subnetwork-all-0", diff --git a/consensus/core/src/hashing/tx.rs b/consensus/core/src/hashing/tx.rs index 019f2a8f5b..9216a1c16e 100644 --- a/consensus/core/src/hashing/tx.rs +++ b/consensus/core/src/hashing/tx.rs @@ -157,6 +157,13 @@ mod tests { expected_hash: "31da267d5c34f0740c77b8c9ebde0845a01179ec68074578227b804bac306361", }); + // Test #8, same as 7 but with a non-zero payload. The test checks id and hash are affected by payload change + tests.push(Test { + tx: Transaction::new(2, inputs.clone(), outputs.clone(), 54, subnets::SUBNETWORK_ID_REGISTRY, 3, vec![1, 2, 3]), + expected_id: "1f18b18ab004ff1b44dd915554b486d64d7ebc02c054e867cc44e3d746e80b3b", + expected_hash: "a2029ebd66d29d41aa7b0c40230c1bfa7fe8e026fb44b7815dda4e991b9a5fad", + }); + for (i, test) in tests.iter().enumerate() { assert_eq!(test.tx.id(), Hash::from_str(test.expected_id).unwrap(), "transaction id failed for test {}", i + 1); assert_eq!( diff --git a/consensus/src/consensus/services.rs b/consensus/src/consensus/services.rs index 16247db18b..06abb4e0bb 100644 --- a/consensus/src/consensus/services.rs +++ b/consensus/src/consensus/services.rs @@ -147,6 +147,7 @@ impl ConsensusServices { mass_calculator.clone(), params.storage_mass_activation, params.kip10_activation, + params.payload_activation, ); let pruning_point_manager = PruningPointManager::new( diff --git a/consensus/src/pipeline/body_processor/body_validation_in_context.rs b/consensus/src/pipeline/body_processor/body_validation_in_context.rs index ec42f0f447..2b1bd99487 100644 --- a/consensus/src/pipeline/body_processor/body_validation_in_context.rs +++ b/consensus/src/pipeline/body_processor/body_validation_in_context.rs @@ -8,7 +8,6 @@ use kaspa_consensus_core::block::Block; use kaspa_database::prelude::StoreResultExtensions; use kaspa_hashes::Hash; use kaspa_utils::option::OptionExtensions; -use once_cell::unsync::Lazy; use std::sync::Arc; impl BlockBodyProcessor { @@ -21,27 +20,17 @@ impl BlockBodyProcessor { fn check_block_transactions_in_context(self: &Arc, block: &Block) -> BlockProcessResult<()> { // Note: This is somewhat expensive during ibd, as it incurs cache misses. - // Use lazy evaluation to avoid unnecessary work, as most of the time we expect the txs not to have lock time. - let lazy_pmt_res = - Lazy::new(|| match self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap()) { - Ok((pmt, pmt_window)) => { - if !self.block_window_cache_for_past_median_time.contains_key(&block.hash()) { - self.block_window_cache_for_past_median_time.insert(block.hash(), pmt_window); - }; - Ok(pmt) - } - Err(e) => Err(e), - }); + let pmt = { + let (pmt, pmt_window) = self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap())?; + if !self.block_window_cache_for_past_median_time.contains_key(&block.hash()) { + self.block_window_cache_for_past_median_time.insert(block.hash(), pmt_window); + }; + pmt + }; for tx in block.transactions.iter() { - // Quick check to avoid the expensive Lazy eval during ibd (in most cases). - // TODO: refactor this and avoid classifying the tx lock outside of the transaction validator. - if tx.lock_time != 0 { - if let Err(e) = - self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, (*lazy_pmt_res).clone()?) - { - return Err(RuleError::TxInContextFailed(tx.id(), e)); - }; + if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, pmt) { + return Err(RuleError::TxInContextFailed(tx.id(), e)); }; } Ok(()) diff --git a/consensus/src/processes/transaction_validator/mod.rs b/consensus/src/processes/transaction_validator/mod.rs index 7d007a3350..3f091dfd76 100644 --- a/consensus/src/processes/transaction_validator/mod.rs +++ b/consensus/src/processes/transaction_validator/mod.rs @@ -30,6 +30,7 @@ pub struct TransactionValidator { storage_mass_activation: ForkActivation, /// KIP-10 hardfork DAA score kip10_activation: ForkActivation, + payload_activation: ForkActivation, } impl TransactionValidator { @@ -46,6 +47,7 @@ impl TransactionValidator { mass_calculator: MassCalculator, storage_mass_activation: ForkActivation, kip10_activation: ForkActivation, + payload_activation: ForkActivation, ) -> Self { Self { max_tx_inputs, @@ -59,6 +61,7 @@ impl TransactionValidator { mass_calculator, storage_mass_activation, kip10_activation, + payload_activation, } } @@ -84,6 +87,7 @@ impl TransactionValidator { mass_calculator: MassCalculator::new(0, 0, 0, 0), storage_mass_activation: ForkActivation::never(), kip10_activation: ForkActivation::never(), + payload_activation: ForkActivation::never(), } } } diff --git a/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs b/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs index 914624f940..a08b83d94e 100644 --- a/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs +++ b/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs @@ -16,7 +16,6 @@ impl TransactionValidator { check_transaction_output_value_ranges(tx)?; check_duplicate_transaction_inputs(tx)?; check_gas(tx)?; - check_transaction_payload(tx)?; check_transaction_subnetwork(tx)?; check_transaction_version(tx) } @@ -107,14 +106,6 @@ fn check_gas(tx: &Transaction) -> TxResult<()> { Ok(()) } -fn check_transaction_payload(tx: &Transaction) -> TxResult<()> { - // This should be revised if subnetworks are activated (along with other validations that weren't copied from kaspad) - if !tx.is_coinbase() && !tx.payload.is_empty() { - return Err(TxRuleError::NonCoinbaseTxHasPayload); - } - Ok(()) -} - fn check_transaction_version(tx: &Transaction) -> TxResult<()> { if tx.version != TX_VERSION { return Err(TxRuleError::UnknownTxVersion(tx.version)); @@ -304,7 +295,7 @@ mod tests { let mut tx = valid_tx.clone(); tx.payload = vec![0]; - assert_match!(tv.validate_tx_in_isolation(&tx), Err(TxRuleError::NonCoinbaseTxHasPayload)); + assert_match!(tv.validate_tx_in_isolation(&tx), Ok(())); let mut tx = valid_tx; tx.version = TX_VERSION + 1; diff --git a/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs b/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs index 4cfa72b464..3a854948ac 100644 --- a/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs +++ b/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs @@ -9,6 +9,7 @@ use super::{ impl TransactionValidator { pub fn utxo_free_tx_validation(&self, tx: &Transaction, ctx_daa_score: u64, ctx_block_time: u64) -> TxResult<()> { + self.check_transaction_payload(tx, ctx_daa_score)?; self.check_tx_is_finalized(tx, ctx_daa_score, ctx_block_time) } @@ -38,4 +39,15 @@ impl TransactionValidator { Ok(()) } + + fn check_transaction_payload(&self, tx: &Transaction, ctx_daa_score: u64) -> TxResult<()> { + if self.payload_activation.is_active(ctx_daa_score) { + Ok(()) + } else { + if !tx.is_coinbase() && !tx.payload.is_empty() { + return Err(TxRuleError::NonCoinbaseTxHasPayload); + } + Ok(()) + } + } } diff --git a/simpa/Cargo.toml b/simpa/Cargo.toml index 815edf6a64..bea3110e1e 100644 --- a/simpa/Cargo.toml +++ b/simpa/Cargo.toml @@ -40,3 +40,8 @@ tokio = { workspace = true, features = ["rt", "macros", "rt-multi-thread"] } [features] heap = ["dhat", "kaspa-alloc/heap"] semaphore-trace = ["kaspa-utils/semaphore-trace"] + +[profile.heap] +inherits = "release" +debug = true +strip = false diff --git a/simpa/src/main.rs b/simpa/src/main.rs index a2365e1c9f..c35c0c640e 100644 --- a/simpa/src/main.rs +++ b/simpa/src/main.rs @@ -122,6 +122,8 @@ struct Args { rocksdb_files_limit: Option, #[arg(long)] rocksdb_mem_budget: Option, + #[arg(long, default_value_t = false)] + long_payload: bool, } #[cfg(feature = "heap")] @@ -191,6 +193,7 @@ fn main_impl(mut args: Args) { let mut params = if args.testnet11 { TESTNET11_PARAMS } else { DEVNET_PARAMS }; params.storage_mass_activation = ForkActivation::new(400); params.storage_mass_parameter = 10_000; + params.payload_activation = ForkActivation::always(); let mut builder = ConfigBuilder::new(params) .apply_args(|config| apply_args_to_consensus_params(&args, &mut config.params)) .apply_args(|config| apply_args_to_perf_params(&args, &mut config.perf)) @@ -245,6 +248,7 @@ fn main_impl(mut args: Args) { args.rocksdb_stats_period_sec, args.rocksdb_files_limit, args.rocksdb_mem_budget, + args.long_payload, ) .run(until); consensus.shutdown(handles); diff --git a/simpa/src/simulator/miner.rs b/simpa/src/simulator/miner.rs index a9a4a3423d..9cd985937f 100644 --- a/simpa/src/simulator/miner.rs +++ b/simpa/src/simulator/miner.rs @@ -74,6 +74,7 @@ pub struct Miner { target_txs_per_block: u64, target_blocks: Option, max_cached_outpoints: usize, + long_payload: bool, // Mass calculator mass_calculator: MassCalculator, @@ -90,6 +91,7 @@ impl Miner { params: &Params, target_txs_per_block: u64, target_blocks: Option, + long_payload: bool, ) -> Self { let (schnorr_public_key, _) = pk.x_only_public_key(); let script_pub_key_script = once(0x20).chain(schnorr_public_key.serialize()).chain(once(0xac)).collect_vec(); // TODO: Use script builder when available to create p2pk properly @@ -114,6 +116,7 @@ impl Miner { params.mass_per_sig_op, params.storage_mass_parameter, ), + long_payload, } } @@ -143,7 +146,10 @@ impl Miner { .iter() .filter_map(|&outpoint| { let entry = self.get_spendable_entry(virtual_utxo_view, outpoint, virtual_state.daa_score)?; - let unsigned_tx = self.create_unsigned_tx(outpoint, entry.amount, multiple_outputs); + let mut unsigned_tx = self.create_unsigned_tx(outpoint, entry.amount, multiple_outputs); + if self.long_payload { + unsigned_tx.payload = vec![0; 90_000]; + } Some(MutableTransaction::with_entries(unsigned_tx, vec![entry])) }) .take(self.target_txs_per_block as usize) diff --git a/simpa/src/simulator/network.rs b/simpa/src/simulator/network.rs index 63e5a3b6cc..79ac6fad75 100644 --- a/simpa/src/simulator/network.rs +++ b/simpa/src/simulator/network.rs @@ -50,6 +50,7 @@ impl KaspaNetworkSimulator { rocksdb_stats_period_sec: Option, rocksdb_files_limit: Option, rocksdb_mem_budget: Option, + long_payload: bool, ) -> &mut Self { let secp = secp256k1::Secp256k1::new(); let mut rng = rand::thread_rng(); @@ -98,6 +99,7 @@ impl KaspaNetworkSimulator { &self.config, target_txs_per_block, self.target_blocks, + long_payload, )); self.simulation.register(i, miner_process); self.consensuses.push((consensus, handles, lifetime)); diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index 3db614dc41..58a6e2bb33 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -27,6 +27,7 @@ use kaspa_consensus_core::api::{BlockValidationFutures, ConsensusApi}; use kaspa_consensus_core::block::Block; use kaspa_consensus_core::blockhash::new_unique; use kaspa_consensus_core::blockstatus::BlockStatus; +use kaspa_consensus_core::coinbase::MinerData; use kaspa_consensus_core::constants::{BLOCK_VERSION, SOMPI_PER_KASPA, STORAGE_MASS_PARAMETER}; use kaspa_consensus_core::errors::block::{BlockProcessResult, RuleError}; use kaspa_consensus_core::header::Header; @@ -47,7 +48,7 @@ use crate::common; use flate2::read::GzDecoder; use futures_util::future::try_join_all; use itertools::Itertools; -use kaspa_consensus_core::coinbase::MinerData; +use kaspa_consensus_core::errors::tx::TxRuleError; use kaspa_consensus_core::merkle::calc_hash_merkle_root; use kaspa_consensus_core::muhash::MuHashExtensions; use kaspa_core::core::Core; @@ -61,6 +62,7 @@ use kaspa_math::Uint256; use kaspa_muhash::MuHash; use kaspa_notify::subscription::context::SubscriptionContext; use kaspa_txscript::caches::TxScriptCacheCounters; +use kaspa_txscript::opcodes::codes::OpTrue; use kaspa_utxoindex::api::{UtxoIndexApi, UtxoIndexProxy}; use kaspa_utxoindex::UtxoIndex; use serde::{Deserialize, Serialize}; @@ -842,6 +844,7 @@ impl KaspadGoParams { skip_proof_of_work: self.SkipProofOfWork, max_block_level: self.MaxBlockLevel, pruning_proof_m: self.PruningProofM, + payload_activation: ForkActivation::never(), } } } @@ -1865,3 +1868,140 @@ async fn run_kip10_activation_test() { assert!(matches!(status, Ok(BlockStatus::StatusUTXOValid))); assert!(consensus.lkg_virtual_state.load().accepted_tx_ids.contains(&tx_id)); } + +#[tokio::test] +async fn payload_test() { + let config = ConfigBuilder::new(DEVNET_PARAMS) + .skip_proof_of_work() + .edit_consensus_params(|p| { + p.coinbase_maturity = 0; + p.payload_activation = ForkActivation::always() + }) + .build(); + let consensus = TestConsensus::new(&config); + let wait_handles = consensus.init(); + + let miner_data = MinerData::new(ScriptPublicKey::from_vec(0, vec![OpTrue]), vec![]); + let b = consensus.build_utxo_valid_block_with_parents(1.into(), vec![config.genesis.hash], miner_data.clone(), vec![]); + consensus.validate_and_insert_block(b.to_immutable()).virtual_state_task.await.unwrap(); + let funding_block = consensus.build_utxo_valid_block_with_parents(2.into(), vec![1.into()], miner_data, vec![]); + let cb_id = { + let mut cb = funding_block.transactions[0].clone(); + cb.finalize(); + cb.id() + }; + consensus.validate_and_insert_block(funding_block.to_immutable()).virtual_state_task.await.unwrap(); + let tx = Transaction::new( + 0, + vec![TransactionInput::new(TransactionOutpoint { transaction_id: cb_id, index: 0 }, vec![], 0, 0)], + vec![TransactionOutput::new(1, ScriptPublicKey::default())], + 0, + SubnetworkId::default(), + 0, + vec![0; (config.params.max_block_mass / 2) as usize], + ); + consensus.add_utxo_valid_block_with_parents(3.into(), vec![2.into()], vec![tx]).await.unwrap(); + + consensus.shutdown(wait_handles); +} + +#[tokio::test] +async fn payload_activation_test() { + use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; + + // Set payload activation at DAA score 3 for this test + const PAYLOAD_ACTIVATION_DAA_SCORE: u64 = 3; + + init_allocator_with_default_settings(); + + // Create initial UTXO to fund our test transactions + let initial_utxo_collection = [( + TransactionOutpoint::new(1.into(), 0), + UtxoEntry { + amount: SOMPI_PER_KASPA, + script_public_key: ScriptPublicKey::from_vec(0, vec![OpTrue]), + block_daa_score: 0, + is_coinbase: false, + }, + )]; + + // Initialize consensus with payload activation point + let config = ConfigBuilder::new(DEVNET_PARAMS) + .skip_proof_of_work() + .apply_args(|cfg| { + let mut genesis_multiset = MuHash::new(); + initial_utxo_collection.iter().for_each(|(outpoint, utxo)| { + genesis_multiset.add_utxo(outpoint, utxo); + }); + cfg.params.genesis.utxo_commitment = genesis_multiset.finalize(); + let genesis_header: Header = (&cfg.params.genesis).into(); + cfg.params.genesis.hash = genesis_header.hash; + }) + .edit_consensus_params(|p| { + p.payload_activation = ForkActivation::new(PAYLOAD_ACTIVATION_DAA_SCORE); + }) + .build(); + + let consensus = TestConsensus::new(&config); + let mut genesis_multiset = MuHash::new(); + consensus.append_imported_pruning_point_utxos(&initial_utxo_collection, &mut genesis_multiset); + consensus.import_pruning_point_utxo_set(config.genesis.hash, genesis_multiset).unwrap(); + consensus.init(); + + // Build blockchain up to one block before activation + let mut index = 0; + for _ in 0..PAYLOAD_ACTIVATION_DAA_SCORE - 1 { + let parent = if index == 0 { config.genesis.hash } else { index.into() }; + consensus.add_utxo_valid_block_with_parents((index + 1).into(), vec![parent], vec![]).await.unwrap(); + index += 1; + } + assert_eq!(consensus.get_virtual_daa_score(), index); + + // Create transaction with large payload + let large_payload = vec![0u8; (config.params.max_block_mass / 2) as usize]; + let mut tx_with_payload = Transaction::new( + 0, + vec![TransactionInput::new( + initial_utxo_collection[0].0, + vec![], // Empty signature script since we're using OpTrue + 0, + 0, + )], + vec![TransactionOutput::new(initial_utxo_collection[0].1.amount - 5000, ScriptPublicKey::from_vec(0, vec![OpTrue]))], + 0, + SUBNETWORK_ID_NATIVE, + 0, + large_payload, + ); + tx_with_payload.finalize(); + let tx_id = tx_with_payload.id(); + + // Test 1: Build empty block, then manually insert invalid tx and verify consensus rejects it + { + let miner_data = MinerData::new(ScriptPublicKey::from_vec(0, vec![]), vec![]); + + // First build block without transactions + let mut block = + consensus.build_utxo_valid_block_with_parents((index + 1).into(), vec![index.into()], miner_data.clone(), vec![]); + + // Insert our test transaction and recalculate block hashes + block.transactions.push(tx_with_payload.clone()); + + block.header.hash_merkle_root = calc_hash_merkle_root(block.transactions.iter(), false); + let block_status = consensus.validate_and_insert_block(block.to_immutable()).virtual_state_task.await; + assert!(matches!(block_status, Err(RuleError::TxInContextFailed(tx, TxRuleError::NonCoinbaseTxHasPayload)) if tx == tx_id)); + assert_eq!(consensus.lkg_virtual_state.load().daa_score, PAYLOAD_ACTIVATION_DAA_SCORE - 1); + index += 1; + } + + // Add one more block to reach activation score + consensus.add_utxo_valid_block_with_parents((index + 1).into(), vec![(index - 1).into()], vec![]).await.unwrap(); + index += 1; + + // Test 2: Verify the same transaction is accepted after activation + let status = + consensus.add_utxo_valid_block_with_parents((index + 1).into(), vec![index.into()], vec![tx_with_payload.clone()]).await; + + assert!(matches!(status, Ok(BlockStatus::StatusUTXOValid))); + assert!(consensus.lkg_virtual_state.load().accepted_tx_ids.contains(&tx_id)); +} From ea6b83e7b78d303a7103d9eefa0adec82f16b8b5 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 28 Nov 2024 13:11:53 +0200 Subject: [PATCH 2/5] Small fixes related to enabling payload (#605) --- consensus/core/src/hashing/sighash.rs | 30 ++++----------------------- simpa/Cargo.toml | 5 ----- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/consensus/core/src/hashing/sighash.rs b/consensus/core/src/hashing/sighash.rs index 05645356dd..2c8006f75d 100644 --- a/consensus/core/src/hashing/sighash.rs +++ b/consensus/core/src/hashing/sighash.rs @@ -48,28 +48,6 @@ pub trait SigHashReusedValues { fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash; } -impl SigHashReusedValues for Arc { - fn previous_outputs_hash(&self, set: impl Fn() -> Hash) -> Hash { - self.as_ref().previous_outputs_hash(set) - } - - fn sequences_hash(&self, set: impl Fn() -> Hash) -> Hash { - self.as_ref().sequences_hash(set) - } - - fn sig_op_counts_hash(&self, set: impl Fn() -> Hash) -> Hash { - self.as_ref().sig_op_counts_hash(set) - } - - fn outputs_hash(&self, set: impl Fn() -> Hash) -> Hash { - self.as_ref().outputs_hash(set) - } - - fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash { - self.as_ref().outputs_hash(set) - } -} - impl SigHashReusedValues for SigHashReusedValuesUnsync { fn previous_outputs_hash(&self, set: impl Fn() -> Hash) -> Hash { self.previous_outputs_hash.get().unwrap_or_else(|| { @@ -204,11 +182,11 @@ pub fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_value } pub fn payload_hash(tx: &Transaction, reused_values: &impl SigHashReusedValues) -> Hash { - let hash = || { - if tx.subnetwork_id.is_native() && tx.payload.is_empty() { - return ZERO_HASH; - } + if tx.subnetwork_id.is_native() && tx.payload.is_empty() { + return ZERO_HASH; + } + let hash = || { let mut hasher = TransactionSigningHash::new(); hasher.write_var_bytes(&tx.payload); hasher.finalize() diff --git a/simpa/Cargo.toml b/simpa/Cargo.toml index bea3110e1e..815edf6a64 100644 --- a/simpa/Cargo.toml +++ b/simpa/Cargo.toml @@ -40,8 +40,3 @@ tokio = { workspace = true, features = ["rt", "macros", "rt-multi-thread"] } [features] heap = ["dhat", "kaspa-alloc/heap"] semaphore-trace = ["kaspa-utils/semaphore-trace"] - -[profile.heap] -inherits = "release" -debug = true -strip = false From 73159f78767669548ee4470ab2fbed172c855f64 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 28 Nov 2024 20:48:22 +0200 Subject: [PATCH 3/5] Fix new lints required by Rust 1.83 (#606) * cargo fix all new 1.83 lints except for static_mut_refs * use LazyLock for CONTEXT * allow static_mut_refs for wallet storage paths * bump MSRV to 1.82 and use the recent is_none_or --- Cargo.toml | 2 +- cli/src/modules/history.rs | 10 +------- consensus/client/src/header.rs | 2 +- consensus/client/src/input.rs | 2 +- consensus/client/src/output.rs | 2 +- consensus/client/src/transaction.rs | 2 +- consensus/client/src/utxo.rs | 4 ++-- consensus/core/src/config/constants.rs | 5 ++-- consensus/core/src/network.rs | 4 ++-- consensus/core/src/tx.rs | 6 ++--- consensus/core/src/tx/script_public_key.rs | 4 ++-- consensus/src/model/services/reachability.rs | 1 - consensus/src/model/stores/ghostdag.rs | 8 +++---- consensus/src/model/stores/relations.rs | 2 +- consensus/src/model/stores/utxo_diffs.rs | 1 - .../body_validation_in_context.rs | 3 +-- consensus/src/processes/coinbase.rs | 11 ++++----- consensus/src/processes/pruning.rs | 3 +-- consensus/src/processes/sync/mod.rs | 3 +-- consensus/src/test_helpers.rs | 7 ++---- core/src/task/runtime.rs | 5 ++-- crypto/addresses/src/lib.rs | 2 +- crypto/hashes/src/lib.rs | 2 +- crypto/muhash/src/lib.rs | 2 +- crypto/txscript/src/lib.rs | 2 +- database/src/access.rs | 6 +++-- .../src/mempool/model/frontier/search_tree.rs | 2 +- mining/src/model/topological_sort.rs | 4 ++-- protocol/flows/src/flowcontext/orphans.rs | 8 +++---- .../flows/src/flowcontext/transactions.rs | 3 +-- rothschild/src/main.rs | 2 +- rpc/core/src/model/header.rs | 1 - rpc/wrpc/wasm/src/resolver.rs | 2 +- utils/src/as_slice.rs | 6 ++--- utils/src/iter.rs | 4 ++-- utils/src/lib.rs | 1 - utils/src/option.rs | 13 ----------- utils/src/serde_bytes/de.rs | 2 +- wallet/bip32/src/mnemonic/bits.rs | 2 +- wallet/bip32/src/xpublic_key.rs | 2 +- wallet/core/src/storage/local/mod.rs | 15 +++++++++--- wallet/core/src/tx/generator/generator.rs | 1 - wallet/core/src/tx/payment.rs | 4 ++-- wallet/core/src/wallet/mod.rs | 4 ++-- wallet/core/src/wasm/cryptobox.rs | 4 ++-- wallet/core/src/wasm/utxo/context.rs | 2 +- wallet/core/src/wasm/utxo/processor.rs | 2 +- wallet/keys/src/derivation_path.rs | 2 +- wallet/keys/src/keypair.rs | 2 +- wallet/keys/src/privatekey.rs | 2 +- wallet/keys/src/publickey.rs | 2 +- wallet/keys/src/xprv.rs | 2 +- wallet/keys/src/xpub.rs | 2 +- wallet/pskt/src/bundle.rs | 23 ++++++++----------- wallet/pskt/src/wasm/pskt.rs | 2 +- 55 files changed, 94 insertions(+), 128 deletions(-) delete mode 100644 utils/src/option.rs diff --git a/Cargo.toml b/Cargo.toml index 7141101f9a..aa304d37fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ members = [ ] [workspace.package] -rust-version = "1.81.0" +rust-version = "1.82.0" version = "0.15.3" authors = ["Kaspa developers"] license = "ISC" diff --git a/cli/src/modules/history.rs b/cli/src/modules/history.rs index 8fdf31f4db..f46527f1d6 100644 --- a/cli/src/modules/history.rs +++ b/cli/src/modules/history.rs @@ -86,15 +86,7 @@ impl History { } }; let length = ids.size_hint().0; - let skip = if let Some(last) = last { - if last > length { - 0 - } else { - length - last - } - } else { - 0 - }; + let skip = if let Some(last) = last { length.saturating_sub(last) } else { 0 }; let mut index = 0; let page = 25; diff --git a/consensus/client/src/header.rs b/consensus/client/src/header.rs index 6f04a73c43..7d2e25b393 100644 --- a/consensus/client/src/header.rs +++ b/consensus/client/src/header.rs @@ -266,7 +266,7 @@ impl Header { impl TryCastFromJs for Header { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/client/src/input.rs b/consensus/client/src/input.rs index a5018199d5..0c5e052f2a 100644 --- a/consensus/client/src/input.rs +++ b/consensus/client/src/input.rs @@ -200,7 +200,7 @@ impl AsRef for TransactionInput { impl TryCastFromJs for TransactionInput { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/client/src/output.rs b/consensus/client/src/output.rs index 17b4a58c80..01772dde32 100644 --- a/consensus/client/src/output.rs +++ b/consensus/client/src/output.rs @@ -139,7 +139,7 @@ impl From<&TransactionOutput> for cctx::TransactionOutput { impl TryCastFromJs for TransactionOutput { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/client/src/transaction.rs b/consensus/client/src/transaction.rs index 17cc381265..4026ac1ebc 100644 --- a/consensus/client/src/transaction.rs +++ b/consensus/client/src/transaction.rs @@ -280,7 +280,7 @@ impl Transaction { impl TryCastFromJs for Transaction { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index bbfc1199d1..99a663fd05 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -282,7 +282,7 @@ impl TryIntoUtxoEntryReferences for JsValue { impl TryCastFromJs for UtxoEntry { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { @@ -405,7 +405,7 @@ impl TryFrom for UtxoEntries { impl TryCastFromJs for UtxoEntryReference { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/core/src/config/constants.rs b/consensus/core/src/config/constants.rs index 146e30a17c..899773bbf0 100644 --- a/consensus/core/src/config/constants.rs +++ b/consensus/core/src/config/constants.rs @@ -36,7 +36,7 @@ pub mod consensus { /// Size of the **sampled** median time window (independent of BPS) pub const MEDIAN_TIME_SAMPLED_WINDOW_SIZE: u64 = - ((2 * NEW_TIMESTAMP_DEVIATION_TOLERANCE - 1) + PAST_MEDIAN_TIME_SAMPLE_INTERVAL - 1) / PAST_MEDIAN_TIME_SAMPLE_INTERVAL; + (2 * NEW_TIMESTAMP_DEVIATION_TOLERANCE - 1).div_ceil(PAST_MEDIAN_TIME_SAMPLE_INTERVAL); // // ~~~~~~~~~~~~~~~~~~~~~~~~~ Max difficulty target ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -71,8 +71,7 @@ pub mod consensus { pub const DIFFICULTY_WINDOW_SAMPLE_INTERVAL: u64 = 4; /// Size of the **sampled** difficulty window (independent of BPS) - pub const DIFFICULTY_SAMPLED_WINDOW_SIZE: u64 = - (NEW_DIFFICULTY_WINDOW_DURATION + DIFFICULTY_WINDOW_SAMPLE_INTERVAL - 1) / DIFFICULTY_WINDOW_SAMPLE_INTERVAL; + pub const DIFFICULTY_SAMPLED_WINDOW_SIZE: u64 = NEW_DIFFICULTY_WINDOW_DURATION.div_ceil(DIFFICULTY_WINDOW_SAMPLE_INTERVAL); // // ~~~~~~~~~~~~~~~~~~~ Finality & Pruning ~~~~~~~~~~~~~~~~~~~ diff --git a/consensus/core/src/network.rs b/consensus/core/src/network.rs index 18e52eacbf..2f81444b3c 100644 --- a/consensus/core/src/network.rs +++ b/consensus/core/src/network.rs @@ -344,7 +344,7 @@ impl Serialize for NetworkId { struct NetworkIdVisitor; -impl<'de> de::Visitor<'de> for NetworkIdVisitor { +impl de::Visitor<'_> for NetworkIdVisitor { type Value = NetworkId; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -413,7 +413,7 @@ impl TryFrom for NetworkId { impl TryCastFromJs for NetworkId { type Error = NetworkIdError; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 9f02ade4b6..769d29452c 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -321,7 +321,7 @@ impl<'a, T: VerifiableTransaction> Iterator for PopulatedInputIterator<'a, T> { } } -impl<'a, T: VerifiableTransaction> ExactSizeIterator for PopulatedInputIterator<'a, T> {} +impl ExactSizeIterator for PopulatedInputIterator<'_, T> {} /// Represents a read-only referenced transaction along with fully populated UTXO entry data pub struct PopulatedTransaction<'a> { @@ -336,7 +336,7 @@ impl<'a> PopulatedTransaction<'a> { } } -impl<'a> VerifiableTransaction for PopulatedTransaction<'a> { +impl VerifiableTransaction for PopulatedTransaction<'_> { fn tx(&self) -> &Transaction { self.tx } @@ -368,7 +368,7 @@ impl<'a> ValidatedTransaction<'a> { } } -impl<'a> VerifiableTransaction for ValidatedTransaction<'a> { +impl VerifiableTransaction for ValidatedTransaction<'_> { fn tx(&self) -> &Transaction { self.tx } diff --git a/consensus/core/src/tx/script_public_key.rs b/consensus/core/src/tx/script_public_key.rs index dfed2ab5ce..b0a4756066 100644 --- a/consensus/core/src/tx/script_public_key.rs +++ b/consensus/core/src/tx/script_public_key.rs @@ -94,7 +94,7 @@ impl Serialize for ScriptPublicKey { } } -impl<'de: 'a, 'a> Deserialize<'de> for ScriptPublicKey { +impl<'de> Deserialize<'de> for ScriptPublicKey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -374,7 +374,7 @@ impl BorshDeserialize for ScriptPublicKey { type CastError = workflow_wasm::error::Error; impl TryCastFromJs for ScriptPublicKey { type Error = workflow_wasm::error::Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/consensus/src/model/services/reachability.rs b/consensus/src/model/services/reachability.rs index 39f5ceba2d..1c6282f8e5 100644 --- a/consensus/src/model/services/reachability.rs +++ b/consensus/src/model/services/reachability.rs @@ -154,7 +154,6 @@ impl MTReachabilityService { /// a compromise where the lock is released every constant number of items. /// /// TODO: decide if these alternatives require overall system benchmarking - struct BackwardChainIterator { store: Arc>, current: Option, diff --git a/consensus/src/model/stores/ghostdag.rs b/consensus/src/model/stores/ghostdag.rs index 4ed02e4cec..f74fa125bd 100644 --- a/consensus/src/model/stores/ghostdag.rs +++ b/consensus/src/model/stores/ghostdag.rs @@ -116,7 +116,7 @@ impl GhostdagData { pub fn ascending_mergeset_without_selected_parent<'a>( &'a self, store: &'a (impl GhostdagStoreReader + ?Sized), - ) -> impl Iterator + '_ { + ) -> impl Iterator + 'a { self.mergeset_blues .iter() .skip(1) // Skip the selected parent @@ -139,7 +139,7 @@ impl GhostdagData { pub fn descending_mergeset_without_selected_parent<'a>( &'a self, store: &'a (impl GhostdagStoreReader + ?Sized), - ) -> impl Iterator + '_ { + ) -> impl Iterator + 'a { self.mergeset_blues .iter() .skip(1) // Skip the selected parent @@ -175,7 +175,7 @@ impl GhostdagData { pub fn consensus_ordered_mergeset<'a>( &'a self, store: &'a (impl GhostdagStoreReader + ?Sized), - ) -> impl Iterator + '_ { + ) -> impl Iterator + 'a { once(self.selected_parent).chain(self.ascending_mergeset_without_selected_parent(store).map(|s| s.hash)) } @@ -183,7 +183,7 @@ impl GhostdagData { pub fn consensus_ordered_mergeset_without_selected_parent<'a>( &'a self, store: &'a (impl GhostdagStoreReader + ?Sized), - ) -> impl Iterator + '_ { + ) -> impl Iterator + 'a { self.ascending_mergeset_without_selected_parent(store).map(|s| s.hash) } diff --git a/consensus/src/model/stores/relations.rs b/consensus/src/model/stores/relations.rs index 4734f099a3..2971a3a87d 100644 --- a/consensus/src/model/stores/relations.rs +++ b/consensus/src/model/stores/relations.rs @@ -145,7 +145,7 @@ pub struct StagingRelationsStore<'a> { children_deletions: BlockHashMap, } -impl<'a> ChildrenStore for StagingRelationsStore<'a> { +impl ChildrenStore for StagingRelationsStore<'_> { fn insert_child(&mut self, _writer: impl DbWriter, parent: Hash, child: Hash) -> Result<(), StoreError> { self.check_not_in_entry_deletions(parent)?; self.check_not_in_children_deletions(parent, child)?; // We expect deletion to be permanent diff --git a/consensus/src/model/stores/utxo_diffs.rs b/consensus/src/model/stores/utxo_diffs.rs index 079f08ecbc..20ddd9b107 100644 --- a/consensus/src/model/stores/utxo_diffs.rs +++ b/consensus/src/model/stores/utxo_diffs.rs @@ -14,7 +14,6 @@ use rocksdb::WriteBatch; /// blocks. However, once the diff is computed, it is permanent. This store has a relation to /// block status, such that if a block has status `StatusUTXOValid` then it is expected to have /// utxo diff data as well as utxo multiset data and acceptance data. - pub trait UtxoDiffsStoreReader { fn get(&self, hash: Hash) -> Result, StoreError>; } diff --git a/consensus/src/pipeline/body_processor/body_validation_in_context.rs b/consensus/src/pipeline/body_processor/body_validation_in_context.rs index 2b1bd99487..0eca78651c 100644 --- a/consensus/src/pipeline/body_processor/body_validation_in_context.rs +++ b/consensus/src/pipeline/body_processor/body_validation_in_context.rs @@ -7,7 +7,6 @@ use crate::{ use kaspa_consensus_core::block::Block; use kaspa_database::prelude::StoreResultExtensions; use kaspa_hashes::Hash; -use kaspa_utils::option::OptionExtensions; use std::sync::Arc; impl BlockBodyProcessor { @@ -45,7 +44,7 @@ impl BlockBodyProcessor { .copied() .filter(|parent| { let status_option = statuses_read_guard.get(*parent).unwrap_option(); - status_option.is_none_or_ex(|s| !s.has_block_body()) + status_option.is_none_or(|s| !s.has_block_body()) }) .collect(); if !missing.is_empty() { diff --git a/consensus/src/processes/coinbase.rs b/consensus/src/processes/coinbase.rs index f79bbed751..d67f922c81 100644 --- a/consensus/src/processes/coinbase.rs +++ b/consensus/src/processes/coinbase.rs @@ -72,7 +72,7 @@ impl CoinbaseManager { // Precomputed subsidy by month table for the actual block per second rate // Here values are rounded up so that we keep the same number of rewarding months as in the original 1 BPS table. // In a 10 BPS network, the induced increase in total rewards is 51 KAS (see tests::calc_high_bps_total_rewards_delta()) - let subsidy_by_month_table: SubsidyByMonthTable = core::array::from_fn(|i| (SUBSIDY_BY_MONTH_TABLE[i] + bps - 1) / bps); + let subsidy_by_month_table: SubsidyByMonthTable = core::array::from_fn(|i| SUBSIDY_BY_MONTH_TABLE[i].div_ceil(bps)); Self { coinbase_payload_script_public_key_max_len, max_coinbase_payload_len, @@ -288,10 +288,7 @@ mod tests { let total_rewards: u64 = pre_deflationary_rewards + SUBSIDY_BY_MONTH_TABLE.iter().map(|x| x * SECONDS_PER_MONTH).sum::(); let testnet_11_bps = TESTNET11_PARAMS.bps(); let total_high_bps_rewards_rounded_up: u64 = pre_deflationary_rewards - + SUBSIDY_BY_MONTH_TABLE - .iter() - .map(|x| ((x + testnet_11_bps - 1) / testnet_11_bps * testnet_11_bps) * SECONDS_PER_MONTH) - .sum::(); + + SUBSIDY_BY_MONTH_TABLE.iter().map(|x| (x.div_ceil(testnet_11_bps) * testnet_11_bps) * SECONDS_PER_MONTH).sum::(); let cbm = create_manager(&TESTNET11_PARAMS); let total_high_bps_rewards: u64 = @@ -316,7 +313,7 @@ mod tests { let cbm = create_manager(&network_id.into()); cbm.subsidy_by_month_table.iter().enumerate().for_each(|(i, x)| { assert_eq!( - (SUBSIDY_BY_MONTH_TABLE[i] + cbm.bps() - 1) / cbm.bps(), + SUBSIDY_BY_MONTH_TABLE[i].div_ceil(cbm.bps()), *x, "{}: locally computed and precomputed values must match", network_id @@ -376,7 +373,7 @@ mod tests { Test { name: "after 32 halvings", daa_score: params.deflationary_phase_daa_score + 32 * blocks_per_halving, - expected: ((DEFLATIONARY_PHASE_INITIAL_SUBSIDY / 2_u64.pow(32)) + cbm.bps() - 1) / cbm.bps(), + expected: (DEFLATIONARY_PHASE_INITIAL_SUBSIDY / 2_u64.pow(32)).div_ceil(cbm.bps()), }, Test { name: "just before subsidy depleted", diff --git a/consensus/src/processes/pruning.rs b/consensus/src/processes/pruning.rs index 7c534af8ed..5916df74d7 100644 --- a/consensus/src/processes/pruning.rs +++ b/consensus/src/processes/pruning.rs @@ -13,7 +13,6 @@ use crate::model::{ }, }; use kaspa_hashes::Hash; -use kaspa_utils::option::OptionExtensions; use parking_lot::RwLock; #[derive(Clone)] @@ -213,7 +212,7 @@ impl< let mut expected_pps_queue = VecDeque::new(); for current in self.reachability_service.backward_chain_iterator(hst, pruning_info.pruning_point, false) { let current_header = self.headers_store.get_header(current).unwrap(); - if expected_pps_queue.back().is_none_or_ex(|&&h| h != current_header.pruning_point) { + if expected_pps_queue.back().is_none_or(|&h| h != current_header.pruning_point) { expected_pps_queue.push_back(current_header.pruning_point); } } diff --git a/consensus/src/processes/sync/mod.rs b/consensus/src/processes/sync/mod.rs index 3978913bae..839e48a9ef 100644 --- a/consensus/src/processes/sync/mod.rs +++ b/consensus/src/processes/sync/mod.rs @@ -5,7 +5,6 @@ use kaspa_consensus_core::errors::sync::{SyncManagerError, SyncManagerResult}; use kaspa_database::prelude::StoreResultExtensions; use kaspa_hashes::Hash; use kaspa_math::uint::malachite_base::num::arithmetic::traits::CeilingLogBase2; -use kaspa_utils::option::OptionExtensions; use parking_lot::RwLock; use crate::model::{ @@ -191,7 +190,7 @@ impl< } } - if highest_with_body.is_none_or_ex(|&h| h == high) { + if highest_with_body.is_none_or(|h| h == high) { return Ok(vec![]); }; diff --git a/consensus/src/test_helpers.rs b/consensus/src/test_helpers.rs index c119c6d6d2..b3867f145e 100644 --- a/consensus/src/test_helpers.rs +++ b/consensus/src/test_helpers.rs @@ -19,7 +19,7 @@ pub fn block_from_precomputed_hash(hash: Hash, parents: Vec) -> Block { pub fn generate_random_utxos_from_script_public_key_pool( rng: &mut SmallRng, amount: usize, - script_public_key_pool: &Vec, + script_public_key_pool: &[ScriptPublicKey], ) -> UtxoCollection { let mut i = 0; let mut collection = UtxoCollection::with_capacity(amount); @@ -40,10 +40,7 @@ pub fn generate_random_outpoint(rng: &mut SmallRng) -> TransactionOutpoint { TransactionOutpoint::new(generate_random_hash(rng), rng.gen::()) } -pub fn generate_random_utxo_from_script_public_key_pool( - rng: &mut SmallRng, - script_public_key_pool: &Vec, -) -> UtxoEntry { +pub fn generate_random_utxo_from_script_public_key_pool(rng: &mut SmallRng, script_public_key_pool: &[ScriptPublicKey]) -> UtxoEntry { UtxoEntry::new( rng.gen_range(1..100_000), //we choose small amounts as to not overflow with large utxosets. script_public_key_pool.choose(rng).expect("expected_script_public key").clone(), diff --git a/core/src/task/runtime.rs b/core/src/task/runtime.rs index 13deaae6b8..1bc3e6952e 100644 --- a/core/src/task/runtime.rs +++ b/core/src/task/runtime.rs @@ -50,14 +50,13 @@ impl AsyncRuntime { } /// Launch a tokio Runtime and run the top-level async objects - pub fn worker(self: &Arc, core: Arc) { - return tokio::runtime::Builder::new_multi_thread() + tokio::runtime::Builder::new_multi_thread() .worker_threads(self.threads) .enable_all() .build() .expect("Failed building the Runtime") - .block_on(async { self.worker_impl(core).await }); + .block_on(async { self.worker_impl(core).await }) } pub async fn worker_impl(self: &Arc, core: Arc) { diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index 8e3ea385a8..48c0baf198 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -506,7 +506,7 @@ impl<'de> Deserialize<'de> for Address { impl TryCastFromJs for Address { type Error = AddressError; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/crypto/hashes/src/lib.rs b/crypto/hashes/src/lib.rs index d9ff47997c..da9019af29 100644 --- a/crypto/hashes/src/lib.rs +++ b/crypto/hashes/src/lib.rs @@ -187,7 +187,7 @@ impl Hash { type TryFromError = workflow_wasm::error::Error; impl TryCastFromJs for Hash { type Error = TryFromError; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/crypto/muhash/src/lib.rs b/crypto/muhash/src/lib.rs index 3fa7fc6e69..2ad0594663 100644 --- a/crypto/muhash/src/lib.rs +++ b/crypto/muhash/src/lib.rs @@ -146,7 +146,7 @@ pub struct MuHashElementBuilder<'a> { element_hasher: MuHashElementHash, } -impl<'a> HasherBase for MuHashElementBuilder<'a> { +impl HasherBase for MuHashElementBuilder<'_> { fn update>(&mut self, data: A) -> &mut Self { self.element_hasher.write(data); self diff --git a/crypto/txscript/src/lib.rs b/crypto/txscript/src/lib.rs index a82be592f6..637a10aff2 100644 --- a/crypto/txscript/src/lib.rs +++ b/crypto/txscript/src/lib.rs @@ -230,7 +230,7 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' #[inline] pub fn is_executing(&self) -> bool { - return self.cond_stack.is_empty() || *self.cond_stack.last().expect("Checked not empty") == OpCond::True; + self.cond_stack.is_empty() || *self.cond_stack.last().expect("Checked not empty") == OpCond::True } fn execute_opcode(&mut self, opcode: DynOpcodeImplementation) -> Result<(), TxScriptError> { diff --git a/database/src/access.rs b/database/src/access.rs index ad82197dbb..fad8ee1300 100644 --- a/database/src/access.rs +++ b/database/src/access.rs @@ -22,6 +22,8 @@ where prefix: Vec, } +pub type KeyDataResult = Result<(Box<[u8]>, TData), Box>; + impl CachedDbAccess where TKey: Clone + std::hash::Hash + Eq + Send + Sync, @@ -65,7 +67,7 @@ where } } - pub fn iterator(&self) -> impl Iterator, TData), Box>> + '_ + pub fn iterator(&self) -> impl Iterator> + '_ where TKey: Clone + AsRef<[u8]>, TData: DeserializeOwned, // We need `DeserializeOwned` since the slice coming from `db.get_pinned` has short lifetime @@ -173,7 +175,7 @@ where seek_from: Option, // iter whole range if None limit: usize, // amount to take. skip_first: bool, // skips the first value, (useful in conjunction with the seek-key, as to not re-retrieve). - ) -> impl Iterator, TData), Box>> + '_ + ) -> impl Iterator> + '_ where TKey: Clone + AsRef<[u8]>, TData: DeserializeOwned, diff --git a/mining/src/mempool/model/frontier/search_tree.rs b/mining/src/mempool/model/frontier/search_tree.rs index edf34c2710..136269a794 100644 --- a/mining/src/mempool/model/frontier/search_tree.rs +++ b/mining/src/mempool/model/frontier/search_tree.rs @@ -111,7 +111,7 @@ impl<'a> PrefixWeightVisitor<'a> { } } -impl<'a> DescendVisit for PrefixWeightVisitor<'a> { +impl DescendVisit for PrefixWeightVisitor<'_> { type Result = f64; fn visit_inner(&mut self, keys: &[FeerateKey], arguments: &[FeerateWeight]) -> DescendVisitResult { diff --git a/mining/src/model/topological_sort.rs b/mining/src/model/topological_sort.rs index aa88cce023..fb276ac004 100644 --- a/mining/src/model/topological_sort.rs +++ b/mining/src/model/topological_sort.rs @@ -166,8 +166,8 @@ impl<'a, T: AsRef> Iterator for TopologicalIter<'a, T> { } } -impl<'a, T: AsRef> FusedIterator for TopologicalIter<'a, T> {} -impl<'a, T: AsRef> ExactSizeIterator for TopologicalIter<'a, T> { +impl> FusedIterator for TopologicalIter<'_, T> {} +impl> ExactSizeIterator for TopologicalIter<'_, T> { fn len(&self) -> usize { self.transactions.len() } diff --git a/protocol/flows/src/flowcontext/orphans.rs b/protocol/flows/src/flowcontext/orphans.rs index f18649e558..41bf940a17 100644 --- a/protocol/flows/src/flowcontext/orphans.rs +++ b/protocol/flows/src/flowcontext/orphans.rs @@ -6,7 +6,6 @@ use kaspa_consensus_core::{ use kaspa_consensusmanager::{BlockProcessingBatch, ConsensusProxy}; use kaspa_core::debug; use kaspa_hashes::Hash; -use kaspa_utils::option::OptionExtensions; use rand::Rng; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -166,7 +165,7 @@ impl OrphanBlocksPool { } } else { let status = consensus.async_get_block_status(current).await; - if status.is_none_or_ex(|s| s.is_header_only()) { + if status.is_none_or(|s| s.is_header_only()) { // Block is not in the orphan pool nor does its body exist consensus-wise, so it is a root roots.push(current); } @@ -193,8 +192,7 @@ impl OrphanBlocksPool { if let Occupied(entry) = self.orphans.entry(orphan_hash) { let mut processable = true; for p in entry.get().block.header.direct_parents().iter().copied() { - if !processing.contains_key(&p) && consensus.async_get_block_status(p).await.is_none_or_ex(|s| s.is_header_only()) - { + if !processing.contains_key(&p) && consensus.async_get_block_status(p).await.is_none_or(|s| s.is_header_only()) { processable = false; break; } @@ -250,7 +248,7 @@ impl OrphanBlocksPool { let mut processable = true; for parent in block.block.header.direct_parents().iter().copied() { if self.orphans.contains_key(&parent) - || consensus.async_get_block_status(parent).await.is_none_or_ex(|status| status.is_header_only()) + || consensus.async_get_block_status(parent).await.is_none_or(|status| status.is_header_only()) { processable = false; break; diff --git a/protocol/flows/src/flowcontext/transactions.rs b/protocol/flows/src/flowcontext/transactions.rs index 110b378b70..5fe1bb5939 100644 --- a/protocol/flows/src/flowcontext/transactions.rs +++ b/protocol/flows/src/flowcontext/transactions.rs @@ -47,8 +47,7 @@ impl TransactionsSpread { // Keep the launching times aligned to exact intervals. Note that `delta=10.1` seconds will result in // adding 10 seconds to last scan time, while `delta=11` will result in adding 20 (assuming scanning // interval is 10 seconds). - self.last_scanning_time += - Duration::from_secs(((delta.as_secs() + SCANNING_TASK_INTERVAL - 1) / SCANNING_TASK_INTERVAL) * SCANNING_TASK_INTERVAL); + self.last_scanning_time += Duration::from_secs(delta.as_secs().div_ceil(SCANNING_TASK_INTERVAL) * SCANNING_TASK_INTERVAL); self.scanning_job_count += 1; self.scanning_task_running = true; diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index 35d08493bb..9baeaa04e7 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -254,7 +254,7 @@ async fn main() { (stats.utxos_amount / stats.num_utxos as u64), stats.num_utxos / stats.num_txs, stats.num_outs / stats.num_txs, - if utxos_len > pending_len { utxos_len - pending_len } else { 0 }, + utxos_len.saturating_sub(pending_len), ); stats.since = now; stats.num_txs = 0; diff --git a/rpc/core/src/model/header.rs b/rpc/core/src/model/header.rs index dddf767b7f..fda6b70e1e 100644 --- a/rpc/core/src/model/header.rs +++ b/rpc/core/src/model/header.rs @@ -8,7 +8,6 @@ use workflow_serializer::prelude::*; /// Used for mining APIs (get_block_template & submit_block) #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] - pub struct RpcRawHeader { pub version: u16, pub parents_by_level: Vec>, diff --git a/rpc/wrpc/wasm/src/resolver.rs b/rpc/wrpc/wasm/src/resolver.rs index 7abfdb6884..1753534372 100644 --- a/rpc/wrpc/wasm/src/resolver.rs +++ b/rpc/wrpc/wasm/src/resolver.rs @@ -198,7 +198,7 @@ impl TryFrom for NativeResolver { impl TryCastFromJs for Resolver { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result> + fn try_cast_from<'a, R>(value: &'a R) -> Result> where R: AsRef + 'a, { diff --git a/utils/src/as_slice.rs b/utils/src/as_slice.rs index fd73c39039..7fc0459c6a 100644 --- a/utils/src/as_slice.rs +++ b/utils/src/as_slice.rs @@ -16,7 +16,7 @@ pub trait AsMutSlice: AsSlice { fn as_mut_slice(&mut self) -> &mut [Self::Element]; } -impl<'a, S> AsSlice for &'a S +impl AsSlice for &S where S: ?Sized + AsSlice, { @@ -27,7 +27,7 @@ where } } -impl<'a, S> AsSlice for &'a mut S +impl AsSlice for &mut S where S: ?Sized + AsSlice, { @@ -38,7 +38,7 @@ where } } -impl<'a, S> AsMutSlice for &'a mut S +impl AsMutSlice for &mut S where S: ?Sized + AsMutSlice, { diff --git a/utils/src/iter.rs b/utils/src/iter.rs index 3c4c98c64a..3d38eff12a 100644 --- a/utils/src/iter.rs +++ b/utils/src/iter.rs @@ -25,7 +25,7 @@ impl<'a, I> ReusableIterFormat<'a, I> { } } -impl<'a, I> std::fmt::Display for ReusableIterFormat<'a, I> +impl std::fmt::Display for ReusableIterFormat<'_, I> where I: std::clone::Clone, I: Iterator, @@ -37,7 +37,7 @@ where } } -impl<'a, I> std::fmt::Debug for ReusableIterFormat<'a, I> +impl std::fmt::Debug for ReusableIterFormat<'_, I> where I: std::clone::Clone, I: Iterator, diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 3d1bb54384..c6cc077c4a 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -14,7 +14,6 @@ pub mod hex; pub mod iter; pub mod mem_size; pub mod networking; -pub mod option; pub mod refs; pub mod as_slice; diff --git a/utils/src/option.rs b/utils/src/option.rs deleted file mode 100644 index 3e619f46fa..0000000000 --- a/utils/src/option.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub trait OptionExtensions { - /// Substitute for unstable [`Option::is_none_or`] - fn is_none_or_ex(&self, f: impl FnOnce(&T) -> bool) -> bool; -} - -impl OptionExtensions for Option { - fn is_none_or_ex(&self, f: impl FnOnce(&T) -> bool) -> bool { - match self { - Some(v) => f(v), - None => true, - } - } -} diff --git a/utils/src/serde_bytes/de.rs b/utils/src/serde_bytes/de.rs index 66064bfe33..6a634db0c7 100644 --- a/utils/src/serde_bytes/de.rs +++ b/utils/src/serde_bytes/de.rs @@ -29,7 +29,7 @@ pub struct FromHexVisitor<'de, T: FromHex> { lifetime: std::marker::PhantomData<&'de ()>, } -impl<'de, T: FromHex> Default for FromHexVisitor<'de, T> { +impl Default for FromHexVisitor<'_, T> { fn default() -> Self { Self { marker: Default::default(), lifetime: Default::default() } } diff --git a/wallet/bip32/src/mnemonic/bits.rs b/wallet/bip32/src/mnemonic/bits.rs index 5ed09af008..08ff65ef6d 100644 --- a/wallet/bip32/src/mnemonic/bits.rs +++ b/wallet/bip32/src/mnemonic/bits.rs @@ -56,7 +56,7 @@ impl Bits for u8 { } } -impl<'a> Bits for &'a u8 { +impl Bits for &'_ u8 { const SIZE: usize = 8; fn bits(self) -> u32 { diff --git a/wallet/bip32/src/xpublic_key.rs b/wallet/bip32/src/xpublic_key.rs index ac4eb720c9..ff9678cac1 100644 --- a/wallet/bip32/src/xpublic_key.rs +++ b/wallet/bip32/src/xpublic_key.rs @@ -10,7 +10,7 @@ use hmac::Mac; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; -/// Extended public secp256k1 ECDSA verification key. +///// Extended public secp256k1 ECDSA verification key. //#[cfg(feature = "secp256k1")] //#[cfg_attr(docsrs, doc(cfg(feature = "secp256k1")))] //pub type XPub = ExtendedPublicKey; diff --git a/wallet/core/src/storage/local/mod.rs b/wallet/core/src/storage/local/mod.rs index 07e612a512..0c220b64ff 100644 --- a/wallet/core/src/storage/local/mod.rs +++ b/wallet/core/src/storage/local/mod.rs @@ -37,21 +37,30 @@ pub fn default_storage_folder() -> &'static str { // SAFETY: This operation is initializing a static mut variable, // however, the actual variable is accessible only through // this function. - unsafe { DEFAULT_STORAGE_FOLDER.get_or_insert("~/.kaspa".to_string()).as_str() } + #[allow(static_mut_refs)] + unsafe { + DEFAULT_STORAGE_FOLDER.get_or_insert("~/.kaspa".to_string()).as_str() + } } pub fn default_wallet_file() -> &'static str { // SAFETY: This operation is initializing a static mut variable, // however, the actual variable is accessible only through // this function. - unsafe { DEFAULT_WALLET_FILE.get_or_insert("kaspa".to_string()).as_str() } + #[allow(static_mut_refs)] + unsafe { + DEFAULT_WALLET_FILE.get_or_insert("kaspa".to_string()).as_str() + } } pub fn default_settings_file() -> &'static str { // SAFETY: This operation is initializing a static mut variable, // however, the actual variable is accessible only through // this function. - unsafe { DEFAULT_SETTINGS_FILE.get_or_insert("kaspa".to_string()).as_str() } + #[allow(static_mut_refs)] + unsafe { + DEFAULT_SETTINGS_FILE.get_or_insert("kaspa".to_string()).as_str() + } } /// Set a custom storage folder for the wallet SDK diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 398ba1b4dc..d736bc73e1 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -592,7 +592,6 @@ impl Generator { } */ - fn generate_transaction_data(&self, context: &mut Context, stage: &mut Stage) -> Result<(DataKind, Data)> { let calc = &self.inner.mass_calculator; let mut data = Data::new(calc); diff --git a/wallet/core/src/tx/payment.rs b/wallet/core/src/tx/payment.rs index c164e0d789..350b8c98f4 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -72,7 +72,7 @@ pub struct PaymentOutput { impl TryCastFromJs for PaymentOutput { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { @@ -158,7 +158,7 @@ impl PaymentOutputs { impl TryCastFromJs for PaymentOutputs { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index d7c9b6c76e..e9316a13b1 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -57,7 +57,7 @@ pub struct SingleWalletFileV1<'a, T: AsRef<[u8]>> { pub ecdsa: bool, } -impl<'a, T: AsRef<[u8]>> SingleWalletFileV1<'a, T> { +impl> SingleWalletFileV1<'_, T> { const NUM_THREADS: u32 = 8; } @@ -80,7 +80,7 @@ pub struct MultisigWalletFileV1<'a, T: AsRef<[u8]>> { pub ecdsa: bool, } -impl<'a, T: AsRef<[u8]>> MultisigWalletFileV1<'a, T> { +impl> MultisigWalletFileV1<'_, T> { const NUM_THREADS: u32 = 8; } diff --git a/wallet/core/src/wasm/cryptobox.rs b/wallet/core/src/wasm/cryptobox.rs index 957d4fc35a..76e9fdc3c0 100644 --- a/wallet/core/src/wasm/cryptobox.rs +++ b/wallet/core/src/wasm/cryptobox.rs @@ -35,7 +35,7 @@ impl CryptoBoxPrivateKey { impl TryCastFromJs for CryptoBoxPrivateKey { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result> + fn try_cast_from<'a, R>(value: &'a R) -> Result> where R: AsRef + 'a, { @@ -66,7 +66,7 @@ pub struct CryptoBoxPublicKey { impl TryCastFromJs for CryptoBoxPublicKey { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result> + fn try_cast_from<'a, R>(value: &'a R) -> Result> where R: AsRef + 'a, { diff --git a/wallet/core/src/wasm/utxo/context.rs b/wallet/core/src/wasm/utxo/context.rs index 3298a4829e..9c78fe6892 100644 --- a/wallet/core/src/wasm/utxo/context.rs +++ b/wallet/core/src/wasm/utxo/context.rs @@ -252,7 +252,7 @@ impl From for native::UtxoContext { impl TryCastFromJs for UtxoContext { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/core/src/wasm/utxo/processor.rs b/wallet/core/src/wasm/utxo/processor.rs index d68f10f763..ac089d4852 100644 --- a/wallet/core/src/wasm/utxo/processor.rs +++ b/wallet/core/src/wasm/utxo/processor.rs @@ -197,7 +197,7 @@ impl UtxoProcessor { impl TryCastFromJs for UtxoProcessor { type Error = workflow_wasm::error::Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/derivation_path.rs b/wallet/keys/src/derivation_path.rs index a5389ca37e..7867f886f5 100644 --- a/wallet/keys/src/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -57,7 +57,7 @@ impl DerivationPath { impl TryCastFromJs for DerivationPath { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/keypair.rs b/wallet/keys/src/keypair.rs index 2cc3d57607..7c58d18bd3 100644 --- a/wallet/keys/src/keypair.rs +++ b/wallet/keys/src/keypair.rs @@ -104,7 +104,7 @@ impl Keypair { impl TryCastFromJs for Keypair { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs index 554bdf36e3..75911a3ff3 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -95,7 +95,7 @@ impl PrivateKey { impl TryCastFromJs for PrivateKey { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index 235eb80804..f3c951ae20 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -155,7 +155,7 @@ extern "C" { impl TryCastFromJs for PublicKey { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/xprv.rs b/wallet/keys/src/xprv.rs index c19e0b9cc8..a0ea428a06 100644 --- a/wallet/keys/src/xprv.rs +++ b/wallet/keys/src/xprv.rs @@ -146,7 +146,7 @@ extern "C" { impl TryCastFromJs for XPrv { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/keys/src/xpub.rs b/wallet/keys/src/xpub.rs index 8706f3fc91..64fc5f78ee 100644 --- a/wallet/keys/src/xpub.rs +++ b/wallet/keys/src/xpub.rs @@ -116,7 +116,7 @@ extern "C" { impl TryCastFromJs for XPub { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> Result, Self::Error> where R: AsRef + 'a, { diff --git a/wallet/pskt/src/bundle.rs b/wallet/pskt/src/bundle.rs index 6c926c6665..e08474d7a4 100644 --- a/wallet/pskt/src/bundle.rs +++ b/wallet/pskt/src/bundle.rs @@ -247,23 +247,18 @@ mod tests { use secp256k1::Secp256k1; use secp256k1::{rand::thread_rng, Keypair}; use std::str::FromStr; - use std::sync::Once; + use std::sync::LazyLock; - static INIT: Once = Once::new(); - static mut CONTEXT: Option)>> = None; + static CONTEXT: LazyLock)>> = LazyLock::new(|| { + let kps = [Keypair::new(&Secp256k1::new(), &mut thread_rng()), Keypair::new(&Secp256k1::new(), &mut thread_rng())]; + let redeem_script: Vec = + multisig_redeem_script(kps.iter().map(|pk| pk.x_only_public_key().0.serialize()), 2).expect("Test multisig redeem script"); - fn mock_context() -> &'static ([Keypair; 2], Vec) { - unsafe { - INIT.call_once(|| { - let kps = [Keypair::new(&Secp256k1::new(), &mut thread_rng()), Keypair::new(&Secp256k1::new(), &mut thread_rng())]; - let redeem_script: Vec = multisig_redeem_script(kps.iter().map(|pk| pk.x_only_public_key().0.serialize()), 2) - .expect("Test multisig redeem script"); - - CONTEXT = Some(Box::new((kps, redeem_script))); - }); + Box::new((kps, redeem_script)) + }); - CONTEXT.as_ref().unwrap() - } + fn mock_context() -> &'static ([Keypair; 2], Vec) { + CONTEXT.as_ref() } // Mock multisig PSKT from example diff --git a/wallet/pskt/src/wasm/pskt.rs b/wallet/pskt/src/wasm/pskt.rs index 8ee370a4b9..53c8a1cc3c 100644 --- a/wallet/pskt/src/wasm/pskt.rs +++ b/wallet/pskt/src/wasm/pskt.rs @@ -90,7 +90,7 @@ pub struct PSKT { impl TryCastFromJs for PSKT { type Error = Error; - fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> + fn try_cast_from<'a, R>(value: &'a R) -> std::result::Result, Self::Error> where R: AsRef + 'a, { From c63dfc003a3c7d651b9ca022d786a9104c3a6edc Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 28 Nov 2024 22:58:10 +0200 Subject: [PATCH 4/5] IBD sync: recover sampled window (#598) * Search for the full consecutive window covering all sampled blocks * unrelated: reachability docs * Avoid searching for the cover if window is not sampled * cleanup WindowType::FullDifficultyWindow * rename * Fix cache origin issue and simplify cache management * prevent access to block window cache get w/o specifying origin * Suggested refactor for determining lock time type prior the call (to avoid leaking logic out of the TransactionValidator) * long due renames * renames and comments * move window "cover" logic into WindowManager * unrelated technical debt: make sure to run par_iter within the context of an existing thread pool (avoid creating a global thread pool if possible) --- consensus/benches/check_scripts.rs | 2 +- consensus/src/consensus/mod.rs | 9 +- consensus/src/model/services/reachability.rs | 21 ++ .../src/model/stores/block_window_cache.rs | 44 ++- .../body_validation_in_context.rs | 30 +- .../src/pipeline/body_processor/processor.rs | 4 - .../pipeline/header_processor/processor.rs | 2 +- .../pipeline/virtual_processor/processor.rs | 25 +- .../virtual_processor/utxo_validation.rs | 2 +- consensus/src/processes/pruning_proof/mod.rs | 10 +- .../src/processes/reachability/inquirer.rs | 12 +- .../processes/transaction_validator/mod.rs | 6 +- .../tx_validation_in_header_context.rs | 102 +++++++ .../tx_validation_in_isolation.rs | 5 + ...ed.rs => tx_validation_in_utxo_context.rs} | 0 .../tx_validation_not_utxo_related.rs | 53 ---- consensus/src/processes/window.rs | 270 +++++++++++++----- .../src/consensus_integration_tests.rs | 2 +- 18 files changed, 419 insertions(+), 180 deletions(-) create mode 100644 consensus/src/processes/transaction_validator/tx_validation_in_header_context.rs rename consensus/src/processes/transaction_validator/{transaction_validator_populated.rs => tx_validation_in_utxo_context.rs} (100%) delete mode 100644 consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs diff --git a/consensus/benches/check_scripts.rs b/consensus/benches/check_scripts.rs index 5d13c43d8e..6462e04e49 100644 --- a/consensus/benches/check_scripts.rs +++ b/consensus/benches/check_scripts.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion, SamplingMode}; use kaspa_addresses::{Address, Prefix, Version}; -use kaspa_consensus::processes::transaction_validator::transaction_validator_populated::{ +use kaspa_consensus::processes::transaction_validator::tx_validation_in_utxo_context::{ check_scripts_par_iter, check_scripts_par_iter_pool, check_scripts_sequential, }; use kaspa_consensus_core::hashing::sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync}; diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index eca78ee2a4..99719d4ac2 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -767,12 +767,13 @@ impl ConsensusApi for Consensus { let mut pruning_utxoset_write = self.pruning_utxoset_stores.write(); pruning_utxoset_write.utxo_set.write_many(utxoset_chunk).unwrap(); - // Parallelize processing - let inner_multiset = + // Parallelize processing using the context of an existing thread pool. + let inner_multiset = self.virtual_processor.install(|| { utxoset_chunk.par_iter().map(|(outpoint, entry)| MuHash::from_utxo(outpoint, entry)).reduce(MuHash::new, |mut a, b| { a.combine(&b); a - }); + }) + }); current_multiset.combine(&inner_multiset); } @@ -979,7 +980,7 @@ impl ConsensusApi for Consensus { Ok(self .services .window_manager - .block_window(&self.ghostdag_store.get_data(hash).unwrap(), WindowType::SampledDifficultyWindow) + .block_window(&self.ghostdag_store.get_data(hash).unwrap(), WindowType::DifficultyWindow) .unwrap() .deref() .iter() diff --git a/consensus/src/model/services/reachability.rs b/consensus/src/model/services/reachability.rs index 1c6282f8e5..a3aa83c7a4 100644 --- a/consensus/src/model/services/reachability.rs +++ b/consensus/src/model/services/reachability.rs @@ -9,14 +9,35 @@ use crate::processes::reachability::{inquirer, Result}; use kaspa_hashes::Hash; pub trait ReachabilityService { + /// Checks if `this` block is a chain ancestor of `queried` block (i.e., `this ∈ chain(queried) ∪ {queried}`). + /// Note that we use the graph theory convention here which defines that a block is also an ancestor of itself. fn is_chain_ancestor_of(&self, this: Hash, queried: Hash) -> bool; + + /// Result version of [`is_dag_ancestor_of`] (avoids unwrapping internally) fn is_dag_ancestor_of_result(&self, this: Hash, queried: Hash) -> Result; + + /// Returns true if `this` is a DAG ancestor of `queried` (i.e., `queried ∈ future(this) ∪ {this}`). + /// Note: this method will return true if `this == queried`. + /// The complexity of this method is `O(log(|future_covering_set(this)|))` fn is_dag_ancestor_of(&self, this: Hash, queried: Hash) -> bool; + + /// Checks if `this` is DAG ancestor of any of the blocks in `queried`. See [`is_dag_ancestor_of`] as well. fn is_dag_ancestor_of_any(&self, this: Hash, queried: &mut impl Iterator) -> bool; + + /// Checks if any of the blocks in `list` is DAG ancestor of `queried`. See [`is_dag_ancestor_of`] as well. fn is_any_dag_ancestor(&self, list: &mut impl Iterator, queried: Hash) -> bool; + + /// Result version of [`is_any_dag_ancestor`] (avoids unwrapping internally) fn is_any_dag_ancestor_result(&self, list: &mut impl Iterator, queried: Hash) -> Result; + + /// Finds the tree child of `ancestor` which is also a chain ancestor of `descendant`. + /// (A "tree child of X" is a block which X is its chain parent) fn get_next_chain_ancestor(&self, descendant: Hash, ancestor: Hash) -> Hash; + + /// Returns the chain parent of `this` fn get_chain_parent(&self, this: Hash) -> Hash; + + /// Checks whether `this` has reachability data fn has_reachability_data(&self, this: Hash) -> bool; } diff --git a/consensus/src/model/stores/block_window_cache.rs b/consensus/src/model/stores/block_window_cache.rs index 5fee0e1f84..2088cd2d18 100644 --- a/consensus/src/model/stores/block_window_cache.rs +++ b/consensus/src/model/stores/block_window_cache.rs @@ -1,6 +1,6 @@ use crate::processes::ghostdag::ordering::SortableBlock; use kaspa_consensus_core::BlockHasher; -use kaspa_database::prelude::Cache; +use kaspa_database::prelude::{Cache, CachePolicy}; use kaspa_hashes::Hash; use kaspa_utils::mem_size::MemSizeEstimator; use std::{ @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WindowOrigin { Full, Sampled, @@ -54,16 +54,46 @@ impl DerefMut for BlockWindowHeap { } } +/// A newtype wrapper over `[Cache]` meant to prevent erroneous reads of windows from different origins +#[derive(Clone)] +pub struct BlockWindowCacheStore { + inner: Cache, BlockHasher>, +} + +impl BlockWindowCacheStore { + pub fn new(policy: CachePolicy) -> Self { + Self { inner: Cache::new(policy) } + } + + pub fn contains_key(&self, key: &Hash) -> bool { + self.inner.contains_key(key) + } + + pub fn remove(&self, key: &Hash) -> Option> { + self.inner.remove(key) + } +} + /// Reader API for `BlockWindowCacheStore`. pub trait BlockWindowCacheReader { - fn get(&self, hash: &Hash) -> Option>; + /// Get the cache entry to this hash conditioned that *it matches the provided origin*. + /// We demand the origin to be provided in order to prevent reader errors. + fn get(&self, hash: &Hash, origin: WindowOrigin) -> Option>; } -pub type BlockWindowCacheStore = Cache, BlockHasher>; - impl BlockWindowCacheReader for BlockWindowCacheStore { #[inline(always)] - fn get(&self, hash: &Hash) -> Option> { - self.get(hash) + fn get(&self, hash: &Hash, origin: WindowOrigin) -> Option> { + self.inner.get(hash).and_then(|win| if win.origin() == origin { Some(win) } else { None }) + } +} + +pub trait BlockWindowCacheWriter { + fn insert(&self, hash: Hash, window: Arc); +} + +impl BlockWindowCacheWriter for BlockWindowCacheStore { + fn insert(&self, hash: Hash, window: Arc) { + self.inner.insert(hash, window); } } diff --git a/consensus/src/pipeline/body_processor/body_validation_in_context.rs b/consensus/src/pipeline/body_processor/body_validation_in_context.rs index 0eca78651c..08eb49f63b 100644 --- a/consensus/src/pipeline/body_processor/body_validation_in_context.rs +++ b/consensus/src/pipeline/body_processor/body_validation_in_context.rs @@ -1,12 +1,19 @@ use super::BlockBodyProcessor; use crate::{ errors::{BlockProcessResult, RuleError}, - model::stores::{ghostdag::GhostdagStoreReader, statuses::StatusesStoreReader}, - processes::window::WindowManager, + model::stores::statuses::StatusesStoreReader, + processes::{ + transaction_validator::{ + tx_validation_in_header_context::{LockTimeArg, LockTimeType}, + TransactionValidator, + }, + window::WindowManager, + }, }; use kaspa_consensus_core::block::Block; use kaspa_database::prelude::StoreResultExtensions; use kaspa_hashes::Hash; +use once_cell::unsync::Lazy; use std::sync::Arc; impl BlockBodyProcessor { @@ -17,18 +24,17 @@ impl BlockBodyProcessor { } fn check_block_transactions_in_context(self: &Arc, block: &Block) -> BlockProcessResult<()> { - // Note: This is somewhat expensive during ibd, as it incurs cache misses. - - let pmt = { - let (pmt, pmt_window) = self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap())?; - if !self.block_window_cache_for_past_median_time.contains_key(&block.hash()) { - self.block_window_cache_for_past_median_time.insert(block.hash(), pmt_window); - }; - pmt - }; + // Use lazy evaluation to avoid unnecessary work, as most of the time we expect the txs not to have lock time. + let lazy_pmt_res = Lazy::new(|| self.window_manager.calc_past_median_time_for_known_hash(block.hash())); for tx in block.transactions.iter() { - if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, pmt) { + let lock_time_arg = match TransactionValidator::get_lock_time_type(tx) { + LockTimeType::Finalized => LockTimeArg::Finalized, + LockTimeType::DaaScore => LockTimeArg::DaaScore(block.header.daa_score), + // We only evaluate the pmt calculation when actually needed + LockTimeType::Time => LockTimeArg::MedianTime((*lazy_pmt_res).clone()?), + }; + if let Err(e) = self.transaction_validator.validate_tx_in_header_context(tx, block.header.daa_score, lock_time_arg) { return Err(RuleError::TxInContextFailed(tx.id(), e)); }; } diff --git a/consensus/src/pipeline/body_processor/processor.rs b/consensus/src/pipeline/body_processor/processor.rs index ebb11a2003..7bad12ce3f 100644 --- a/consensus/src/pipeline/body_processor/processor.rs +++ b/consensus/src/pipeline/body_processor/processor.rs @@ -8,7 +8,6 @@ use crate::{ services::reachability::MTReachabilityService, stores::{ block_transactions::DbBlockTransactionsStore, - block_window_cache::BlockWindowCacheStore, ghostdag::DbGhostdagStore, headers::DbHeadersStore, reachability::DbReachabilityStore, @@ -67,7 +66,6 @@ pub struct BlockBodyProcessor { pub(super) headers_store: Arc, pub(super) block_transactions_store: Arc, pub(super) body_tips_store: Arc>, - pub(super) block_window_cache_for_past_median_time: Arc, // Managers and services pub(super) reachability_service: MTReachabilityService, @@ -93,7 +91,6 @@ pub struct BlockBodyProcessor { } impl BlockBodyProcessor { - #[allow(clippy::too_many_arguments)] pub fn new( receiver: Receiver, sender: Sender, @@ -122,7 +119,6 @@ impl BlockBodyProcessor { headers_store: storage.headers_store.clone(), block_transactions_store: storage.block_transactions_store.clone(), body_tips_store: storage.body_tips_store.clone(), - block_window_cache_for_past_median_time: storage.block_window_cache_for_past_median_time.clone(), reachability_service: services.reachability_service.clone(), coinbase_manager: services.coinbase_manager.clone(), diff --git a/consensus/src/pipeline/header_processor/processor.rs b/consensus/src/pipeline/header_processor/processor.rs index 4ecc761af1..f467b6d975 100644 --- a/consensus/src/pipeline/header_processor/processor.rs +++ b/consensus/src/pipeline/header_processor/processor.rs @@ -10,7 +10,7 @@ use crate::{ model::{ services::reachability::MTReachabilityService, stores::{ - block_window_cache::{BlockWindowCacheStore, BlockWindowHeap}, + block_window_cache::{BlockWindowCacheStore, BlockWindowCacheWriter, BlockWindowHeap}, daa::DbDaaStore, depth::DbDepthStore, ghostdag::{DbGhostdagStore, GhostdagData, GhostdagStoreReader}, diff --git a/consensus/src/pipeline/virtual_processor/processor.rs b/consensus/src/pipeline/virtual_processor/processor.rs index 1f0c4ff38b..a8e1f7f2f4 100644 --- a/consensus/src/pipeline/virtual_processor/processor.rs +++ b/consensus/src/pipeline/virtual_processor/processor.rs @@ -16,7 +16,7 @@ use crate::{ stores::{ acceptance_data::{AcceptanceDataStoreReader, DbAcceptanceDataStore}, block_transactions::{BlockTransactionsStoreReader, DbBlockTransactionsStore}, - block_window_cache::BlockWindowCacheStore, + block_window_cache::{BlockWindowCacheStore, BlockWindowCacheWriter}, daa::DbDaaStore, depth::{DbDepthStore, DepthStoreReader}, ghostdag::{DbGhostdagStore, GhostdagData, GhostdagStoreReader}, @@ -43,7 +43,7 @@ use crate::{ processes::{ coinbase::CoinbaseManager, ghostdag::ordering::SortableBlock, - transaction_validator::{errors::TxResult, transaction_validator_populated::TxValidationFlags, TransactionValidator}, + transaction_validator::{errors::TxResult, tx_validation_in_utxo_context::TxValidationFlags, TransactionValidator}, window::WindowManager, }, }; @@ -807,7 +807,11 @@ impl VirtualStateProcessor { args: &TransactionValidationArgs, ) -> TxResult<()> { self.transaction_validator.validate_tx_in_isolation(&mutable_tx.tx)?; - self.transaction_validator.utxo_free_tx_validation(&mutable_tx.tx, virtual_daa_score, virtual_past_median_time)?; + self.transaction_validator.validate_tx_in_header_context_with_args( + &mutable_tx.tx, + virtual_daa_score, + virtual_past_median_time, + )?; self.validate_mempool_transaction_in_utxo_context(mutable_tx, virtual_utxo_view, virtual_daa_score, args)?; Ok(()) } @@ -896,7 +900,11 @@ impl VirtualStateProcessor { // No need to validate the transaction in isolation since we rely on the mining manager to submit transactions // which were previously validated through `validate_mempool_transaction_and_populate`, hence we only perform // in-context validations - self.transaction_validator.utxo_free_tx_validation(tx, virtual_state.daa_score, virtual_state.past_median_time)?; + self.transaction_validator.validate_tx_in_header_context_with_args( + tx, + virtual_state.daa_score, + virtual_state.past_median_time, + )?; let ValidatedTransaction { calculated_fee, .. } = self.validate_transaction_in_utxo_context(tx, utxo_view, virtual_state.daa_score, TxValidationFlags::Full)?; Ok(calculated_fee) @@ -1202,6 +1210,15 @@ impl VirtualStateProcessor { true } } + + /// Executes `op` within the thread pool associated with this processor. + pub fn install(&self, op: OP) -> R + where + OP: FnOnce() -> R + Send, + R: Send, + { + self.thread_pool.install(op) + } } enum MergesetIncreaseResult { diff --git a/consensus/src/pipeline/virtual_processor/utxo_validation.rs b/consensus/src/pipeline/virtual_processor/utxo_validation.rs index 4a62a4ae8e..2e9c7ddb4a 100644 --- a/consensus/src/pipeline/virtual_processor/utxo_validation.rs +++ b/consensus/src/pipeline/virtual_processor/utxo_validation.rs @@ -7,7 +7,7 @@ use crate::{ model::stores::{block_transactions::BlockTransactionsStoreReader, daa::DaaStoreReader, ghostdag::GhostdagData}, processes::transaction_validator::{ errors::{TxResult, TxRuleError}, - transaction_validator_populated::TxValidationFlags, + tx_validation_in_utxo_context::TxValidationFlags, }, }; use kaspa_consensus_core::{ diff --git a/consensus/src/processes/pruning_proof/mod.rs b/consensus/src/processes/pruning_proof/mod.rs index 2b3ba5f9d8..a9412bbf60 100644 --- a/consensus/src/processes/pruning_proof/mod.rs +++ b/consensus/src/processes/pruning_proof/mod.rs @@ -7,7 +7,6 @@ use std::{ hash_map::Entry::{self}, VecDeque, }, - ops::Deref, sync::{atomic::AtomicBool, Arc}, }; @@ -279,12 +278,11 @@ impl PruningProofManager { // PRUNE SAFETY: called either via consensus under the prune guard or by the pruning processor (hence no pruning in parallel) for anticone_block in anticone.iter().copied() { - let window = self - .window_manager - .block_window(&self.ghostdag_store.get_data(anticone_block).unwrap(), WindowType::FullDifficultyWindow) - .unwrap(); + let ghostdag = self.ghostdag_store.get_data(anticone_block).unwrap(); + let window = self.window_manager.block_window(&ghostdag, WindowType::DifficultyWindow).unwrap(); + let cover = self.window_manager.consecutive_cover_for_window(ghostdag, &window); - for hash in window.deref().iter().map(|block| block.0.hash) { + for hash in cover { if let Entry::Vacant(e) = daa_window_blocks.entry(hash) { e.insert(TrustedHeader { header: self.headers_store.get_header(hash).unwrap(), diff --git a/consensus/src/processes/reachability/inquirer.rs b/consensus/src/processes/reachability/inquirer.rs index ff09849b4a..3c1b153de3 100644 --- a/consensus/src/processes/reachability/inquirer.rs +++ b/consensus/src/processes/reachability/inquirer.rs @@ -156,21 +156,21 @@ pub fn hint_virtual_selected_parent(store: &mut (impl ReachabilityStore + ?Sized ) } -/// Checks if the `this` block is a strict chain ancestor of the `queried` block (aka `this ∈ chain(queried)`). +/// Checks if the `this` block is a strict chain ancestor of the `queried` block (i.e., `this ∈ chain(queried)`). /// Note that this results in `false` if `this == queried` pub fn is_strict_chain_ancestor_of(store: &(impl ReachabilityStoreReader + ?Sized), this: Hash, queried: Hash) -> Result { Ok(store.get_interval(this)?.strictly_contains(store.get_interval(queried)?)) } -/// Checks if `this` block is a chain ancestor of `queried` block (aka `this ∈ chain(queried) ∪ {queried}`). +/// Checks if `this` block is a chain ancestor of `queried` block (i.e., `this ∈ chain(queried) ∪ {queried}`). /// Note that we use the graph theory convention here which defines that a block is also an ancestor of itself. pub fn is_chain_ancestor_of(store: &(impl ReachabilityStoreReader + ?Sized), this: Hash, queried: Hash) -> Result { Ok(store.get_interval(this)?.contains(store.get_interval(queried)?)) } -/// Returns true if `this` is a DAG ancestor of `queried` (aka `queried ∈ future(this) ∪ {this}`). +/// Returns true if `this` is a DAG ancestor of `queried` (i.e., `queried ∈ future(this) ∪ {this}`). /// Note: this method will return true if `this == queried`. -/// The complexity of this method is O(log(|future_covering_set(this)|)) +/// The complexity of this method is `O(log(|future_covering_set(this)|))` pub fn is_dag_ancestor_of(store: &(impl ReachabilityStoreReader + ?Sized), this: Hash, queried: Hash) -> Result { // First, check if `this` is a chain ancestor of queried if is_chain_ancestor_of(store, this, queried)? { @@ -184,7 +184,7 @@ pub fn is_dag_ancestor_of(store: &(impl ReachabilityStoreReader + ?Sized), this: } } -/// Finds the child of `ancestor` which is also a chain ancestor of `descendant`. +/// Finds the tree child of `ancestor` which is also a chain ancestor of `descendant`. pub fn get_next_chain_ancestor(store: &(impl ReachabilityStoreReader + ?Sized), descendant: Hash, ancestor: Hash) -> Result { if descendant == ancestor { // The next ancestor does not exist @@ -200,7 +200,7 @@ pub fn get_next_chain_ancestor(store: &(impl ReachabilityStoreReader + ?Sized), } /// Note: it is important to keep the unchecked version for internal module use, -/// since in some scenarios during reindexing `descendant` might have a modified +/// since in some scenarios during reindexing `ancestor` might have a modified /// interval which was not propagated yet. pub(super) fn get_next_chain_ancestor_unchecked( store: &(impl ReachabilityStoreReader + ?Sized), diff --git a/consensus/src/processes/transaction_validator/mod.rs b/consensus/src/processes/transaction_validator/mod.rs index 3f091dfd76..b4a946c2ff 100644 --- a/consensus/src/processes/transaction_validator/mod.rs +++ b/consensus/src/processes/transaction_validator/mod.rs @@ -1,7 +1,7 @@ pub mod errors; -pub mod transaction_validator_populated; -mod tx_validation_in_isolation; -pub mod tx_validation_not_utxo_related; +pub mod tx_validation_in_header_context; +pub mod tx_validation_in_isolation; +pub mod tx_validation_in_utxo_context; use std::sync::Arc; use crate::model::stores::ghostdag; diff --git a/consensus/src/processes/transaction_validator/tx_validation_in_header_context.rs b/consensus/src/processes/transaction_validator/tx_validation_in_header_context.rs new file mode 100644 index 0000000000..129627c59d --- /dev/null +++ b/consensus/src/processes/transaction_validator/tx_validation_in_header_context.rs @@ -0,0 +1,102 @@ +//! Groups transaction validations that depend on the containing header and/or +//! its past headers (but do not depend on UTXO state or other transactions in +//! the containing block) + +use super::{ + errors::{TxResult, TxRuleError}, + TransactionValidator, +}; +use crate::constants::LOCK_TIME_THRESHOLD; +use kaspa_consensus_core::tx::Transaction; + +pub(crate) enum LockTimeType { + Finalized, + DaaScore, + Time, +} + +pub(crate) enum LockTimeArg { + Finalized, + DaaScore(u64), + MedianTime(u64), +} + +impl TransactionValidator { + pub(crate) fn validate_tx_in_header_context_with_args( + &self, + tx: &Transaction, + ctx_daa_score: u64, + ctx_block_time: u64, + ) -> TxResult<()> { + self.validate_tx_in_header_context( + tx, + ctx_daa_score, + match Self::get_lock_time_type(tx) { + LockTimeType::Finalized => LockTimeArg::Finalized, + LockTimeType::DaaScore => LockTimeArg::DaaScore(ctx_daa_score), + LockTimeType::Time => LockTimeArg::MedianTime(ctx_block_time), + }, + ) + } + + pub(crate) fn validate_tx_in_header_context( + &self, + tx: &Transaction, + ctx_daa_score: u64, + lock_time_arg: LockTimeArg, + ) -> TxResult<()> { + self.check_transaction_payload(tx, ctx_daa_score)?; + self.check_tx_is_finalized(tx, lock_time_arg) + } + + pub(crate) fn get_lock_time_type(tx: &Transaction) -> LockTimeType { + match tx.lock_time { + // Lock time of zero means the transaction is finalized. + 0 => LockTimeType::Finalized, + + // The lock time field of a transaction is either a block DAA score at + // which the transaction is finalized or a timestamp depending on if the + // value is before the LOCK_TIME_THRESHOLD. When it is under the + // threshold it is a DAA score + t if t < LOCK_TIME_THRESHOLD => LockTimeType::DaaScore, + + // ..and when equal or above the threshold it represents time + _t => LockTimeType::Time, + } + } + + fn check_tx_is_finalized(&self, tx: &Transaction, lock_time_arg: LockTimeArg) -> TxResult<()> { + let block_time_or_daa_score = match lock_time_arg { + LockTimeArg::Finalized => return Ok(()), + LockTimeArg::DaaScore(ctx_daa_score) => ctx_daa_score, + LockTimeArg::MedianTime(ctx_block_time) => ctx_block_time, + }; + + if tx.lock_time < block_time_or_daa_score { + return Ok(()); + } + + // At this point, the transaction's lock time hasn't occurred yet, but + // the transaction might still be finalized if the sequence number + // for all transaction inputs is maxed out. + for (i, input) in tx.inputs.iter().enumerate() { + if input.sequence != u64::MAX { + return Err(TxRuleError::NotFinalized(i)); + } + } + + Ok(()) + } + + fn check_transaction_payload(&self, tx: &Transaction, ctx_daa_score: u64) -> TxResult<()> { + // TODO (post HF): move back to in isolation validation + if self.payload_activation.is_active(ctx_daa_score) { + Ok(()) + } else { + if !tx.is_coinbase() && !tx.payload.is_empty() { + return Err(TxRuleError::NonCoinbaseTxHasPayload); + } + Ok(()) + } + } +} diff --git a/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs b/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs index a08b83d94e..b509a71c72 100644 --- a/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs +++ b/consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs @@ -8,6 +8,11 @@ use super::{ }; impl TransactionValidator { + /// Performs a variety of transaction validation checks which are independent of any + /// context -- header or utxo. **Note** that any check performed here should be moved to + /// header contextual validation if it becomes HF activation dependent. This is bcs we rely + /// on checks here to be truly independent and avoid calling it multiple times wherever possible + /// (e.g., BBT relies on mempool in isolation checks even though virtual daa score might have changed) pub fn validate_tx_in_isolation(&self, tx: &Transaction) -> TxResult<()> { self.check_transaction_inputs_in_isolation(tx)?; self.check_transaction_outputs_in_isolation(tx)?; diff --git a/consensus/src/processes/transaction_validator/transaction_validator_populated.rs b/consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs similarity index 100% rename from consensus/src/processes/transaction_validator/transaction_validator_populated.rs rename to consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs diff --git a/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs b/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs deleted file mode 100644 index 3a854948ac..0000000000 --- a/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs +++ /dev/null @@ -1,53 +0,0 @@ -use kaspa_consensus_core::tx::Transaction; - -use crate::constants::LOCK_TIME_THRESHOLD; - -use super::{ - errors::{TxResult, TxRuleError}, - TransactionValidator, -}; - -impl TransactionValidator { - pub fn utxo_free_tx_validation(&self, tx: &Transaction, ctx_daa_score: u64, ctx_block_time: u64) -> TxResult<()> { - self.check_transaction_payload(tx, ctx_daa_score)?; - self.check_tx_is_finalized(tx, ctx_daa_score, ctx_block_time) - } - - fn check_tx_is_finalized(&self, tx: &Transaction, ctx_daa_score: u64, ctx_block_time: u64) -> TxResult<()> { - // Lock time of zero means the transaction is finalized. - if tx.lock_time == 0 { - return Ok(()); - } - - // The lock time field of a transaction is either a block DAA score at - // which the transaction is finalized or a timestamp depending on if the - // value is before the LOCK_TIME_THRESHOLD. When it is under the - // threshold it is a DAA score. - let block_time_or_daa_score = if tx.lock_time < LOCK_TIME_THRESHOLD { ctx_daa_score } else { ctx_block_time }; - if tx.lock_time < block_time_or_daa_score { - return Ok(()); - } - - // At this point, the transaction's lock time hasn't occurred yet, but - // the transaction might still be finalized if the sequence number - // for all transaction inputs is maxed out. - for (i, input) in tx.inputs.iter().enumerate() { - if input.sequence != u64::MAX { - return Err(TxRuleError::NotFinalized(i)); - } - } - - Ok(()) - } - - fn check_transaction_payload(&self, tx: &Transaction, ctx_daa_score: u64) -> TxResult<()> { - if self.payload_activation.is_active(ctx_daa_score) { - Ok(()) - } else { - if !tx.is_coinbase() && !tx.payload.is_empty() { - return Err(TxRuleError::NonCoinbaseTxHasPayload); - } - Ok(()) - } - } -} diff --git a/consensus/src/processes/window.rs b/consensus/src/processes/window.rs index ab09b1e7cb..1caff9c007 100644 --- a/consensus/src/processes/window.rs +++ b/consensus/src/processes/window.rs @@ -1,6 +1,6 @@ use crate::{ model::stores::{ - block_window_cache::{BlockWindowCacheReader, BlockWindowHeap, WindowOrigin}, + block_window_cache::{BlockWindowCacheReader, BlockWindowCacheWriter, BlockWindowHeap, WindowOrigin}, daa::DaaStoreReader, ghostdag::{GhostdagData, GhostdagStoreReader}, headers::HeaderStoreReader, @@ -31,9 +31,8 @@ use super::{ #[derive(Clone, Copy)] pub enum WindowType { - SampledDifficultyWindow, - FullDifficultyWindow, - SampledMedianTimeWindow, + DifficultyWindow, + MedianTimeWindow, VaryingWindow(usize), } @@ -55,15 +54,44 @@ pub trait WindowManager { fn block_daa_window(&self, ghostdag_data: &GhostdagData) -> Result; fn calculate_difficulty_bits(&self, ghostdag_data: &GhostdagData, daa_window: &DaaWindow) -> u32; fn calc_past_median_time(&self, ghostdag_data: &GhostdagData) -> Result<(u64, Arc), RuleError>; + fn calc_past_median_time_for_known_hash(&self, hash: Hash) -> Result; fn estimate_network_hashes_per_second(&self, window: Arc) -> DifficultyResult; fn window_size(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> usize; fn sample_rate(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> u64; + + /// Returns the full consecutive sub-DAG containing all blocks required to restore the (possibly sampled) window. + fn consecutive_cover_for_window(&self, ghostdag_data: Arc, window: &BlockWindowHeap) -> Vec; +} + +trait AffiliatedWindowCacheReader { + fn get(&self, hash: &Hash) -> Option>; +} + +/// A local wrapper over an (optional) block window cache which filters cache hits based on a pre-specified window origin +struct AffiliatedWindowCache<'a, U: BlockWindowCacheReader> { + /// The inner underlying cache + inner: Option<&'a Arc>, + /// The affiliated origin (sampled vs. full) + origin: WindowOrigin, +} + +impl<'a, U: BlockWindowCacheReader> AffiliatedWindowCache<'a, U> { + fn new(inner: Option<&'a Arc>, origin: WindowOrigin) -> Self { + Self { inner, origin } + } +} + +impl AffiliatedWindowCacheReader for AffiliatedWindowCache<'_, U> { + fn get(&self, hash: &Hash) -> Option> { + // Only return the cached window if it originates from the affiliated origin + self.inner.and_then(|cache| cache.get(hash, self.origin)) + } } /// A window manager conforming (indirectly) to the legacy golang implementation /// based on full, hence un-sampled, windows #[derive(Clone)] -pub struct FullWindowManager { +pub struct FullWindowManager { genesis_hash: Hash, ghostdag_store: Arc, block_window_cache_for_difficulty: Arc, @@ -74,7 +102,7 @@ pub struct FullWindowManager, } -impl FullWindowManager { +impl FullWindowManager { pub fn new( genesis: &GenesisBlock, ghostdag_store: Arc, @@ -114,30 +142,29 @@ impl Fu return Ok(Arc::new(BlockWindowHeap::new(WindowOrigin::Full))); } - let cache = if window_size == self.difficulty_window_size { + let inner_cache = if window_size == self.difficulty_window_size { Some(&self.block_window_cache_for_difficulty) } else if window_size == self.past_median_time_window_size { Some(&self.block_window_cache_for_past_median_time) } else { None }; - - if let Some(cache) = cache { - if let Some(selected_parent_binary_heap) = cache.get(&ghostdag_data.selected_parent) { - // Only use the cached window if it originates from here - if let WindowOrigin::Full = selected_parent_binary_heap.origin() { - let mut window_heap = BoundedSizeBlockHeap::from_binary_heap(window_size, (*selected_parent_binary_heap).clone()); - if ghostdag_data.selected_parent != self.genesis_hash { - self.try_push_mergeset( - &mut window_heap, - ghostdag_data, - self.ghostdag_store.get_blue_work(ghostdag_data.selected_parent).unwrap(), - ); - } - - return Ok(Arc::new(window_heap.binary_heap)); - } + // Wrap the inner cache with a cache affiliated with this origin (WindowOrigin::Full). + // This is crucial for hardfork times where the DAA mechanism changes thereby invalidating cache entries + // originating from the prior mechanism + let cache = AffiliatedWindowCache::new(inner_cache, WindowOrigin::Full); + + if let Some(selected_parent_binary_heap) = cache.get(&ghostdag_data.selected_parent) { + let mut window_heap = BoundedSizeBlockHeap::from_binary_heap(window_size, (*selected_parent_binary_heap).clone()); + if ghostdag_data.selected_parent != self.genesis_hash { + self.try_push_mergeset( + &mut window_heap, + ghostdag_data, + self.ghostdag_store.get_blue_work(ghostdag_data.selected_parent).unwrap(), + ); } + + return Ok(Arc::new(window_heap.binary_heap)); } let mut window_heap = BoundedSizeBlockHeap::new(WindowOrigin::Full, window_size); @@ -194,7 +221,9 @@ impl Fu } } -impl WindowManager for FullWindowManager { +impl WindowManager + for FullWindowManager +{ fn block_window(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> Result, RuleError> { self.build_block_window(ghostdag_data, window_type) } @@ -206,7 +235,7 @@ impl Wi } fn block_daa_window(&self, ghostdag_data: &GhostdagData) -> Result { - let window = self.block_window(ghostdag_data, WindowType::SampledDifficultyWindow)?; + let window = self.block_window(ghostdag_data, WindowType::DifficultyWindow)?; Ok(self.calc_daa_window(ghostdag_data, window)) } @@ -215,19 +244,31 @@ impl Wi } fn calc_past_median_time(&self, ghostdag_data: &GhostdagData) -> Result<(u64, Arc), RuleError> { - let window = self.block_window(ghostdag_data, WindowType::SampledMedianTimeWindow)?; + let window = self.block_window(ghostdag_data, WindowType::MedianTimeWindow)?; let past_median_time = self.past_median_time_manager.calc_past_median_time(&window)?; Ok((past_median_time, window)) } + fn calc_past_median_time_for_known_hash(&self, hash: Hash) -> Result { + if let Some(window) = self.block_window_cache_for_past_median_time.get(&hash, WindowOrigin::Full) { + let past_median_time = self.past_median_time_manager.calc_past_median_time(&window)?; + Ok(past_median_time) + } else { + let ghostdag_data = self.ghostdag_store.get_data(hash).unwrap(); + let (past_median_time, window) = self.calc_past_median_time(&ghostdag_data)?; + self.block_window_cache_for_past_median_time.insert(hash, window); + Ok(past_median_time) + } + } + fn estimate_network_hashes_per_second(&self, window: Arc) -> DifficultyResult { self.difficulty_manager.estimate_network_hashes_per_second(&window) } fn window_size(&self, _ghostdag_data: &GhostdagData, window_type: WindowType) -> usize { match window_type { - WindowType::SampledDifficultyWindow | WindowType::FullDifficultyWindow => self.difficulty_window_size, - WindowType::SampledMedianTimeWindow => self.past_median_time_window_size, + WindowType::DifficultyWindow => self.difficulty_window_size, + WindowType::MedianTimeWindow => self.past_median_time_window_size, WindowType::VaryingWindow(size) => size, } } @@ -235,6 +276,11 @@ impl Wi fn sample_rate(&self, _ghostdag_data: &GhostdagData, _window_type: WindowType) -> u64 { 1 } + + fn consecutive_cover_for_window(&self, _ghostdag_data: Arc, window: &BlockWindowHeap) -> Vec { + assert_eq!(WindowOrigin::Full, window.origin()); + window.iter().map(|b| b.0.hash).collect() + } } type DaaStatus = Option<(u64, BlockHashSet)>; @@ -246,7 +292,12 @@ enum SampledBlock { /// A sampled window manager implementing [KIP-0004](https://github.com/kaspanet/kips/blob/master/kip-0004.md) #[derive(Clone)] -pub struct SampledWindowManager { +pub struct SampledWindowManager< + T: GhostdagStoreReader, + U: BlockWindowCacheReader + BlockWindowCacheWriter, + V: HeaderStoreReader, + W: DaaStoreReader, +> { genesis_hash: Hash, ghostdag_store: Arc, headers_store: Arc, @@ -263,7 +314,9 @@ pub struct SampledWindowManager, } -impl SampledWindowManager { +impl + SampledWindowManager +{ #[allow(clippy::too_many_arguments)] pub fn new( genesis: &GenesisBlock, @@ -331,11 +384,15 @@ impl Some(&self.block_window_cache_for_difficulty), - WindowType::SampledMedianTimeWindow => Some(&self.block_window_cache_for_past_median_time), - WindowType::FullDifficultyWindow | WindowType::VaryingWindow(_) => None, + let inner_cache = match window_type { + WindowType::DifficultyWindow => Some(&self.block_window_cache_for_difficulty), + WindowType::MedianTimeWindow => Some(&self.block_window_cache_for_past_median_time), + WindowType::VaryingWindow(_) => None, }; + // Wrap the inner cache with a cache affiliated with this origin (WindowOrigin::Sampled). + // This is crucial for hardfork times where the DAA mechanism changes thereby invalidating cache entries + // originating from the prior mechanism + let cache = AffiliatedWindowCache::new(inner_cache, WindowOrigin::Sampled); let selected_parent_blue_work = self.ghostdag_store.get_blue_work(ghostdag_data.selected_parent).unwrap(); @@ -343,7 +400,7 @@ impl); // see if we can inherit and merge with the selected parent cache - if self.try_merge_with_selected_parent_cache(&mut window_heap, cache, ¤t_ghostdag.selected_parent) { + if self.try_merge_with_selected_parent_cache(&mut window_heap, &cache, ¤t_ghostdag.selected_parent) { // if successful, we may break out of the loop, with the window already filled. break; }; @@ -439,36 +496,33 @@ impl>, + cache: &impl AffiliatedWindowCacheReader, ghostdag_data: &GhostdagData, selected_parent_blue_work: BlueWorkType, mergeset_non_daa_inserter: Option, ) -> Option> { - cache.and_then(|cache| { - cache.get(&ghostdag_data.selected_parent).map(|selected_parent_window| { - let mut heap = Lazy::new(|| BoundedSizeBlockHeap::from_binary_heap(window_size, (*selected_parent_window).clone())); - // We pass a Lazy heap as an optimization to avoid cloning the selected parent heap in cases where the mergeset contains no samples - self.push_mergeset(&mut heap, sample_rate, ghostdag_data, selected_parent_blue_work, mergeset_non_daa_inserter); - if let Ok(heap) = Lazy::into_value(heap) { - Arc::new(heap.binary_heap) - } else { - selected_parent_window.clone() - } - }) + cache.get(&ghostdag_data.selected_parent).map(|selected_parent_window| { + let mut heap = Lazy::new(|| BoundedSizeBlockHeap::from_binary_heap(window_size, (*selected_parent_window).clone())); + // We pass a Lazy heap as an optimization to avoid cloning the selected parent heap in cases where the mergeset contains no samples + self.push_mergeset(&mut heap, sample_rate, ghostdag_data, selected_parent_blue_work, mergeset_non_daa_inserter); + if let Ok(heap) = Lazy::into_value(heap) { + Arc::new(heap.binary_heap) + } else { + selected_parent_window.clone() + } }) } fn try_merge_with_selected_parent_cache( &self, heap: &mut BoundedSizeBlockHeap, - cache: Option<&Arc>, + cache: &impl AffiliatedWindowCacheReader, selected_parent: &Hash, ) -> bool { cache - .and_then(|cache| { - cache.get(selected_parent).map(|selected_parent_window| { - heap.merge_ancestor_heap(&mut (*selected_parent_window).clone()); - }) + .get(selected_parent) + .map(|selected_parent_window| { + heap.merge_ancestor_heap(&mut (*selected_parent_window).clone()); }) .is_some() } @@ -501,7 +555,7 @@ impl WindowManager +impl WindowManager for SampledWindowManager { fn block_window(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> Result, RuleError> { @@ -516,7 +570,7 @@ impl Result { let mut mergeset_non_daa = BlockHashSet::default(); - let window = self.build_block_window(ghostdag_data, WindowType::SampledDifficultyWindow, |hash| { + let window = self.build_block_window(ghostdag_data, WindowType::DifficultyWindow, |hash| { mergeset_non_daa.insert(hash); })?; let daa_score = self.difficulty_manager.calc_daa_score(ghostdag_data, &mergeset_non_daa); @@ -528,33 +582,73 @@ impl Result<(u64, Arc), RuleError> { - let window = self.block_window(ghostdag_data, WindowType::SampledMedianTimeWindow)?; + let window = self.block_window(ghostdag_data, WindowType::MedianTimeWindow)?; let past_median_time = self.past_median_time_manager.calc_past_median_time(&window)?; Ok((past_median_time, window)) } + fn calc_past_median_time_for_known_hash(&self, hash: Hash) -> Result { + if let Some(window) = self.block_window_cache_for_past_median_time.get(&hash, WindowOrigin::Sampled) { + let past_median_time = self.past_median_time_manager.calc_past_median_time(&window)?; + Ok(past_median_time) + } else { + let ghostdag_data = self.ghostdag_store.get_data(hash).unwrap(); + let (past_median_time, window) = self.calc_past_median_time(&ghostdag_data)?; + self.block_window_cache_for_past_median_time.insert(hash, window); + Ok(past_median_time) + } + } + fn estimate_network_hashes_per_second(&self, window: Arc) -> DifficultyResult { self.difficulty_manager.estimate_network_hashes_per_second(&window) } fn window_size(&self, _ghostdag_data: &GhostdagData, window_type: WindowType) -> usize { match window_type { - WindowType::SampledDifficultyWindow => self.difficulty_window_size, - // We aim to return a full window such that it contains what would be the sampled window. Note that the - // product below addresses also the worst-case scenario where the last sampled block is exactly `sample_rate` - // blocks from the end of the full window - WindowType::FullDifficultyWindow => self.difficulty_window_size * self.difficulty_sample_rate as usize, - WindowType::SampledMedianTimeWindow => self.past_median_time_window_size, + WindowType::DifficultyWindow => self.difficulty_window_size, + WindowType::MedianTimeWindow => self.past_median_time_window_size, WindowType::VaryingWindow(size) => size, } } fn sample_rate(&self, _ghostdag_data: &GhostdagData, window_type: WindowType) -> u64 { match window_type { - WindowType::SampledDifficultyWindow => self.difficulty_sample_rate, - WindowType::SampledMedianTimeWindow => self.past_median_time_sample_rate, - WindowType::FullDifficultyWindow | WindowType::VaryingWindow(_) => 1, + WindowType::DifficultyWindow => self.difficulty_sample_rate, + WindowType::MedianTimeWindow => self.past_median_time_sample_rate, + WindowType::VaryingWindow(_) => 1, + } + } + + fn consecutive_cover_for_window(&self, mut ghostdag: Arc, window: &BlockWindowHeap) -> Vec { + assert_eq!(WindowOrigin::Sampled, window.origin()); + + // In the sampled case, the sampling logic relies on DAA indexes which can only be calculated correctly if the full + // mergesets covering all sampled blocks are sent. + + // Tracks the window blocks to make sure we visit all blocks + let mut unvisited: BlockHashSet = window.iter().map(|b| b.0.hash).collect(); + let capacity_estimate = window.len() * self.difficulty_sample_rate as usize; + // The full consecutive window covering all sampled window blocks and the full mergesets containing them + let mut cover = Vec::with_capacity(capacity_estimate); + while !unvisited.is_empty() { + assert!(!ghostdag.selected_parent.is_origin(), "unvisited still not empty"); + // TODO (relaxed): a possible optimization here is to iterate in the same order as + // sampled_mergeset_iterator (descending_mergeset) and to break once all samples from + // this mergeset are reached. + // * Why is this sufficient? bcs we still send the prefix of the mergeset required for + // obtaining the DAA index for all sampled blocks. + // * What's the benefit? This might exclude deeply merged blocks which in turn will help + // reducing the number of trusted blocks sent to a fresh syncing peer. + for merged in ghostdag.unordered_mergeset() { + cover.push(merged); + unvisited.remove(&merged); + } + if unvisited.is_empty() { + break; + } + ghostdag = self.ghostdag_store.get_data(ghostdag.selected_parent).unwrap(); } + cover } } @@ -562,7 +656,12 @@ impl { +pub struct DualWindowManager< + T: GhostdagStoreReader, + U: BlockWindowCacheReader + BlockWindowCacheWriter, + V: HeaderStoreReader, + W: DaaStoreReader, +> { ghostdag_store: Arc, headers_store: Arc, sampling_activation: ForkActivation, @@ -570,7 +669,9 @@ pub struct DualWindowManager, } -impl DualWindowManager { +impl + DualWindowManager +{ #[allow(clippy::too_many_arguments)] pub fn new( genesis: &GenesisBlock, @@ -621,67 +722,82 @@ impl bool { - let sp_daa_score = self.headers_store.get_daa_score(ghostdag_data.selected_parent).unwrap(); + /// Checks whether sampling mode was activated based on the selected parent (internally checking its DAA score) + pub(crate) fn sampling(&self, selected_parent: Hash) -> bool { + let sp_daa_score = self.headers_store.get_daa_score(selected_parent).unwrap(); self.sampling_activation.is_active(sp_daa_score) } } -impl WindowManager +impl WindowManager for DualWindowManager { fn block_window(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> Result, RuleError> { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.block_window(ghostdag_data, window_type), false => self.full_window_manager.block_window(ghostdag_data, window_type), } } fn calc_daa_window(&self, ghostdag_data: &GhostdagData, window: Arc) -> DaaWindow { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.calc_daa_window(ghostdag_data, window), false => self.full_window_manager.calc_daa_window(ghostdag_data, window), } } fn block_daa_window(&self, ghostdag_data: &GhostdagData) -> Result { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.block_daa_window(ghostdag_data), false => self.full_window_manager.block_daa_window(ghostdag_data), } } fn calculate_difficulty_bits(&self, ghostdag_data: &GhostdagData, daa_window: &DaaWindow) -> u32 { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.calculate_difficulty_bits(ghostdag_data, daa_window), false => self.full_window_manager.calculate_difficulty_bits(ghostdag_data, daa_window), } } fn calc_past_median_time(&self, ghostdag_data: &GhostdagData) -> Result<(u64, Arc), RuleError> { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.calc_past_median_time(ghostdag_data), false => self.full_window_manager.calc_past_median_time(ghostdag_data), } } + fn calc_past_median_time_for_known_hash(&self, hash: Hash) -> Result { + match self.sampling(self.ghostdag_store.get_selected_parent(hash).unwrap()) { + true => self.sampled_window_manager.calc_past_median_time_for_known_hash(hash), + false => self.full_window_manager.calc_past_median_time_for_known_hash(hash), + } + } + fn estimate_network_hashes_per_second(&self, window: Arc) -> DifficultyResult { self.sampled_window_manager.estimate_network_hashes_per_second(window) } fn window_size(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> usize { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.window_size(ghostdag_data, window_type), false => self.full_window_manager.window_size(ghostdag_data, window_type), } } fn sample_rate(&self, ghostdag_data: &GhostdagData, window_type: WindowType) -> u64 { - match self.sampling(ghostdag_data) { + match self.sampling(ghostdag_data.selected_parent) { true => self.sampled_window_manager.sample_rate(ghostdag_data, window_type), false => self.full_window_manager.sample_rate(ghostdag_data, window_type), } } + + fn consecutive_cover_for_window(&self, ghostdag_data: Arc, window: &BlockWindowHeap) -> Vec { + match window.origin() { + WindowOrigin::Sampled => self.sampled_window_manager.consecutive_cover_for_window(ghostdag_data, window), + WindowOrigin::Full => self.full_window_manager.consecutive_cover_for_window(ghostdag_data, window), + } + } } struct BoundedSizeBlockHeap { diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index 58a6e2bb33..52d9b79865 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -1367,7 +1367,7 @@ async fn difficulty_test() { fn full_window_bits(consensus: &TestConsensus, hash: Hash) -> u32 { let window_size = consensus.params().difficulty_window_size(0) * consensus.params().difficulty_sample_rate(0) as usize; let ghostdag_data = &consensus.ghostdag_store().get_data(hash).unwrap(); - let window = consensus.window_manager().block_window(ghostdag_data, WindowType::FullDifficultyWindow).unwrap(); + let window = consensus.window_manager().block_window(ghostdag_data, WindowType::VaryingWindow(window_size)).unwrap(); assert_eq!(window.blocks.len(), window_size); let daa_window = consensus.window_manager().calc_daa_window(ghostdag_data, window); consensus.window_manager().calculate_difficulty_bits(ghostdag_data, &daa_window) From 233552b4f37dba410c8008fdce5711342254b6d5 Mon Sep 17 00:00:00 2001 From: Michael Sutton Date: Thu, 28 Nov 2024 23:42:06 +0200 Subject: [PATCH 5/5] Track the average transaction mass throughout the mempool's lifespan (#599) * Track the average transaction mass throughout the mempool's lifespan using a decaying formulae * notebook analysis * finalize notebook * relax feerate estimator test comparisons to avoid arbitrary CI failures * review comment --- mining/src/feerate/fee_estimation.ipynb | 159 +++++++++++++++++++++++- mining/src/mempool/model/frontier.rs | 48 ++++--- 2 files changed, 188 insertions(+), 19 deletions(-) diff --git a/mining/src/feerate/fee_estimation.ipynb b/mining/src/feerate/fee_estimation.ipynb index a8b8fbfc89..51b905fa29 100644 --- a/mining/src/feerate/fee_estimation.ipynb +++ b/mining/src/feerate/fee_estimation.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 97, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -464,6 +464,155 @@ "pred" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Avg transaction mass\n", + "\n", + "We suggest a decaying weight formula for calculating the average mass throughout history, as opposed to using the average mass of the currently existing transactions. The following code compares the two approaches." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "Helper function for creating a long sequence of transaction masses with periods with highly unusual mass clusters\n", + "N = sequence length\n", + "M = length of each unusual cluster\n", + "X = number of unusual clusters\n", + "\"\"\"\n", + "def generate_seq(N, M, X, mean=2036, var=100, mean_cluster=50000, var_cluster=10000):\n", + " seq = np.random.normal(loc=mean, scale=var, size=N)\n", + " clusters = np.random.normal(loc=mean_cluster, scale=var_cluster, size=X * M)\n", + " cluster_indices = np.random.choice(N - M, size=X, replace=False)\n", + " for i, idx in enumerate(cluster_indices):\n", + " seq[idx:idx+M] = clusters[i*M:(i+1)*M]\n", + " return seq" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create a long sequence of transaction masses with periods with highly unusual mass clusters\n", + "N = 500_000\n", + "seq = generate_seq(N, 50, 40)\n", + "\n", + "# Approach 1 - calculate the current average\n", + "\n", + "# Requires a removal strategy for having a meaningful \"current\"\n", + "R = 0.8\n", + "# At each time step, remove the first element with probability 1 - R\n", + "removals = np.random.choice([0, 1], size=N, p=[R, 1 - R])\n", + "# After const steps, remove with probability 1, so that we simulate a mempool with nearly const size\n", + "removals[256:] = 1\n", + "j = 0\n", + "y = []\n", + "for i in range(1, N+1):\n", + " y.append(np.sum(seq[j:i])/(i-j))\n", + " if removals[i-1] == 1:\n", + " j += 1\n", + "\n", + "x = np.arange(0, N)\n", + "plt.figure()\n", + "plt.plot(x, y)\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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vffipptu0nSOhx9+GKtXr8bLL7+Myy+/HKdOncKpU6dUnYOVeImIiEgTm82GpUuXYtKkSY7nqqqq8OSTT+LDDz9EeXk5+vTpg7///e+O1iW7du1C3759UVhYiB49emj+2lxCIiIiIt1kZ2cjLy8PCxcuxPbt2/HLX/4S1157Lfbs2QMAWLZsGbp06YLly5ejc+fO6NSpE+677z7VMzAMYIiIiEgXxcXFmDNnDhYvXowRI0aga9eu+OMf/4grr7wSc+bMAQDs378fP/30ExYvXox58+Zh7ty5yM/Px80336zqazEHhoiIiHSxY8cO1NXVoXv37i7PV1VVoXXr1gCA+vp6VFVVYd68eY7jZs+ejUGDBqGoqEjxshIDGCIiItLF2bNnERkZifz8fERGRrp8rGXLlgCAdu3aISoqyiXI6dWrF4DGGRwGMERERBRQAwYMQF1dHY4dO4YRI0Z4PGb48OGora3Fvn370LVrVwDAjz/+CADo2LGj4q/FXUhERESk2NmzZ7F3714AjQHLrFmzMHr0aCQnJ6NDhw749a9/jW+++QavvPIKBgwYgOPHj2PNmjXo27cvJkyYgPr6egwZMgQtW7bEa6+9hvr6emRlZSEhIQGrV69WPA4GMERERKTY+vXrMXr06Iuev+uuuzB37lzU1NTgb3/7G+bNm4cjR46gTZs2GDZsGJ599llcfvnlAICSkhI8+OCDWL16NVq0aIHx48fjlVdeQXJysuJxMIAhIiKioMNt1ERERBR0GMAQERFR0GEAQ0REREGHAQwREREFHQYwREREFHQYwBAREVHQYQBDREREQYcBDBEREQUdBjBEREQUdBjAEBERUdBhAENERERBhwEMERERBZ3/B1bXq0p/cA7rAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "N = 5_000_000\n", + "seq = generate_seq(N, 50, 40)\n", + "# Approach 2 - calculate a decaying average\n", + "D = 0.99999\n", + "y = []\n", + "avg = 2000\n", + "for i in range(N):\n", + " avg = avg * D + seq[i] * (1 - D) \n", + " y.append(avg)\n", + "\n", + "x = np.arange(0, N)\n", + "plt.figure()\n", + "plt.plot(x, y)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Approach 1: \n", + "Clearly fails in this case and is highly influenced by the unusual clusters. \n", + "\n", + "#### Approach 2:\n", + "Learns the long term average and clusters only cause minor spikes." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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f+7tZjUoIy9nv/fee63t2rWzBgYGWtu3b2+99957rQcOHPB2szzq008/tfbt29caFBRk7dWrl/Wdd97xdpM8bvXq1VZJ1n379nm7KV5hslqtVu9ELgAAgIbFHB8AANBoEHwAAECjQfABAACNBsEHAAA0GgQfAADQaBB8AABAo0HwAQAAjQbBBwAAeFxGRoZGjx6t6OhomUwmLVu2rM6vYbVa9eqrr6pHjx4KCgpS+/bt9eKLL9bpNRrhXtUAAKChFRUVKS4uTg899JDGjh1br9eYOHGiPv/8c7366qvq16+fzp07p3PnztXpNdi5GQAANCiTyaSlS5dqzJgx9mPFxcX6wx/+oH/84x/Ky8tT37599fLLL+vWW2+VJH333Xfq37+/du/erZ49e9b7vRnqAgAAXjdhwgRt2rRJixcv1jfffKN77rlHP/nJT/T9999Lkj799FN16dJFn332mTp37qzY2Fg9/PDDda74EHwAAIBXHTlyRPPnz9eHH36oYcOGqWvXrnryySd1yy23aP78+ZKkQ4cO6fDhw/rwww/1wQcfaMGCBcrMzNTdd99dp/dijg8AAPCqXbt2yWw2q0ePHg7Hi4uL1apVK0mSxWJRcXGxPvjgA/t58+bN08CBA7Vv375aD38RfAAAgFcVFhbK399fmZmZ8vf3d/heaGioJKldu3YKCAhwCEe9e/eWVF4xIvgAAACfcOONN8psNis3N1fDhg1zec7QoUNVVlamgwcPqmvXrpKk/fv3S5I6depU6/diVRcAAPC4wsJCHThwQFJ50Hn99dc1YsQItWzZUjExMbr//vv11Vdf6bXXXtONN96o06dPKz09Xf3799eoUaNksVg0aNAghYaGatasWbJYLEpNTVV4eLg+//zzWreD4AMAADxuw4YNGjFihNPxlJQULViwQKWlpZo+fbo++OADHT9+XJGRkbr55pv1xz/+Uf369ZMknThxQo8//rg+//xzNWvWTMnJyXrttdfUsmXLWreD4AMAABoNlrMDAIBGg+ADAAAaDYIPAABoNAg+AACg0SD4AACARoPgAwAAGg2CDwAAaDQIPgAAoNEg+AAAgEaD4AMAABoNgg8AAGg0CD4AAKDR+P8cUnyKMvhSngAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Increase the mean in the long term and see how long it takes the decaying avg to fully adjust\n", + "seq2 = np.concatenate((seq, np.repeat(4000, N // 2)))\n", + "N2 = len(seq2)\n", + "y = []\n", + "avg = 2000\n", + "for i in range(N2):\n", + " avg = avg * D + seq2[i] * (1 - D) \n", + " y.append(avg)\n", + "\n", + "x = np.arange(0, N2)\n", + "plt.figure()\n", + "plt.plot(x, y)\n", + "plt.yscale('log')\n", + "plt.show()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -474,9 +623,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:gr]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-gr-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -488,9 +637,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/mining/src/mempool/model/frontier.rs b/mining/src/mempool/model/frontier.rs index 8d21953271..70ac215bad 100644 --- a/mining/src/mempool/model/frontier.rs +++ b/mining/src/mempool/model/frontier.rs @@ -25,20 +25,30 @@ const COLLISION_FACTOR: u64 = 4; /// hard limit in order to allow the SequenceSelector to compensate for consensus rejections. const MASS_LIMIT_FACTOR: f64 = 1.2; -/// A rough estimation for the average transaction mass. The usage is a non-important edge case -/// hence we just throw this here (as oppose to performing an accurate estimation) -const TYPICAL_TX_MASS: f64 = 2000.0; +/// Initial estimation of the average transaction mass. +const INITIAL_AVG_MASS: f64 = 2036.0; + +/// Decay factor of average mass weighting. +const AVG_MASS_DECAY_FACTOR: f64 = 0.99999; /// Management of the transaction pool frontier, that is, the set of transactions in /// the transaction pool which have no mempool ancestors and are essentially ready /// to enter the next block template. -#[derive(Default)] pub struct Frontier { /// Frontier transactions sorted by feerate order and searchable for weight sampling search_tree: SearchTree, /// Total masses: Σ_{tx in frontier} tx.mass total_mass: u64, + + /// Tracks the average transaction mass throughout the mempool's lifespan using a decayed weighting mechanism + average_transaction_mass: f64, +} + +impl Default for Frontier { + fn default() -> Self { + Self { search_tree: Default::default(), total_mass: Default::default(), average_transaction_mass: INITIAL_AVG_MASS } + } } impl Frontier { @@ -62,6 +72,11 @@ impl Frontier { let mass = key.mass; if self.search_tree.insert(key) { self.total_mass += mass; + // A decaying average formula. Denote ɛ = 1 - AVG_MASS_DECAY_FACTOR. A transaction inserted N slots ago has + // ɛ * (1 - ɛ)^N weight within the updated average. This gives some weight to the full mempool history while + // giving higher importance to more recent samples. + self.average_transaction_mass = + self.average_transaction_mass * AVG_MASS_DECAY_FACTOR + mass as f64 * (1.0 - AVG_MASS_DECAY_FACTOR); true } else { false @@ -210,10 +225,7 @@ impl Frontier { /// Builds a feerate estimator based on internal state of the ready transactions frontier pub fn build_feerate_estimator(&self, args: FeerateEstimatorArgs) -> FeerateEstimator { - let average_transaction_mass = match self.len() { - 0 => TYPICAL_TX_MASS, - n => self.total_mass() as f64 / n as f64, - }; + let average_transaction_mass = self.average_transaction_mass; let bps = args.network_blocks_per_second as f64; let mut mass_per_block = args.maximum_mass_per_block as f64; let mut inclusion_interval = average_transaction_mass / (mass_per_block * bps); @@ -368,8 +380,12 @@ mod tests { assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::()); } + /// Epsilon used for various test comparisons + const EPS: f64 = 0.000001; + #[test] fn test_feerate_estimator() { + const MIN_FEERATE: f64 = 1.0; let mut rng = thread_rng(); let cap = 2000; let mut map = HashMap::with_capacity(cap); @@ -394,13 +410,13 @@ mod tests { let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 }; // We are testing that the build function actually returns and is not looping indefinitely let estimator = frontier.build_feerate_estimator(args); - let estimations = estimator.calc_estimations(1.0); + let estimations = estimator.calc_estimations(MIN_FEERATE); let buckets = estimations.ordered_buckets(); // Test for the absence of NaN, infinite or zero values in buckets for b in buckets.iter() { assert!( - b.feerate.is_normal() && b.feerate >= 1.0, + b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS, "bucket feerate must be a finite number greater or equal to the minimum standard feerate" ); assert!( @@ -441,7 +457,7 @@ mod tests { // Test for the absence of NaN, infinite or zero values in buckets for b in buckets.iter() { assert!( - b.feerate.is_normal() && b.feerate >= MIN_FEERATE, + b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS, "bucket feerate must be a finite number greater or equal to the minimum standard feerate" ); assert!( @@ -492,7 +508,7 @@ mod tests { // Test for the absence of NaN, infinite or zero values in buckets for b in buckets.iter() { assert!( - b.feerate.is_normal() && b.feerate >= MIN_FEERATE, + b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS, "bucket feerate must be a finite number greater or equal to the minimum standard feerate" ); assert!( @@ -506,6 +522,7 @@ mod tests { #[test] fn test_feerate_estimator_with_less_than_block_capacity() { + const MIN_FEERATE: f64 = 1.0; let mut map = HashMap::new(); for i in 0..304 { let mass: u64 = 1650; @@ -524,13 +541,16 @@ mod tests { let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 }; // We are testing that the build function actually returns and is not looping indefinitely let estimator = frontier.build_feerate_estimator(args); - let estimations = estimator.calc_estimations(1.0); + let estimations = estimator.calc_estimations(MIN_FEERATE); let buckets = estimations.ordered_buckets(); // Test for the absence of NaN, infinite or zero values in buckets for b in buckets.iter() { // Expect min feerate bcs blocks are not full - assert!(b.feerate == 1.0, "bucket feerate is expected to be equal to the minimum standard feerate"); + assert!( + (b.feerate - MIN_FEERATE).abs() <= EPS, + "bucket feerate is expected to be equal to the minimum standard feerate" + ); assert!( b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0 && b.estimated_seconds <= 1.0, "bucket estimated seconds must be a finite number greater than zero & less than 1.0"