Skip to content

Commit 64eeb89

Browse files
Enable payloads for non coinbase transactions (#591)
* Enable payloads for non coinbase transactions * Add payload hash to sighash * test reflects enabling payload * Enhance benchmarking: add payload size variations Refactored `mock_tx` to `mock_tx_with_payload` to support custom payload sizes. Introduced new benchmark function `benchmark_check_scripts_with_payload` to test performance with varying payload sizes. Commented out the old benchmark function to focus on payload-based tests. * Enhance script checking benchmarks Added benchmarks to evaluate script checking performance with varying payload sizes and input counts. This helps in understanding the impact of transaction payload size on validation and the relationship between input count and payload processing overhead. * Add new test case for transaction hashing and refactor code This commit introduces a new test case to verify that transaction IDs and hashes change with payload modifications. Additionally, code readability and consistency are improved by refactoring multi-line expressions into single lines where appropriate. * Add payload activation test for transactions This commit introduces a new integration test to validate the enforcement of payload activation rules at a specified DAA score. The test ensures that transactions with large payloads are rejected before activation and accepted afterward, maintaining consensus integrity. * style: fmt * test: add test that checks that payload change reflects sighash * rename test * Don't ever skip utxo_free_tx_validation * lints --------- Co-authored-by: max143672 <[email protected]>
1 parent a0aeec3 commit 64eeb89

File tree

14 files changed

+297
-58
lines changed

14 files changed

+297
-58
lines changed

consensus/benches/check_scripts.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ use kaspa_utils::iter::parallelism_in_power_steps;
1313
use rand::{thread_rng, Rng};
1414
use secp256k1::Keypair;
1515

16-
// You may need to add more detailed mocks depending on your actual code.
17-
fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec<UtxoEntry>) {
16+
fn mock_tx_with_payload(inputs_count: usize, non_uniq_signatures: usize, payload_size: usize) -> (Transaction, Vec<UtxoEntry>) {
17+
let mut payload = vec![0u8; payload_size];
18+
thread_rng().fill(&mut payload[..]);
19+
1820
let reused_values = SigHashReusedValuesUnsync::new();
1921
let dummy_prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1);
2022
let mut tx = Transaction::new(
@@ -24,10 +26,11 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec
2426
0,
2527
SubnetworkId::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
2628
0,
27-
vec![],
29+
payload,
2830
);
2931
let mut utxos = vec![];
3032
let mut kps = vec![];
33+
3134
for _ in 0..inputs_count - non_uniq_signatures {
3235
let kp = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());
3336
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
4043
});
4144
kps.push(kp);
4245
}
46+
4347
for _ in 0..non_uniq_signatures {
4448
let kp = kps.last().unwrap();
4549
tx.inputs.push(TransactionInput { previous_outpoint: dummy_prev_out, signature_script: vec![], sequence: 0, sig_op_count: 1 });
@@ -51,31 +55,32 @@ fn mock_tx(inputs_count: usize, non_uniq_signatures: usize) -> (Transaction, Vec
5155
is_coinbase: false,
5256
});
5357
}
58+
5459
for (i, kp) in kps.iter().enumerate().take(inputs_count - non_uniq_signatures) {
5560
let mut_tx = MutableTransaction::with_entries(&tx, utxos.clone());
5661
let sig_hash = calc_schnorr_signature_hash(&mut_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values);
5762
let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
5863
let sig: [u8; 64] = *kp.sign_schnorr(msg).as_ref();
59-
// This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
6064
tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect();
6165
}
66+
6267
let length = tx.inputs.len();
6368
for i in (inputs_count - non_uniq_signatures)..length {
6469
let kp = kps.last().unwrap();
6570
let mut_tx = MutableTransaction::with_entries(&tx, utxos.clone());
6671
let sig_hash = calc_schnorr_signature_hash(&mut_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values);
6772
let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
6873
let sig: [u8; 64] = *kp.sign_schnorr(msg).as_ref();
69-
// This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
7074
tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect();
7175
}
76+
7277
(tx, utxos)
7378
}
7479

