diff --git a/consensus/benches/check_scripts.rs b/consensus/benches/check_scripts.rs index 6462e04e49..772d2c9070 100644 --- a/consensus/benches/check_scripts.rs +++ b/consensus/benches/check_scripts.rs @@ -89,7 +89,7 @@ fn benchmark_check_scripts(c: &mut Criterion) { let cache = Cache::new(inputs_count as u64); b.iter(|| { cache.clear(); - check_scripts_sequential(black_box(&cache), black_box(&tx.as_verifiable()), false).unwrap(); + check_scripts_sequential(black_box(&cache), black_box(&tx.as_verifiable()), false, false).unwrap(); }) }); @@ -98,7 +98,7 @@ fn benchmark_check_scripts(c: &mut Criterion) { 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(); + check_scripts_par_iter(black_box(&cache), black_box(&tx.as_verifiable()), false, false).unwrap(); }) }); @@ -110,8 +110,14 @@ fn benchmark_check_scripts(c: &mut Criterion) { let cache = Cache::new(inputs_count as u64); b.iter(|| { cache.clear(); - check_scripts_par_iter_pool(black_box(&cache), black_box(&tx.as_verifiable()), black_box(&pool), false) - .unwrap(); + check_scripts_par_iter_pool( + black_box(&cache), + black_box(&tx.as_verifiable()), + black_box(&pool), + false, + false, + ) + .unwrap(); }) }); } @@ -147,7 +153,7 @@ fn benchmark_check_scripts_with_payload(c: &mut Criterion) { 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(); + check_scripts_par_iter(black_box(&cache), black_box(&tx.as_verifiable()), false, false).unwrap(); }) }); } diff --git a/consensus/core/src/config/params.rs b/consensus/core/src/config/params.rs index 68bede6482..b0ab02e98e 100644 --- a/consensus/core/src/config/params.rs +++ b/consensus/core/src/config/params.rs @@ -133,6 +133,7 @@ pub struct Params { /// Activation rules for when to enable using the payload field in transactions pub payload_activation: ForkActivation, + pub runtime_sig_op_counting: ForkActivation, } fn unix_now() -> u64 { @@ -411,6 +412,7 @@ pub const MAINNET_PARAMS: Params = Params { pruning_proof_m: 1000, payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), }; pub const TESTNET_PARAMS: Params = Params { @@ -476,6 +478,7 @@ pub const TESTNET_PARAMS: Params = Params { pruning_proof_m: 1000, payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), }; pub const TESTNET11_PARAMS: Params = Params { @@ -539,6 +542,8 @@ pub const TESTNET11_PARAMS: Params = Params { skip_proof_of_work: false, max_block_level: 250, + + runtime_sig_op_counting: ForkActivation::never(), }; pub const SIMNET_PARAMS: Params = Params { @@ -595,6 +600,7 @@ pub const SIMNET_PARAMS: Params = Params { max_block_level: 250, payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), }; pub const DEVNET_PARAMS: Params = Params { @@ -654,4 +660,5 @@ pub const DEVNET_PARAMS: Params = Params { pruning_proof_m: 1000, payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), }; diff --git a/consensus/src/consensus/services.rs b/consensus/src/consensus/services.rs index 06abb4e0bb..2ff7578e1d 100644 --- a/consensus/src/consensus/services.rs +++ b/consensus/src/consensus/services.rs @@ -148,6 +148,7 @@ impl ConsensusServices { params.storage_mass_activation, params.kip10_activation, params.payload_activation, + params.runtime_sig_op_counting, ); let pruning_point_manager = PruningPointManager::new( diff --git a/consensus/src/processes/transaction_validator/mod.rs b/consensus/src/processes/transaction_validator/mod.rs index b4a946c2ff..519a196f82 100644 --- a/consensus/src/processes/transaction_validator/mod.rs +++ b/consensus/src/processes/transaction_validator/mod.rs @@ -31,6 +31,7 @@ pub struct TransactionValidator { /// KIP-10 hardfork DAA score kip10_activation: ForkActivation, payload_activation: ForkActivation, + runtime_sig_op_counting: ForkActivation, } impl TransactionValidator { @@ -48,6 +49,7 @@ impl TransactionValidator { storage_mass_activation: ForkActivation, kip10_activation: ForkActivation, payload_activation: ForkActivation, + runtime_sig_op_counting: ForkActivation, ) -> Self { Self { max_tx_inputs, @@ -62,6 +64,7 @@ impl TransactionValidator { storage_mass_activation, kip10_activation, payload_activation, + runtime_sig_op_counting, } } @@ -88,6 +91,7 @@ impl TransactionValidator { storage_mass_activation: ForkActivation::never(), kip10_activation: ForkActivation::never(), payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), } } } diff --git a/consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs b/consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs index 854c46de08..d8e0951a6f 100644 --- a/consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs +++ b/consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs @@ -59,7 +59,9 @@ impl TransactionValidator { match flags { TxValidationFlags::Full | TxValidationFlags::SkipMassCheck => { - Self::check_sig_op_counts(tx)?; + if !self.runtime_sig_op_counting.is_active(pov_daa_score) { + Self::check_sig_op_counts(tx)?; + } self.check_scripts(tx, pov_daa_score)?; } TxValidationFlags::SkipScriptChecks => {} @@ -171,7 +173,12 @@ impl TransactionValidator { } pub fn check_scripts(&self, tx: &(impl VerifiableTransaction + Sync), pov_daa_score: u64) -> TxResult<()> { - check_scripts(&self.sig_cache, tx, self.kip10_activation.is_active(pov_daa_score)) + check_scripts( + &self.sig_cache, + tx, + self.kip10_activation.is_active(pov_daa_score), + self.runtime_sig_op_counting.is_active(pov_daa_score), + ) } } @@ -179,11 +186,12 @@ pub fn check_scripts( sig_cache: &Cache, tx: &(impl VerifiableTransaction + Sync), kip10_enabled: bool, + runtime_sig_op_counting: bool, ) -> TxResult<()> { if tx.inputs().len() > CHECK_SCRIPTS_PARALLELISM_THRESHOLD { - check_scripts_par_iter(sig_cache, tx, kip10_enabled) + check_scripts_par_iter(sig_cache, tx, kip10_enabled, runtime_sig_op_counting) } else { - check_scripts_sequential(sig_cache, tx, kip10_enabled) + check_scripts_sequential(sig_cache, tx, kip10_enabled, runtime_sig_op_counting) } } @@ -191,10 +199,11 @@ pub fn check_scripts_sequential( sig_cache: &Cache, tx: &impl VerifiableTransaction, kip10_enabled: bool, + runtime_sig_op_counting: bool, ) -> TxResult<()> { let reused_values = SigHashReusedValuesUnsync::new(); for (i, (input, entry)) in tx.populated_inputs().enumerate() { - TxScriptEngine::from_transaction_input(tx, input, i, entry, &reused_values, sig_cache, kip10_enabled) + TxScriptEngine::from_transaction_input(tx, input, i, entry, &reused_values, sig_cache, kip10_enabled, runtime_sig_op_counting) .execute() .map_err(|err| map_script_err(err, input))?; } @@ -205,11 +214,12 @@ pub fn check_scripts_par_iter( sig_cache: &Cache, tx: &(impl VerifiableTransaction + Sync), kip10_enabled: bool, + runtime_sig_op_counting: bool, ) -> TxResult<()> { let reused_values = SigHashReusedValuesSync::new(); (0..tx.inputs().len()).into_par_iter().try_for_each(|idx| { let (input, utxo) = tx.populated_input(idx); - TxScriptEngine::from_transaction_input(tx, input, idx, utxo, &reused_values, sig_cache, kip10_enabled) + TxScriptEngine::from_transaction_input(tx, input, idx, utxo, &reused_values, sig_cache, kip10_enabled, runtime_sig_op_counting) .execute() .map_err(|err| map_script_err(err, input)) }) @@ -220,8 +230,9 @@ pub fn check_scripts_par_iter_pool( tx: &(impl VerifiableTransaction + Sync), pool: &ThreadPool, kip10_enabled: bool, + runtime_sig_op_counting: bool, ) -> TxResult<()> { - pool.install(|| check_scripts_par_iter(sig_cache, tx, kip10_enabled)) + pool.install(|| check_scripts_par_iter(sig_cache, tx, kip10_enabled, runtime_sig_op_counting)) } fn map_script_err(script_err: TxScriptError, input: &TransactionInput) -> TxRuleError { diff --git a/crypto/txscript/errors/src/lib.rs b/crypto/txscript/errors/src/lib.rs index b16ec4cead..43d5f1151f 100644 --- a/crypto/txscript/errors/src/lib.rs +++ b/crypto/txscript/errors/src/lib.rs @@ -73,6 +73,8 @@ pub enum TxScriptError { InvalidOutputIndex(i32, usize), #[error(transparent)] Serialization(#[from] SerializationError), + #[error("sig op count exceed passed limit of {1}")] + ExceededSigOpLimit(u16, u8), } #[derive(Error, PartialEq, Eq, Debug, Clone, Copy)] diff --git a/crypto/txscript/examples/kip-10.rs b/crypto/txscript/examples/kip-10.rs index 4077385a72..e6c887340d 100644 --- a/crypto/txscript/examples/kip-10.rs +++ b/crypto/txscript/examples/kip-10.rs @@ -126,7 +126,8 @@ fn threshold_scenario() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[STANDARD] Owner branch execution successful"); } @@ -136,7 +137,8 @@ fn threshold_scenario() -> ScriptBuilderResult<()> { println!("[STANDARD] Checking borrower branch"); tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&script)?.drain(); let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[STANDARD] Borrower branch execution successful"); } @@ -147,7 +149,8 @@ fn threshold_scenario() -> ScriptBuilderResult<()> { // Less than threshold tx.outputs[0].value -= 1; let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Err(EvalFalse)); println!("[STANDARD] Borrower branch with threshold not reached failed as expected"); } @@ -295,7 +298,8 @@ fn threshold_scenario_limited_one_time() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[ONE-TIME] Owner branch execution successful"); } @@ -305,7 +309,8 @@ fn threshold_scenario_limited_one_time() -> ScriptBuilderResult<()> { println!("[ONE-TIME] Checking borrower branch"); tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&script)?.drain(); let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[ONE-TIME] Borrower branch execution successful"); } @@ -316,7 +321,8 @@ fn threshold_scenario_limited_one_time() -> ScriptBuilderResult<()> { // Less than threshold tx.outputs[0].value -= 1; let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Err(EvalFalse)); println!("[ONE-TIME] Borrower branch with threshold not reached failed as expected"); } @@ -346,6 +352,7 @@ fn threshold_scenario_limited_one_time() -> ScriptBuilderResult<()> { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(VerifyError)); println!("[ONE-TIME] Borrower branch with output going to wrong address failed as expected"); @@ -455,7 +462,8 @@ fn threshold_scenario_limited_2_times() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[TWO-TIMES] Owner branch execution successful"); } @@ -465,7 +473,8 @@ fn threshold_scenario_limited_2_times() -> ScriptBuilderResult<()> { println!("[TWO-TIMES] Checking borrower branch (first borrowing)"); tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&two_times_script)?.drain(); let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[TWO-TIMES] Borrower branch (first borrowing) execution successful"); } @@ -476,7 +485,8 @@ fn threshold_scenario_limited_2_times() -> ScriptBuilderResult<()> { // Less than threshold tx.outputs[0].value -= 1; let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Err(EvalFalse)); println!("[TWO-TIMES] Borrower branch with threshold not reached failed as expected"); } @@ -506,6 +516,7 @@ fn threshold_scenario_limited_2_times() -> ScriptBuilderResult<()> { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(VerifyError)); println!("[TWO-TIMES] Borrower branch with output going to wrong address failed as expected"); @@ -617,7 +628,8 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[SHARED-SECRET] Owner branch execution successful"); } @@ -635,7 +647,8 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Ok(())); println!("[SHARED-SECRET] Borrower branch with correct shared secret execution successful"); } @@ -653,7 +666,8 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> { } let tx = tx.as_verifiable(); - let mut vm = TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, true, false); assert_eq!(vm.execute(), Err(VerifyError)); println!("[SHARED-SECRET] Borrower branch with incorrect secret failed as expected"); } diff --git a/crypto/txscript/src/lib.rs b/crypto/txscript/src/lib.rs index 637a10aff2..d9088d3d16 100644 --- a/crypto/txscript/src/lib.rs +++ b/crypto/txscript/src/lib.rs @@ -16,9 +16,14 @@ use crate::caches::Cache; use crate::data_stack::{DataStack, Stack}; use crate::opcodes::{deserialize_next_opcode, OpCodeImplementation}; use itertools::Itertools; -use kaspa_consensus_core::hashing::sighash::{calc_ecdsa_signature_hash, calc_schnorr_signature_hash, SigHashReusedValues}; +use kaspa_consensus_core::hashing::sighash::{ + calc_ecdsa_signature_hash, calc_schnorr_signature_hash, SigHashReusedValues, SigHashReusedValuesUnsync, +}; use kaspa_consensus_core::hashing::sighash_type::SigHashType; -use kaspa_consensus_core::tx::{ScriptPublicKey, TransactionInput, UtxoEntry, VerifiableTransaction}; +use kaspa_consensus_core::tx::{ + PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionOutpoint, UtxoEntry, + VerifiableTransaction, +}; use kaspa_txscript_errors::TxScriptError; use log::trace; use opcodes::codes::OpReturn; @@ -72,6 +77,18 @@ enum ScriptSource<'a, T: VerifiableTransaction> { StandAloneScripts(Vec<&'a [u8]>), } +/// RuntimeSigOpCounter represents the state tracking of signature operations during script execution. +/// Unlike the static counting approach which counts all possible signature operations, +/// this tracks only the actually executed signature operations, leading to more accurate +/// mass calculations and potentially lower fees for conditional scripts. +#[derive(Debug, Clone, Copy)] +pub struct RuntimeSigOpCounter { + /// Maximum number of signature operations allowed for this input + pub sig_op_limit: u8, + /// Remaining signature operations that can be executed + pub sig_op_remaining: u8, +} + pub struct TxScriptEngine<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> { dstack: Stack, astack: Stack, @@ -86,6 +103,7 @@ pub struct TxScriptEngine<'a, T: VerifiableTransaction, Reused: SigHashReusedVal num_ops: i32, kip10_enabled: bool, + runtime_sig_op_counting: Option, } fn parse_script( @@ -94,6 +112,36 @@ fn parse_script( script.iter().batching(|it| deserialize_next_opcode(it)) } +pub fn get_sig_op_count_via_simulation( + signature_script: &[u8], + prev_script_public_key: &ScriptPublicKey, + kip10_enabled: bool, +) -> Result { + // Ensure encapsulation of variables (no leaking between tests) + let mock_input = TransactionInput { + previous_outpoint: TransactionOutpoint { + transaction_id: TransactionId::from_bytes([ + 0xc9, 0x97, 0xa5, 0xe5, 0x6e, 0x10, 0x41, 0x02, 0xfa, 0x20, 0x9c, 0x6a, 0x85, 0x2d, 0xd9, 0x06, 0x60, 0xa2, 0x0b, + 0x2d, 0x9c, 0x35, 0x24, 0x23, 0xed, 0xce, 0x25, 0x85, 0x7f, 0xcd, 0x37, 0x04, + ]), + index: 0, + }, + signature_script: signature_script.to_vec(), + sequence: 4294967295, + sig_op_count: 255, + }; + + let tx = Transaction::new(1, vec![mock_input.clone()], vec![], 0, Default::default(), 0, vec![]); + let utxo_entry = UtxoEntry::new(0, prev_script_public_key.clone(), 0, false); + let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &mock_input, 0, &utxo_entry, &reused_values, &sig_cache, kip10_enabled, true); + vm.execute()?; + Ok(vm.sig_op_required().unwrap()) +} + #[must_use] pub fn get_sig_op_count( signature_script: &[u8], @@ -165,9 +213,18 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' cond_stack: vec![], num_ops: 0, kip10_enabled, + runtime_sig_op_counting: None, } } + /// Returns the number of signature operations that were actually required during script execution. + /// Must only be called after script execution completes successfully. + /// + /// Returns the difference between the input's sig_op_limit and remaining sig ops. + pub fn sig_op_required(&self) -> Option { + self.runtime_sig_op_counting.map(|RuntimeSigOpCounter { sig_op_limit, sig_op_remaining }| sig_op_limit - sig_op_remaining) + } + /// Creates a new Script Engine for validating transaction input. /// /// # Arguments @@ -192,6 +249,7 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' reused_values: &'a Reused, sig_cache: &'a Cache, kip10_enabled: bool, + runtime_sig_op_counting: bool, ) -> Self { let script_public_key = utxo_entry.script_public_key.script(); // The script_public_key in P2SH is just validating the hash on the OpMultiSig script @@ -207,6 +265,8 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' cond_stack: Default::default(), num_ops: 0, kip10_enabled, + runtime_sig_op_counting: runtime_sig_op_counting + .then_some(RuntimeSigOpCounter { sig_op_limit: input.sig_op_count, sig_op_remaining: input.sig_op_count }), } } @@ -225,6 +285,7 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' cond_stack: Default::default(), num_ops: 0, kip10_enabled, + runtime_sig_op_counting: None, // todo? } } @@ -405,8 +466,13 @@ impl<'a, T: VerifiableTransaction, Reused: SigHashReusedValues> TxScriptEngine<' } else if num_sigs > num_keys { return Err(TxScriptError::InvalidSignatureCount(format!("more signatures than pubkeys {num_sigs} > {num_keys}"))); } + if let Some(RuntimeSigOpCounter { sig_op_remaining, sig_op_limit }) = &mut self.runtime_sig_op_counting { + let num_sigs = + num_sigs.try_into().map_err(|_| TxScriptError::NumberTooBig("Signatures count mustn't exceed 255".to_string()))?; + *sig_op_remaining = + sig_op_remaining.checked_sub(num_sigs).ok_or(TxScriptError::ExceededSigOpLimit(num_sigs as u16, *sig_op_limit))?; + } let num_sigs = num_sigs as usize; - let signatures = match self.dstack.len() >= num_sigs { true => self.dstack.split_off(self.dstack.len() - num_sigs), false => return Err(TxScriptError::InvalidStackOperation(num_sigs, self.dstack.len())), @@ -550,12 +616,17 @@ impl SpkEncoding for ScriptPublicKey { mod tests { use std::iter::once; - use crate::opcodes::codes::{OpBlake2b, OpCheckSig, OpData1, OpData2, OpData32, OpDup, OpEqual, OpPushData1, OpTrue}; + use crate::opcodes::codes::{ + OpBlake2b, OpCheckMultiSig, OpCheckSig, OpCheckSigECDSA, OpCheckSigVerify, OpData1, OpData2, OpData32, OpDup, OpEndIf, + OpEqual, OpFalse, OpIf, OpPushData1, OpTrue, OpVerify, + }; use super::*; + use crate::script_builder::{ScriptBuilder, ScriptBuilderResult}; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; + use kaspa_consensus_core::hashing::sighash_type::SIG_HASH_ALL; use kaspa_consensus_core::tx::{ - PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionOutpoint, TransactionOutput, + MutableTransaction, PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionOutpoint, TransactionOutput, }; use smallvec::SmallVec; @@ -611,16 +682,19 @@ mod tests { let populated_tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]); [false, true].into_iter().for_each(|kip10_enabled| { - let mut vm = TxScriptEngine::from_transaction_input( - &populated_tx, - &input, - 0, - &utxo_entry, - &reused_values, - &sig_cache, - kip10_enabled, - ); - assert_eq!(vm.execute(), test.expected_result); + [false, true].into_iter().for_each(|runtime_sig_op_counting| { + let mut vm = TxScriptEngine::from_transaction_input( + &populated_tx, + &input, + 0, + &utxo_entry, + &reused_values, + &sig_cache, + kip10_enabled, + runtime_sig_op_counting, + ); + assert_eq!(vm.execute(), test.expected_result); + }); }); } } @@ -973,6 +1047,231 @@ mod tests { ); } } + + #[derive(Clone)] + struct SignatureData { + signature: Vec, + public_key: Vec, + } + + enum SignatureScriptBuilder { + Single(SignatureData), + Multi(Vec), + Mixed(Vec), + None, // For tests that don't need signatures + } + + type SigBuilder = Box, &SigHashReusedValuesUnsync) -> SignatureScriptBuilder>; + type ScriptBuilderFn = Box ScriptBuilderResult<&mut ScriptBuilder>>; + + struct TestCase { + name: &'static str, + script_builder: ScriptBuilderFn, + sig_builder: SigBuilder, + expected_sig_ops: u8, + sig_op_limit: u8, + should_pass: bool, + } + + impl SignatureScriptBuilder { + fn build(self, script: &[u8]) -> ScriptBuilderResult> { + let mut builder = ScriptBuilder::new(); + + match self { + SignatureScriptBuilder::Single(sig_data) => { + builder.add_data(&sig_data.signature)?; + builder.add_data(&sig_data.public_key)?; + } + SignatureScriptBuilder::Multi(sig_data_vec) => { + for sig_data in sig_data_vec { + builder.add_data(&sig_data.signature)?; + } + } + SignatureScriptBuilder::Mixed(sig_data_vec) => { + for sig_data in sig_data_vec { + builder.add_data(&sig_data.signature)?; + builder.add_data(&sig_data.public_key)?; + } + } + SignatureScriptBuilder::None => {} + } + + builder.add_data(script)?; + Ok(builder.drain()) + } + } + + #[test] + fn test_runtime_sig_op_count() -> ScriptBuilderResult<()> { + // Setup keys and test environment + let secp = secp256k1::Secp256k1::new(); + let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng()); + let keypair = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &secret_key.secret_bytes()).unwrap(); + + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + + // Helper functions for creating signatures + let create_schnorr_signature = move |tx: &MutableTransaction, reused: &SigHashReusedValuesUnsync| { + let hash = calc_schnorr_signature_hash(&tx.as_verifiable(), 0, SIG_HASH_ALL, reused); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + let sig = keypair.sign_schnorr(msg); + let mut signature = sig.as_ref().to_vec(); + signature.push(SIG_HASH_ALL.to_u8()); + SignatureData { signature, public_key: keypair.x_only_public_key().0.serialize().to_vec() } + }; + + let create_ecdsa_signature = move |tx: &MutableTransaction, reused: &SigHashReusedValuesUnsync| { + let hash = calc_ecdsa_signature_hash(&tx.as_verifiable(), 0, SIG_HASH_ALL, reused); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + let sig = keypair.secret_key().sign_ecdsa(msg); + let mut signature = sig.serialize_compact().to_vec(); + signature.push(SIG_HASH_ALL.to_u8()); + SignatureData { signature, public_key: keypair.public_key().serialize().to_vec() } + }; + + let test_cases = vec![ + // Basic Schnorr CheckSig + TestCase { + name: "Basic Schnorr CheckSig", + script_builder: Box::new(|sb| sb.add_op(OpCheckSig)), + sig_builder: Box::new(move |tx, reused| SignatureScriptBuilder::Single(create_schnorr_signature(tx, reused))), + expected_sig_ops: 1, + sig_op_limit: 1, + should_pass: true, + }, + // Basic ECDSA CheckSig + TestCase { + name: "Basic ECDSA CheckSig", + script_builder: Box::new(|sb| sb.add_op(OpCheckSigECDSA)), + sig_builder: Box::new(move |tx, reused| SignatureScriptBuilder::Single(create_ecdsa_signature(tx, reused))), + expected_sig_ops: 1, + sig_op_limit: 1, + should_pass: true, + }, + // Mixed Schnorr and ECDSA + TestCase { + name: "Mixed Schnorr and ECDSA", + script_builder: Box::new(|sb| sb.add_op(OpCheckSigVerify)?.add_op(OpCheckSigECDSA)), + sig_builder: Box::new(move |tx, reused| { + SignatureScriptBuilder::Mixed(vec![create_ecdsa_signature(tx, reused), create_schnorr_signature(tx, reused)]) + }), + expected_sig_ops: 2, + sig_op_limit: 2, + should_pass: true, + }, + // 2-of-3 MultiSig test case + TestCase { + name: "2-of-3 MultiSig", + script_builder: Box::new(move |sb| { + sb.add_i64(2)? + .add_data(&keypair.x_only_public_key().0.serialize())? + .add_data(&keypair.x_only_public_key().0.serialize())? + .add_data(&keypair.x_only_public_key().0.serialize())? + .add_i64(3)? + .add_op(OpCheckMultiSig) + }), + sig_builder: Box::new(move |tx, reused| { + let sig = create_schnorr_signature(tx, reused); + SignatureScriptBuilder::Multi(vec![sig.clone(), sig]) + }), + expected_sig_ops: 2, + sig_op_limit: 2, + should_pass: true, + }, + TestCase { + name: "Mixed Schnorr and ECDSA", + script_builder: Box::new(|sb| sb.add_op(OpCheckSigVerify)?.add_op(OpCheckSigECDSA)), + sig_builder: Box::new(move |tx, reused| { + SignatureScriptBuilder::Mixed(vec![create_ecdsa_signature(tx, reused), create_schnorr_signature(tx, reused)]) + }), + expected_sig_ops: 2, + sig_op_limit: 1, + should_pass: false, + }, + // Conditional execution with sig ops + TestCase { + name: "Conditional sig ops", + script_builder: Box::new(|sb| sb.add_op(OpTrue)?.add_op(OpIf)?.add_op(OpCheckSigECDSA)?.add_op(OpEndIf)), + sig_builder: Box::new(move |tx, reused| SignatureScriptBuilder::Single(create_ecdsa_signature(tx, reused))), + expected_sig_ops: 1, + sig_op_limit: 1, + should_pass: true, + }, + // Conditional execution with sig ops + TestCase { + name: "Conditional sig ops (false branch)", + script_builder: Box::new(|sb| { + sb.add_op(OpFalse)?.add_op(OpIf)?.add_op(OpCheckSigECDSA)?.add_op(OpVerify)?.add_op(OpEndIf)?.add_op(OpTrue) + }), + sig_builder: Box::new(move |_tx, _reused| SignatureScriptBuilder::None), + expected_sig_ops: 0, + sig_op_limit: 0, + should_pass: true, + }, + ]; + + for test in test_cases { + // Create script + let mut script_builder = ScriptBuilder::new(); + (test.script_builder)(&mut script_builder)?; + let script = script_builder.drain(); + + let script_pub_key = pay_to_script_hash_script(&script); + let utxo_entry = UtxoEntry::new(1000, script_pub_key.clone(), 0, false); + + // Create transaction + let tx = Transaction::new( + 1, + vec![TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::default(), index: 0 }, + signature_script: vec![], + sequence: 0, + sig_op_count: test.sig_op_limit, + }], + vec![], + 0, + Default::default(), + 0, + vec![], + ); + + let mut tx = MutableTransaction::new(tx); + tx.entries = vec![Some(utxo_entry.clone())]; + + // Build signature script + let signature_script = (test.sig_builder)(&tx, &reused_values).build(&script)?; + tx.tx.inputs[0].signature_script = signature_script; + + // Execute script + let tx = tx.as_verifiable(); + let mut vm = + TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &reused_values, &sig_cache, false, true); + + let result = vm.execute().map(|_| vm.sig_op_required().unwrap()); + + match (result, test.should_pass) { + (Ok(count), true) => { + assert_eq!( + count, test.expected_sig_ops, + "{} failed: Expected {} sig ops, got {}", + test.name, test.expected_sig_ops, count + ); + } + (Ok(_), false) => { + panic!("{} should have failed but succeeded", test.name); + } + (Err(err), true) => { + panic!("{} failed but should have succeeded with err: {}", test.name, err); + } + (Err(_), false) => { + // Test correctly failed + } + } + } + + Ok(()) + } } #[cfg(test)] @@ -1019,7 +1318,7 @@ mod bitcoind_tests { TransactionOutpoint::new(TransactionId::default(), 0xffffffffu32), vec![0, 0], MAX_TX_IN_SEQUENCE_NUM, - Default::default(), + MAX_PUB_KEYS_PER_MUTLTISIG as u8, )], vec![TransactionOutput::new(0, script_public_key)], Default::default(), @@ -1034,7 +1333,7 @@ mod bitcoind_tests { TransactionOutpoint::new(coinbase.id(), 0u32), sig_script, MAX_TX_IN_SEQUENCE_NUM, - Default::default(), + MAX_PUB_KEYS_PER_MUTLTISIG as u8, )], vec![TransactionOutput::new(0, Default::default())], Default::default(), @@ -1045,7 +1344,7 @@ mod bitcoind_tests { } impl JsonTestRow { - fn test_row(&self, kip10_enabled: bool) -> Result<(), TestError> { + fn test_row(&self, kip10_enabled: bool, runtime_sig_op_counting: bool) -> Result<(), TestError> { // Parse test to objects let (sig_script, script_pub_key, expected_result) = match self.clone() { JsonTestRow::Test(sig_script, sig_pub_key, _, expected_result) => (sig_script, sig_pub_key, expected_result), @@ -1057,7 +1356,7 @@ mod bitcoind_tests { } }; - let result = Self::run_test(sig_script, script_pub_key, kip10_enabled); + let result = Self::run_test(sig_script, script_pub_key, kip10_enabled, runtime_sig_op_counting); match Self::result_name(result.clone()).contains(&expected_result.as_str()) { true => Ok(()), @@ -1065,7 +1364,12 @@ mod bitcoind_tests { } } - fn run_test(sig_script: String, script_pub_key: String, kip10_enabled: bool) -> Result<(), UnifiedError> { + fn run_test( + sig_script: String, + script_pub_key: String, + kip10_enabled: bool, + runtime_sig_op_counting: bool, + ) -> Result<(), UnifiedError> { let script_sig = opcodes::parse_short_form(sig_script).map_err(UnifiedError::ScriptBuilderError)?; let script_pub_key = ScriptPublicKey::from_vec(0, opcodes::parse_short_form(script_pub_key).map_err(UnifiedError::ScriptBuilderError)?); @@ -1086,6 +1390,7 @@ mod bitcoind_tests { &reused_values, &sig_cache, kip10_enabled, + runtime_sig_op_counting, ); vm.execute().map_err(UnifiedError::TxScriptError) } @@ -1189,16 +1494,18 @@ mod bitcoind_tests { // When KIP-10 is disabled (pre-activation), the new opcodes will return an InvalidOpcode error // and arithmetic is limited to 4 bytes. When enabled, scripts gain full access to transaction // data and 8-byte arithmetic capabilities. - for (file_name, kip10_enabled) in [("script_tests.json", false), ("script_tests-kip10.json", true)] { - let file = - File::open(Path::new(env!("CARGO_MANIFEST_DIR")).join("test-data").join(file_name)).expect("Could not find test file"); - let reader = BufReader::new(file); - - // Read the JSON contents of the file as an instance of `User`. - let tests: Vec = serde_json::from_reader(reader).expect("Failed Parsing {:?}"); - for row in tests { - if let Err(error) = row.test_row(kip10_enabled) { - panic!("Test: {:?} failed for {}: {:?}", row.clone(), file_name, error); + for runtime_sig_op_counting in [false, true] { + for (file_name, kip10_enabled) in [("script_tests.json", false), ("script_tests-kip10.json", true)] { + let file = File::open(Path::new(env!("CARGO_MANIFEST_DIR")).join("test-data").join(file_name)) + .expect("Could not find test file"); + let reader = BufReader::new(file); + + // Read the JSON contents of the file as an instance of `User`. + let tests: Vec = serde_json::from_reader(reader).expect("Failed Parsing {:?}"); + for row in tests { + if let Err(error) = row.test_row(kip10_enabled, runtime_sig_op_counting) { + panic!("Test: {:?} failed for {}: {:?}", row.clone(), file_name, error); + } } } } diff --git a/crypto/txscript/src/opcodes/mod.rs b/crypto/txscript/src/opcodes/mod.rs index 5ee6fbaae1..08b50ee3b6 100644 --- a/crypto/txscript/src/opcodes/mod.rs +++ b/crypto/txscript/src/opcodes/mod.rs @@ -3,8 +3,8 @@ mod macros; use crate::{ data_stack::{DataStack, Kip10I64, OpcodeData}, - ScriptSource, SpkEncoding, TxScriptEngine, TxScriptError, LOCK_TIME_THRESHOLD, MAX_TX_IN_SEQUENCE_NUM, NO_COST_OPCODE, - SEQUENCE_LOCK_TIME_DISABLED, SEQUENCE_LOCK_TIME_MASK, + RuntimeSigOpCounter, ScriptSource, SpkEncoding, TxScriptEngine, TxScriptError, LOCK_TIME_THRESHOLD, MAX_TX_IN_SEQUENCE_NUM, + NO_COST_OPCODE, SEQUENCE_LOCK_TIME_DISABLED, SEQUENCE_LOCK_TIME_MASK, }; use blake2b_simd::Params; use kaspa_consensus_core::hashing::sighash::SigHashReusedValues; @@ -720,6 +720,10 @@ opcode_list! { match sig.pop() { Some(typ) => { let hash_type = SigHashType::from_u8(typ).map_err(|e| TxScriptError::InvalidSigHashType(typ))?; + if let Some(RuntimeSigOpCounter{ sig_op_limit, sig_op_remaining} ) = &mut vm.runtime_sig_op_counting { + *sig_op_remaining = sig_op_remaining.checked_sub(1).ok_or(TxScriptError::ExceededSigOpLimit(*sig_op_limit as u16 + 1, *sig_op_limit))?; + } + match vm.check_ecdsa_signature(hash_type, key.as_slice(), sig.as_slice()) { Ok(valid) => { vm.dstack.push_item(valid)?; @@ -743,6 +747,9 @@ opcode_list! { match sig.pop() { Some(typ) => { let hash_type = SigHashType::from_u8(typ).map_err(|e| TxScriptError::InvalidSigHashType(typ))?; + if let Some(RuntimeSigOpCounter{ sig_op_limit, sig_op_remaining} ) = &mut vm.runtime_sig_op_counting { + *sig_op_remaining = sig_op_remaining.checked_sub(1).ok_or(TxScriptError::ExceededSigOpLimit(*sig_op_limit as u16 + 1, *sig_op_limit))?; + } match vm.check_schnorr_signature(hash_type, key.as_slice(), sig.as_slice()) { Ok(valid) => { vm.dstack.push_item(valid)?; @@ -2854,7 +2861,7 @@ mod test { ] { let mut tx = base_tx.clone(); tx.0.lock_time = tx_lock_time; - let mut vm = TxScriptEngine::from_transaction_input(&tx, &input, 0, &utxo_entry, &reused_values, &sig_cache, false); + let mut vm = TxScriptEngine::from_transaction_input(&tx, &input, 0, &utxo_entry, &reused_values, &sig_cache, false, false); vm.dstack = vec![lock_time.clone()]; match code.execute(&mut vm) { // Message is based on the should_fail values @@ -2896,7 +2903,7 @@ mod test { ] { let mut input = base_input.clone(); input.sequence = tx_sequence; - let mut vm = TxScriptEngine::from_transaction_input(&tx, &input, 0, &utxo_entry, &reused_values, &sig_cache, false); + let mut vm = TxScriptEngine::from_transaction_input(&tx, &input, 0, &utxo_entry, &reused_values, &sig_cache, false, false); vm.dstack = vec![sequence.clone()]; match code.execute(&mut vm) { // Message is based on the should_fail values @@ -3090,6 +3097,7 @@ mod test { &reused_values, &sig_cache, group.kip10_enabled, + false, ); // Check input index opcode first @@ -3348,51 +3356,54 @@ mod test { let sig_cache = Cache::new(10_000); let reused_values = SigHashReusedValuesUnsync::new(); - // Test with KIP-10 enabled and disabled - for kip10_enabled in [true, false] { - let mut vm = TxScriptEngine::from_transaction_input( - &tx, - &tx.inputs()[0], // Use first input - 0, - tx.utxo(0).unwrap(), - &reused_values, - &sig_cache, - kip10_enabled, - ); - - let op_input_count = opcodes::OpTxInputCount::empty().expect("Should accept empty"); - let op_output_count = opcodes::OpTxOutputCount::empty().expect("Should accept empty"); - - if kip10_enabled { - // Test input count - op_input_count.execute(&mut vm).unwrap(); - assert_eq!( - vm.dstack, - vec![ as OpcodeData>::serialize(&(input_count as i64)).unwrap()], - "Input count mismatch for {} inputs", - input_count - ); - vm.dstack.clear(); - - // Test output count - op_output_count.execute(&mut vm).unwrap(); - assert_eq!( - vm.dstack, - vec![ as OpcodeData>::serialize(&(output_count as i64)).unwrap()], - "Output count mismatch for {} outputs", - output_count - ); - vm.dstack.clear(); - } else { - // Test that operations fail when KIP-10 is disabled - assert!( - matches!(op_input_count.execute(&mut vm), Err(TxScriptError::InvalidOpcode(_))), - "OpInputCount should fail when KIP-10 is disabled" - ); - assert!( - matches!(op_output_count.execute(&mut vm), Err(TxScriptError::InvalidOpcode(_))), - "OpOutputCount should fail when KIP-10 is disabled" + for runtime_sig_op_counting in [true, false] { + // Test with KIP-10 enabled and disabled + for kip10_enabled in [true, false] { + let mut vm = TxScriptEngine::from_transaction_input( + &tx, + &tx.inputs()[0], // Use first input + 0, + tx.utxo(0).unwrap(), + &reused_values, + &sig_cache, + kip10_enabled, + runtime_sig_op_counting, ); + + let op_input_count = opcodes::OpTxInputCount::empty().expect("Should accept empty"); + let op_output_count = opcodes::OpTxOutputCount::empty().expect("Should accept empty"); + + if kip10_enabled { + // Test input count + op_input_count.execute(&mut vm).unwrap(); + assert_eq!( + vm.dstack, + vec![ as OpcodeData>::serialize(&(input_count as i64)).unwrap()], + "Input count mismatch for {} inputs", + input_count + ); + vm.dstack.clear(); + + // Test output count + op_output_count.execute(&mut vm).unwrap(); + assert_eq!( + vm.dstack, + vec![ as OpcodeData>::serialize(&(output_count as i64)).unwrap()], + "Output count mismatch for {} outputs", + output_count + ); + vm.dstack.clear(); + } else { + // Test that operations fail when KIP-10 is disabled + assert!( + matches!(op_input_count.execute(&mut vm), Err(TxScriptError::InvalidOpcode(_))), + "OpInputCount should fail when KIP-10 is disabled" + ); + assert!( + matches!(op_output_count.execute(&mut vm), Err(TxScriptError::InvalidOpcode(_))), + "OpOutputCount should fail when KIP-10 is disabled" + ); + } } } } @@ -3438,6 +3449,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3462,6 +3474,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(TxScriptError::EvalFalse)); @@ -3502,6 +3515,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3528,6 +3542,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(TxScriptError::EvalFalse)); @@ -3549,8 +3564,16 @@ mod test { tx.tx.inputs[0].signature_script = ScriptBuilder::new().add_data(&redeem_script).unwrap().drain(); let tx = tx.as_verifiable(); - let mut vm = - TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, tx.utxo(0).unwrap(), &reused_values, &sig_cache, true); + let mut vm = TxScriptEngine::from_transaction_input( + &tx, + &tx.inputs()[0], + 0, + tx.utxo(0).unwrap(), + &reused_values, + &sig_cache, + true, + false, + ); // OpInputSpk should push input's SPK onto stack, making it non-empty assert_eq!(vm.execute(), Ok(())); @@ -3573,8 +3596,16 @@ mod test { tx.tx.inputs[0].signature_script = ScriptBuilder::new().add_data(&redeem_script).unwrap().drain(); let tx = tx.as_verifiable(); - let mut vm = - TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, tx.utxo(0).unwrap(), &reused_values, &sig_cache, true); + let mut vm = TxScriptEngine::from_transaction_input( + &tx, + &tx.inputs()[0], + 0, + tx.utxo(0).unwrap(), + &reused_values, + &sig_cache, + true, + false, + ); // Should succeed because the SPKs are different assert_eq!(vm.execute(), Ok(())); @@ -3598,8 +3629,16 @@ mod test { tx.tx.inputs[0].signature_script = ScriptBuilder::new().add_data(&redeem_script).unwrap().drain(); let tx = tx.as_verifiable(); - let mut vm = - TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, tx.utxo(0).unwrap(), &reused_values, &sig_cache, true); + let mut vm = TxScriptEngine::from_transaction_input( + &tx, + &tx.inputs()[0], + 0, + tx.utxo(0).unwrap(), + &reused_values, + &sig_cache, + true, + false, + ); // Should succeed because both SPKs are identical assert_eq!(vm.execute(), Ok(())); @@ -3644,6 +3683,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3670,6 +3710,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(TxScriptError::EvalFalse)); @@ -3703,6 +3744,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3727,6 +3769,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); // Should fail because script expects index 0 but we're at index 1 @@ -3773,6 +3816,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3792,6 +3836,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Ok(())); @@ -3815,6 +3860,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(TxScriptError::EvalFalse)); @@ -3837,6 +3883,7 @@ mod test { &reused_values, &sig_cache, true, + false, ); assert_eq!(vm.execute(), Err(TxScriptError::EvalFalse)); diff --git a/crypto/txscript/src/standard/multisig.rs b/crypto/txscript/src/standard/multisig.rs index 6c51fd361f..8133a6899c 100644 --- a/crypto/txscript/src/standard/multisig.rs +++ b/crypto/txscript/src/standard/multisig.rs @@ -184,7 +184,7 @@ mod tests { let (input, entry) = tx.populated_inputs().next().unwrap(); let cache = Cache::new(10_000); - let mut engine = TxScriptEngine::from_transaction_input(&tx, input, 0, entry, &reused_values, &cache, false); + let mut engine = TxScriptEngine::from_transaction_input(&tx, input, 0, entry, &reused_values, &cache, false, false); assert_eq!(engine.execute().is_ok(), is_ok); } #[test] diff --git a/mining/src/mempool/check_transaction_standard.rs b/mining/src/mempool/check_transaction_standard.rs index ef4d3fb9ee..a983b43dcb 100644 --- a/mining/src/mempool/check_transaction_standard.rs +++ b/mining/src/mempool/check_transaction_standard.rs @@ -188,6 +188,7 @@ impl Mempool { ScriptClass::PubKey => {} ScriptClass::PubKeyECDSA => {} ScriptClass::ScriptHash => { + // todo relax due to on fly calculation let num_sig_ops = get_sig_op_count::( &input.signature_script, &entry.script_public_key, diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index 52d9b79865..719158c8b0 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -34,7 +34,9 @@ use kaspa_consensus_core::header::Header; use kaspa_consensus_core::network::{NetworkId, NetworkType::Mainnet}; use kaspa_consensus_core::subnets::SubnetworkId; use kaspa_consensus_core::trusted::{ExternalGhostdagData, TrustedBlock}; -use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry}; +use kaspa_consensus_core::tx::{ + MutableTransaction, ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, +}; use kaspa_consensus_core::{blockhash, hashing, BlockHashMap, BlueWorkType}; use kaspa_consensus_notify::root::ConsensusNotificationRoot; use kaspa_consensus_notify::service::NotifyService; @@ -49,6 +51,7 @@ use flate2::read::GzDecoder; use futures_util::future::try_join_all; use itertools::Itertools; use kaspa_consensus_core::errors::tx::TxRuleError; +use kaspa_consensus_core::hashing::sighash::calc_schnorr_signature_hash; use kaspa_consensus_core::merkle::calc_hash_merkle_root; use kaspa_consensus_core::muhash::MuHashExtensions; use kaspa_core::core::Core; @@ -63,6 +66,7 @@ use kaspa_muhash::MuHash; use kaspa_notify::subscription::context::SubscriptionContext; use kaspa_txscript::caches::TxScriptCacheCounters; use kaspa_txscript::opcodes::codes::OpTrue; +use kaspa_txscript::script_builder::ScriptBuilderResult; use kaspa_utxoindex::api::{UtxoIndexApi, UtxoIndexProxy}; use kaspa_utxoindex::UtxoIndex; use serde::{Deserialize, Serialize}; @@ -845,6 +849,7 @@ impl KaspadGoParams { max_block_level: self.MaxBlockLevel, pruning_proof_m: self.PruningProofM, payload_activation: ForkActivation::never(), + runtime_sig_op_counting: ForkActivation::never(), } } } @@ -2005,3 +2010,133 @@ async fn payload_activation_test() { assert!(matches!(status, Ok(BlockStatus::StatusUTXOValid))); assert!(consensus.lkg_virtual_state.load().accepted_tx_ids.contains(&tx_id)); } + +#[tokio::test] +async fn runtime_sig_op_counting_test() { + use kaspa_consensus_core::{ + hashing::sighash::SigHashReusedValuesUnsync, hashing::sighash_type::SIG_HASH_ALL, subnets::SUBNETWORK_ID_NATIVE, + }; + use kaspa_txscript::{opcodes::codes::*, script_builder::ScriptBuilder}; + + // Runtime sig op counting activates at DAA score 3 + const RUNTIME_SIGOP_ACTIVATION_DAA_SCORE: u64 = 3; + + init_allocator_with_default_settings(); + + // Set up signing key for signature verification + let secp = secp256k1::Secp256k1::new(); + let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng()); + let keypair = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &secret_key.secret_bytes()).unwrap(); + let pub_key = keypair.x_only_public_key().0.serialize(); + + let reused_values = SigHashReusedValuesUnsync::new(); + + // Create redeem script that has 1 sig op in the executed branch (true) + // and 3 sig ops in the non-executed branch (false) + let redeem_script = || -> ScriptBuilderResult> { + Ok(ScriptBuilder::new() + .add_op(OpTrue)? + .add_op(OpIf)? + .add_op(OpCheckSig)? // This sig op gets executed + .add_op(OpElse)? + .add_op(OpCheckSig)? // These sig ops are skipped + .add_op(OpCheckSig)? + .add_op(OpCheckSig)? + .add_op(OpEndIf)? + .drain()) + }() + .unwrap(); + + let script_pub_key = kaspa_txscript::pay_to_script_hash_script(&redeem_script); + + // Set up initial UTXO with P2SH script + let initial_utxo_collection = [( + TransactionOutpoint::new(1.into(), 0), + UtxoEntry { amount: SOMPI_PER_KASPA, script_public_key: script_pub_key.clone(), block_daa_score: 0, is_coinbase: false }, + )]; + + 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.runtime_sig_op_counting = ForkActivation::new(RUNTIME_SIGOP_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..RUNTIME_SIGOP_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; + } + + // Create transaction spending P2SH with 1 sig op limit + let mut tx = Transaction::new( + 0, + vec![TransactionInput::new( + initial_utxo_collection[0].0, + vec![], // Placeholder for signature script + 0, + 1, // Only allowing 1 sig op - important for test + )], + vec![TransactionOutput::new(initial_utxo_collection[0].1.amount - 5000, ScriptPublicKey::from_vec(0, vec![OpTrue]))], + 0, + SUBNETWORK_ID_NATIVE, + 0, + vec![], + ); + + // Sign transaction + let mut tx_for_signing = MutableTransaction::new(tx.clone()); + tx_for_signing.entries = vec![Some(initial_utxo_collection[0].1.clone())]; + + let signature = { + let hash = calc_schnorr_signature_hash(&tx_for_signing.as_verifiable(), 0, SIG_HASH_ALL, &reused_values); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap(); + let sig = keypair.sign_schnorr(msg); + let mut signature = sig.as_ref().to_vec(); + signature.push(SIG_HASH_ALL.to_u8()); + signature + }; + + // Complete transaction with signature script + tx.inputs[0].signature_script = + ScriptBuilder::new().add_data(&signature).unwrap().add_data(&pub_key).unwrap().add_data(&redeem_script).unwrap().drain(); + + tx.finalize(); + + // Test 1: Before activation, tx should be rejected due to static sig op counting (sees 3 ops) + { + let miner_data = MinerData::new(ScriptPublicKey::from_vec(0, vec![]), vec![]); + let mut block = + consensus.build_utxo_valid_block_with_parents((index + 1).into(), vec![index.into()], miner_data.clone(), vec![]); + block.transactions.push(tx.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, Ok(BlockStatus::StatusDisqualifiedFromChain))); + index += 1; + } + + // Add block to reach activation + consensus.add_utxo_valid_block_with_parents((index + 1).into(), vec![(index - 1).into()], vec![]).await.unwrap(); + index += 1; + + // Test 2: After activation, tx should be accepted as runtime counting only sees 1 executed sig op + let status = consensus.add_utxo_valid_block_with_parents((index + 1).into(), vec![index.into()], vec![tx]).await; + assert!(matches!(status, Ok(BlockStatus::StatusUTXOValid))); +} diff --git a/wallet/pskt/src/pskt.rs b/wallet/pskt/src/pskt.rs index abae18f2dc..df7ac21a6e 100644 --- a/wallet/pskt/src/pskt.rs +++ b/wallet/pskt/src/pskt.rs @@ -436,7 +436,7 @@ impl PSKT { let reused_values = SigHashReusedValuesUnsync::new(); tx.populated_inputs().enumerate().try_for_each(|(idx, (input, entry))| { - TxScriptEngine::from_transaction_input(&tx, input, idx, entry, &reused_values, &cache, false).execute()?; + TxScriptEngine::from_transaction_input(&tx, input, idx, entry, &reused_values, &cache, false, false).execute()?; >::Ok(()) })?; }