diff --git a/Cargo.toml b/Cargo.toml index 68ba1e6c17..95e5f64e8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,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/benches/check_scripts.rs b/consensus/benches/check_scripts.rs index a451eec650..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}; @@ -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/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 4e3d08a977..21fbc984c1 100644 --- a/consensus/client/src/input.rs +++ b/consensus/client/src/input.rs @@ -273,7 +273,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 2f5dc7bca3..ca18cd861f 100644 --- a/consensus/client/src/output.rs +++ b/consensus/client/src/output.rs @@ -173,7 +173,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 b620074929..13437db7c9 100644 --- a/consensus/client/src/transaction.rs +++ b/consensus/client/src/transaction.rs @@ -431,7 +431,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 fdcee86739..0c6d4511b2 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -370,7 +370,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, { @@ -522,7 +522,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/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..2c8006f75d 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,24 +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; -} - -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; } impl SigHashReusedValues for SigHashReusedValuesUnsync { @@ -98,6 +80,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 +126,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 +181,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 { +pub fn payload_hash(tx: &Transaction, reused_values: &impl SigHashReusedValues) -> 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 hash = || { + 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 +259,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 +284,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 +607,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/core/src/network.rs b/consensus/core/src/network.rs index 0fc7a45d4c..bf1a60e563 100644 --- a/consensus/core/src/network.rs +++ b/consensus/core/src/network.rs @@ -360,7 +360,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 { @@ -429,7 +429,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 f53e479dfd..8329b26c65 100644 --- a/consensus/core/src/tx/script_public_key.rs +++ b/consensus/core/src/tx/script_public_key.rs @@ -97,7 +97,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>, @@ -392,7 +392,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/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/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/model/services/reachability.rs b/consensus/src/model/services/reachability.rs index 39f5ceba2d..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; } @@ -154,7 +175,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/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/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 ec42f0f447..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,13 +1,18 @@ 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 kaspa_utils::option::OptionExtensions; use once_cell::unsync::Lazy; use std::sync::Arc; @@ -19,29 +24,18 @@ 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 lazy_pmt_res = Lazy::new(|| self.window_manager.calc_past_median_time_for_known_hash(block.hash())); 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)); - }; + 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)); }; } Ok(()) @@ -56,7 +50,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/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/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/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/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/processes/transaction_validator/mod.rs b/consensus/src/processes/transaction_validator/mod.rs index 7d007a3350..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; @@ -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_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 914624f940..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)?; @@ -16,7 +21,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 +111,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 +300,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/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 4cfa72b464..0000000000 --- a/consensus/src/processes/transaction_validator/tx_validation_not_utxo_related.rs +++ /dev/null @@ -1,41 +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_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(()) - } -} 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/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 ba47210114..c2dff202a1 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -560,7 +560,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 1fc77328be..3c20e28dc9 100644 --- a/crypto/hashes/src/lib.rs +++ b/crypto/hashes/src/lib.rs @@ -204,7 +204,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 6f7d879c82..dfd3a9d807 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/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" 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/bindings/wasm/src/resolver.rs b/rpc/wrpc/bindings/wasm/src/resolver.rs index 7abfdb6884..1753534372 100644 --- a/rpc/wrpc/bindings/wasm/src/resolver.rs +++ b/rpc/wrpc/bindings/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/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..52d9b79865 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(), } } } @@ -1364,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) @@ -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)); +} 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/bindings/wasm/cryptobox.rs b/wallet/core/src/bindings/wasm/cryptobox.rs index 957d4fc35a..76e9fdc3c0 100644 --- a/wallet/core/src/bindings/wasm/cryptobox.rs +++ b/wallet/core/src/bindings/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/bindings/wasm/utxo/context.rs b/wallet/core/src/bindings/wasm/utxo/context.rs index 0e2563549a..678c463cf6 100644 --- a/wallet/core/src/bindings/wasm/utxo/context.rs +++ b/wallet/core/src/bindings/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/bindings/wasm/utxo/processor.rs b/wallet/core/src/bindings/wasm/utxo/processor.rs index 905833325f..bc6961a2f0 100644 --- a/wallet/core/src/bindings/wasm/utxo/processor.rs +++ b/wallet/core/src/bindings/wasm/utxo/processor.rs @@ -199,7 +199,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/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 83fc91ac74..1f43c73ea8 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -73,7 +73,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, { @@ -198,7 +198,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/keys/src/derivation_path.rs b/wallet/keys/src/derivation_path.rs index b2bfd33879..dda2a4b7bb 100644 --- a/wallet/keys/src/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -96,7 +96,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 9924e9bc6a..9f1e45e145 100644 --- a/wallet/keys/src/keypair.rs +++ b/wallet/keys/src/keypair.rs @@ -160,7 +160,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 7d03436bad..57a92568fe 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -139,7 +139,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 4baa8b5503..23bae251ea 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -202,7 +202,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 717b85eee9..077b6c8244 100644 --- a/wallet/keys/src/xprv.rs +++ b/wallet/keys/src/xprv.rs @@ -249,7 +249,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 95ac9b5100..a2852f63dc 100644 --- a/wallet/keys/src/xpub.rs +++ b/wallet/keys/src/xpub.rs @@ -183,7 +183,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, {