7580
fn benchmark_check_scripts(c: &mut Criterion) {
7681
for inputs_count in [100, 50, 25, 10, 5, 2] {
7782
for non_uniq_signatures in [0, inputs_count / 2] {
78-
let (tx, utxos) = mock_tx(inputs_count, non_uniq_signatures);
83+
let (tx, utxos) = mock_tx_with_payload(inputs_count, non_uniq_signatures, 0);
7984
let mut group = c.benchmark_group(format!("inputs: {inputs_count}, non uniq: {non_uniq_signatures}"));
8085
group.sampling_mode(SamplingMode::Flat);
8186

@@ -97,12 +102,10 @@ fn benchmark_check_scripts(c: &mut Criterion) {
97102
})
98103
});
99104

100-
// Iterate powers of two up to available parallelism
101105
for i in parallelism_in_power_steps() {
102106
if inputs_count >= i {
103107
group.bench_function(format!("rayon, custom thread pool, thread count {i}"), |b| {
104108
let tx = MutableTransaction::with_entries(tx.clone(), utxos.clone());
105-
// Create a custom thread pool with the specified number of threads
106109
let pool = rayon::ThreadPoolBuilder::new().num_threads(i).build().unwrap();
107110
let cache = Cache::new(inputs_count as u64);
108111
b.iter(|| {
@@ -117,11 +120,44 @@ fn benchmark_check_scripts(c: &mut Criterion) {
117120
}
118121
}
119122

123+
/// Benchmarks script checking performance with different payload sizes and input counts.
124+
///
125+
/// This benchmark evaluates the performance impact of transaction payload size
126+
/// on script validation, testing multiple scenarios:
127+
///
128+
/// * Payload sizes: 0KB, 16KB, 32KB, 64KB, 128KB
129+
/// * Input counts: 1, 2, 10, 50 transactions
130+
///
131+
/// The benchmark helps understand:
132+
/// 1. How payload size affects validation performance
133+
/// 2. The relationship between input count and payload processing overhead
134+
fn benchmark_check_scripts_with_payload(c: &mut Criterion) {
135+
let payload_sizes = [0, 16_384, 32_768, 65_536, 131_072]; // 0, 16KB, 32KB, 64KB, 128KB
136+
let input_counts = [1, 2, 10, 50];
137+
let non_uniq_signatures = 0;
138+
139+
for inputs_count in input_counts {
140+
for &payload_size in &payload_sizes {
141+
let (tx, utxos) = mock_tx_with_payload(inputs_count, non_uniq_signatures, payload_size);
142+
let mut group = c.benchmark_group(format!("script_check/inputs_{}/payload_{}_kb", inputs_count, payload_size / 1024));
143+
group.sampling_mode(SamplingMode::Flat);
144+
145+
group.bench_function("parallel_validation", |b| {
146+
let tx = MutableTransaction::with_entries(tx.clone(), utxos.clone());
147+
let cache = Cache::new(inputs_count as u64);
148+
b.iter(|| {
149+
cache.clear();
150+
check_scripts_par_iter(black_box(&cache), black_box(&tx.as_verifiable()), false).unwrap();
151+
})
152+
});
153+
}
154+
}
155+
}
156+
120157
criterion_group! {
121158
name = benches;
122-
// This can be any expression that returns a `Criterion` object.
123159
config = Criterion::default().with_output_color(true).measurement_time(std::time::Duration::new(20, 0));
124-
targets = benchmark_check_scripts
160+
targets = benchmark_check_scripts, benchmark_check_scripts_with_payload
125161
}
126162

127163
criterion_main!(benches);

consensus/core/src/config/params.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ pub struct Params {
130130
pub skip_proof_of_work: bool,
131131
pub max_block_level: BlockLevel,
132132
pub pruning_proof_m: u64,
133+
134+
/// Activation rules for when to enable using the payload field in transactions
135+
pub payload_activation: ForkActivation,
133136
}
134137

135138
fn unix_now() -> u64 {
@@ -406,6 +409,8 @@ pub const MAINNET_PARAMS: Params = Params {
406409
skip_proof_of_work: false,
407410
max_block_level: 225,
408411
pruning_proof_m: 1000,
412+
413+
payload_activation: ForkActivation::never(),
409414
};
410415

411416
pub const TESTNET_PARAMS: Params = Params {
@@ -469,6 +474,8 @@ pub const TESTNET_PARAMS: Params = Params {
469474
skip_proof_of_work: false,
470475
max_block_level: 250,
471476
pruning_proof_m: 1000,
477+
478+
payload_activation: ForkActivation::never(),
472479
};
473480

474481
pub const TESTNET11_PARAMS: Params = Params {
@@ -530,6 +537,8 @@ pub const TESTNET11_PARAMS: Params = Params {
530537

531538
skip_proof_of_work: false,
532539
max_block_level: 250,
540+
541+
payload_activation: ForkActivation::never(),
533542
};
534543

535544
pub const SIMNET_PARAMS: Params = Params {
@@ -584,6 +593,8 @@ pub const SIMNET_PARAMS: Params = Params {
584593

585594
skip_proof_of_work: true, // For simnet only, PoW can be simulated by default
586595
max_block_level: 250,
596+
597+
payload_activation: ForkActivation::never(),
587598
};
588599

589600
pub const DEVNET_PARAMS: Params = Params {
@@ -641,4 +652,6 @@ pub const DEVNET_PARAMS: Params = Params {
641652
skip_proof_of_work: false,
642653
max_block_level: 250,
643654
pruning_proof_m: 1000,
655+
656+
payload_activation: ForkActivation::never(),
644657
};

consensus/core/src/hashing/sighash.rs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ use kaspa_hashes::{Hash, Hasher, HasherBase, TransactionSigningHash, Transaction
33
use std::cell::Cell;
44
use std::sync::Arc;
55

6-
use crate::{
7-
subnets::SUBNETWORK_ID_NATIVE,
8-
tx::{ScriptPublicKey, Transaction, TransactionOutpoint, TransactionOutput, VerifiableTransaction},
9-
};
6+
use crate::tx::{ScriptPublicKey, Transaction, TransactionOutpoint, TransactionOutput, VerifiableTransaction};
107

118
use super::{sighash_type::SigHashType, HasherExtensions};
129

@@ -19,6 +16,7 @@ pub struct SigHashReusedValuesUnsync {
1916
sequences_hash: Cell<Option<Hash>>,
2017
sig_op_counts_hash: Cell<Option<Hash>>,
2118
outputs_hash: Cell<Option<Hash>>,
19+
payload_hash: Cell<Option<Hash>>,
2220
}
2321

2422
impl SigHashReusedValuesUnsync {
@@ -33,6 +31,7 @@ pub struct SigHashReusedValuesSync {
3331
sequences_hash: ArcSwapOption<Hash>,
3432
sig_op_counts_hash: ArcSwapOption<Hash>,
3533
outputs_hash: ArcSwapOption<Hash>,
34+
payload_hash: ArcSwapOption<Hash>,
3635
}
3736

3837
impl SigHashReusedValuesSync {
@@ -46,6 +45,7 @@ pub trait SigHashReusedValues {
4645
fn sequences_hash(&self, set: impl Fn() -> Hash) -> Hash;
4746
fn sig_op_counts_hash(&self, set: impl Fn() -> Hash) -> Hash;
4847
fn outputs_hash(&self, set: impl Fn() -> Hash) -> Hash;
48+
fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash;
4949
}
5050

5151
impl<T: SigHashReusedValues> SigHashReusedValues for Arc<T> {
@@ -64,6 +64,10 @@ impl<T: SigHashReusedValues> SigHashReusedValues for Arc<T> {
6464
fn outputs_hash(&self, set: impl Fn() -> Hash) -> Hash {
6565
self.as_ref().outputs_hash(set)
6666
}
67+
68+
fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash {
69+
self.as_ref().outputs_hash(set)
70+
}
6771
}
6872

6973
impl SigHashReusedValues for SigHashReusedValuesUnsync {
@@ -98,6 +102,14 @@ impl SigHashReusedValues for SigHashReusedValuesUnsync {
98102
hash
99103
})
100104
}
105+
106+
fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash {
107+
self.payload_hash.get().unwrap_or_else(|| {
108+
let hash = set();
109+
self.payload_hash.set(Some(hash));
110+
hash
111+
})
112+
}
101113
}
102114

103115
impl SigHashReusedValues for SigHashReusedValuesSync {
@@ -136,6 +148,15 @@ impl SigHashReusedValues for SigHashReusedValuesSync {
136148
self.outputs_hash.rcu(|_| Arc::new(hash));
137149
hash
138150
}
151+
152+
fn payload_hash(&self, set: impl Fn() -> Hash) -> Hash {
153+
if let Some(value) = self.payload_hash.load().as_ref() {
154+
return **value;
155+
}
156+
let hash = set();
157+
self.payload_hash.rcu(|_| Arc::new(hash));
158+
hash
159+
}
139160
}
140161

141162
pub fn previous_outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &impl SigHashReusedValues) -> Hash {
@@ -182,17 +203,17 @@ pub fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_value
182203
reused_values.sig_op_counts_hash(hash)
183204
}
184205

185-
pub fn payload_hash(tx: &Transaction) -> Hash {
186-
if tx.subnetwork_id == SUBNETWORK_ID_NATIVE {
187-
return ZERO_HASH;
188-
}
206+
pub fn payload_hash(tx: &Transaction, reused_values: &impl SigHashReusedValues) -> Hash {
207+
let hash = || {
208+
if tx.subnetwork_id.is_native() && tx.payload.is_empty() {
209+
return ZERO_HASH;
210+
}
189211

190-
// TODO: Right now this branch will never be executed, since payload is disabled
191-
// for all non coinbase transactions. Once payload is enabled, the payload hash
192-
// should be cached to make it cost O(1) instead of O(tx.inputs.len()).
193-
let mut hasher = TransactionSigningHash::new();
194-
hasher.write_var_bytes(&tx.payload);
195-
hasher.finalize()
212+
let mut hasher = TransactionSigningHash::new();
213+
hasher.write_var_bytes(&tx.payload);
214+
hasher.finalize()
215+
};
216+
reused_values.payload_hash(hash)
196217
}
197218

198219
pub fn outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &impl SigHashReusedValues, input_index: usize) -> Hash {
@@ -260,7 +281,7 @@ pub fn calc_schnorr_signature_hash(
260281
.write_u64(tx.lock_time)
261282
.update(&tx.subnetwork_id)
262283
.write_u64(tx.gas)
263-
.update(payload_hash(tx))
284+
.update(payload_hash(tx, reused_values))
264285
.write_u8(hash_type.to_u8());
265286
hasher.finalize()
266287
}
@@ -285,7 +306,7 @@ mod tests {
285306

286307
use crate::{
287308
hashing::sighash_type::{SIG_HASH_ALL, SIG_HASH_ANY_ONE_CAN_PAY, SIG_HASH_NONE, SIG_HASH_SINGLE},
288-
subnets::SubnetworkId,
309+
subnets::{SubnetworkId, SUBNETWORK_ID_NATIVE},
289310
tx::{PopulatedTransaction, Transaction, TransactionId, TransactionInput, UtxoEntry},
290311
};
291312

@@ -608,6 +629,14 @@ mod tests {
608629
action: ModifyAction::NoAction,
609630
expected_hash: "846689131fb08b77f83af1d3901076732ef09d3f8fdff945be89aa4300562e5f", // should change the hash
610631
},
632+
TestVector {
633+
name: "native-all-0-modify-payload",
634+
populated_tx: &native_populated_tx,
635+
hash_type: SIG_HASH_ALL,
636+
input_index: 0,
637+
action: ModifyAction::Payload,
638+
expected_hash: "72ea6c2871e0f44499f1c2b556f265d9424bfea67cca9cb343b4b040ead65525", // should change the hash
639+
},
611640
// subnetwork transaction
612641
TestVector {
613642
name: "subnetwork-all-0",

consensus/core/src/hashing/tx.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ mod tests {
157157
expected_hash: "31da267d5c34f0740c77b8c9ebde0845a01179ec68074578227b804bac306361",
158158
});
159159

160+
// Test #8, same as 7 but with a non-zero payload. The test checks id and hash are affected by payload change
161+
tests.push(Test {
162+
tx: Transaction::new(2, inputs.clone(), outputs.clone(), 54, subnets::SUBNETWORK_ID_REGISTRY, 3, vec![1, 2, 3]),
163+
expected_id: "1f18b18ab004ff1b44dd915554b486d64d7ebc02c054e867cc44e3d746e80b3b",
164+
expected_hash: "a2029ebd66d29d41aa7b0c40230c1bfa7fe8e026fb44b7815dda4e991b9a5fad",
165+
});
166+
160167
for (i, test) in tests.iter().enumerate() {
161168
assert_eq!(test.tx.id(), Hash::from_str(test.expected_id).unwrap(), "transaction id failed for test {}", i + 1);
162169
assert_eq!(

consensus/src/consensus/services.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ impl ConsensusServices {
147147
mass_calculator.clone(),
148148
params.storage_mass_activation,
149149
params.kip10_activation,
150+
params.payload_activation,
150151
);
151152

152153
let pruning_point_manager = PruningPointManager::new(

consensus/src/pipeline/body_processor/body_validation_in_context.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use kaspa_consensus_core::block::Block;
88
use kaspa_database::prelude::StoreResultExtensions;
99
use kaspa_hashes::Hash;
1010
use kaspa_utils::option::OptionExtensions;
11-
use once_cell::unsync::Lazy;
1211
use std::sync::Arc;
1312

1413
impl BlockBodyProcessor {
@@ -21,27 +20,17 @@ impl BlockBodyProcessor {
2120
fn check_block_transactions_in_context(self: &Arc<Self>, block: &Block) -> BlockProcessResult<()> {
2221
// Note: This is somewhat expensive during ibd, as it incurs cache misses.
2322

24-
// Use lazy evaluation to avoid unnecessary work, as most of the time we expect the txs not to have lock time.
25-
let lazy_pmt_res =
26-
Lazy::new(|| match self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap()) {
27-
Ok((pmt, pmt_window)) => {
28-
if !self.block_window_cache_for_past_median_time.contains_key(&block.hash()) {
29-
self.block_window_cache_for_past_median_time.insert(block.hash(), pmt_window);
30-
};
31-
Ok(pmt)
32-
}
33-
Err(e) => Err(e),
34-
});
23+
let pmt = {
24+
let (pmt, pmt_window) = self.window_manager.calc_past_median_time(&self.ghostdag_store.get_data(block.hash()).unwrap())?;
25+
if !self.block_window_cache_for_past_median_time.contains_key(&block.hash()) {
26+
self.block_window_cache_for_past_median_time.insert(block.hash(), pmt_window);
27+
};
28+
pmt
29+
};
3530

3631
for tx in block.transactions.iter() {
37-
// Quick check to avoid the expensive Lazy eval during ibd (in most cases).
38-
// TODO: refactor this and avoid classifying the tx lock outside of the transaction validator.
39-
if tx.lock_time != 0 {
40-
if let Err(e) =
41-
self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, (*lazy_pmt_res).clone()?)
42-
{
43-
return Err(RuleError::TxInContextFailed(tx.id(), e));
44-
};
32+
if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, pmt) {
33+
return Err(RuleError::TxInContextFailed(tx.id(), e));
4534
};
4635
}
4736
Ok(())

0 commit comments

Comments
 (0)