From 287437c30fde122c80858fd0eeceb26d4625af71 Mon Sep 17 00:00:00 2001 From: atl4so Date: Tue, 28 Apr 2026 12:34:23 +0100 Subject: [PATCH] feat(debugger): auto-sign sig args from secret keys --- Cargo.lock | 1 + debugger/cli/Cargo.toml | 1 + debugger/cli/src/main.rs | 64 +++++++++++++- debugger/cli/tests/auto_sign.rs | 152 ++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 debugger/cli/tests/auto_sign.rs diff --git a/Cargo.lock b/Cargo.lock index a4963f23..0ae4c2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -782,6 +782,7 @@ dependencies = [ "kaspa-consensus-core", "kaspa-txscript", "kaspa-txscript-errors", + "secp256k1", "silverscript-lang", ] diff --git a/debugger/cli/Cargo.toml b/debugger/cli/Cargo.toml index 7c288061..1273c034 100644 --- a/debugger/cli/Cargo.toml +++ b/debugger/cli/Cargo.toml @@ -17,4 +17,5 @@ silverscript-lang = { path = "../../silverscript-lang" } kaspa-consensus-core.workspace = true kaspa-txscript.workspace = true kaspa-txscript-errors.workspace = true +secp256k1.workspace = true clap = { version = "4.5.60", features = ["derive"] } diff --git a/debugger/cli/src/main.rs b/debugger/cli/src/main.rs index 17a560ab..1436c305 100644 --- a/debugger/cli/src/main.rs +++ b/debugger/cli/src/main.rs @@ -13,7 +13,8 @@ use debugger_session::test_runner::{ }; use debugger_session::{format_failure_report, format_value}; use kaspa_consensus_core::Hash; -use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_consensus_core::hashing::sighash::{SigHashReusedValuesUnsync, calc_schnorr_signature_hash}; +use kaspa_consensus_core::hashing::sighash_type::SIG_HASH_ALL; use kaspa_consensus_core::tx::{ CovenantBinding, PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput, TxInputMass, UtxoEntry, VerifiableTransaction, @@ -22,6 +23,7 @@ use kaspa_txscript::caches::Cache; use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{EngineCtx, EngineFlags, pay_to_script_hash_script}; +use secp256k1::{Keypair, Message, SecretKey, Secp256k1}; use silverscript_lang::ast::{ContractAst, Expr, ExprKind, StateFieldExpr, TypeBase, TypeRef, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, CompiledContract, compile_contract, compile_contract_ast}; @@ -334,6 +336,46 @@ fn sigscript_push_script(script: &[u8]) -> Vec { ScriptBuilder::new().add_data(script).expect("push script data").drain() } +fn materialize_auto_sig_args( + ast: &ContractAst<'_>, + function_name: &str, + raw_args: &mut [String], + tx: &Transaction, + utxos: &[UtxoEntry], + input_idx: usize, +) -> Result<(), Box> { + let Some(function) = ast.functions.iter().find(|function| function.name == function_name) else { + return Ok(()); + }; + let populated = PopulatedTransaction::new(tx, utxos.to_vec()); + let reused_values = SigHashReusedValuesUnsync::new(); + let sig_hash = calc_schnorr_signature_hash(&populated, input_idx, SIG_HASH_ALL, &reused_values); + let msg = Message::from_digest_slice(sig_hash.as_bytes().as_slice())?; + let secp = Secp256k1::new(); + let raw_arg_offset = function.params.len().saturating_sub(raw_args.len()); + for (idx, param) in function.params.iter().enumerate() { + if !matches!(param.type_ref.base, TypeBase::Sig | TypeBase::Datasig) || idx < raw_arg_offset { + continue; + } + let raw_idx = idx - raw_arg_offset; + if raw_idx >= raw_args.len() { + continue; + } + let bytes = parse_hex_bytes(&raw_args[raw_idx])?; + if bytes.len() != 32 { + continue; + } + let secret = SecretKey::from_slice(&bytes)?; + let keypair = Keypair::from_secret_key(&secp, &secret); + let sig = keypair.sign_schnorr(msg); + let mut signature = Vec::with_capacity(65); + signature.extend_from_slice(sig.as_ref()); + signature.push(SIG_HASH_ALL.to_u8()); + raw_args[raw_idx] = signature.iter().map(|byte| format!("{byte:02x}")).collect::(); + } + Ok(()) +} + fn combine_action_and_redeem(action: &[u8], redeem_script: &[u8]) -> Result, Box> { let mut builder = ScriptBuilder::new(); builder.add_ops(action)?; @@ -649,7 +691,7 @@ fn main() -> Result<(), Box> { } else { None }; - let (script_path, mut raw_ctor_args, selected_name, raw_args, tx_scenario, expect) = + let (script_path, mut raw_ctor_args, selected_name, mut raw_args, tx_scenario, expect) = if let Some(test_file) = inferred_test_file.as_deref() { let test_name = cli.test_name.as_deref().ok_or("--test-name requires --test-file or SCRIPT_PATH")?; let script_override = cli.script_path.as_deref().map(Path::new); @@ -859,6 +901,24 @@ fn main() -> Result<(), Box> { }) .collect::>>() }); + let provisional_inputs = tx + .inputs + .iter() + .enumerate() + .map(|(input_idx, _)| TransactionInput { + previous_outpoint: input_prev_outpoints[input_idx], + signature_script: explicit_input_sigs[input_idx].clone().unwrap_or_default(), + sequence: input_sequences[input_idx], + mass: TxInputMass::SigopCount(input_sig_op_counts[input_idx].into()), + }) + .collect::>(); + let provisional_tx = Transaction::new(tx.version, provisional_inputs, tx_outputs.clone(), tx.lock_time, Default::default(), 0, vec![]); + let provisional_utxos = utxo_specs + .iter() + .map(|(value, spk, covenant_id)| UtxoEntry::new(*value, spk.clone(), 0, provisional_tx.is_coinbase(), *covenant_id)) + .collect::>(); + materialize_auto_sig_args(&parsed_contract, &selected_name, &mut raw_args, &provisional_tx, &provisional_utxos, tx.active_input_index)?; + let active_input_ctor_raw = tx.inputs[tx.active_input_index].constructor_args.clone().unwrap_or_else(|| raw_ctor_args.clone()); let active_compiled = compile_contract_for_raw_ctor_args(&source, &parsed_contract, &active_input_ctor_raw)?; let active_is_cov_leader = companion_leader_index.map(|index| index == tx.active_input_index).unwrap_or(true); diff --git a/debugger/cli/tests/auto_sign.rs b/debugger/cli/tests/auto_sign.rs new file mode 100644 index 00000000..ffdcfeba --- /dev/null +++ b/debugger/cli/tests/auto_sign.rs @@ -0,0 +1,152 @@ +use std::fs; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[test] +fn cli_debugger_auto_signs_sig_args_from_secret_keys() { + let cli = env!("CARGO_BIN_EXE_cli-debugger"); + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = std::env::temp_dir().join(format!("silverscript-auto-sign-{nonce}")); + fs::create_dir_all(&dir).unwrap(); + let source = dir.join("auto-sign.sil"); + let tests = dir.join("auto-sign.test.json"); + + fs::write( + &source, + r#"pragma silverscript ^0.1.0; + +contract AutoSigArg(pubkey owner) { + entrypoint function spend(sig s) { + require(checkSig(s, owner)); + } +} +"#, + ) + .unwrap(); + + fs::write( + &tests, + r#"{ + "tests": [ + { + "name": "secret_key_arg_materializes_valid_schnorr_signature", + "function": "spend", + "constructor_args": ["0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"], + "args": ["0x0000000000000000000000000000000000000000000000000000000000000001"], + "expect": "pass", + "tx": { + "active_input_index": 0, + "inputs": [{ "utxo_value": 100000 }], + "outputs": [{ "value": 99000, "p2pk_pubkey": "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" }] + } + }, + { + "name": "wrong_secret_key_fails_checksig", + "function": "spend", + "constructor_args": ["0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"], + "args": ["0x0000000000000000000000000000000000000000000000000000000000000002"], + "expect": "fail", + "tx": { + "active_input_index": 0, + "inputs": [{ "utxo_value": 100000 }], + "outputs": [{ "value": 99000, "p2pk_pubkey": "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" }] + } + } + ] +} +"#, + ) + .unwrap(); + + let output = Command::new(cli) + .arg(&source) + .arg("--run-all") + .arg("--test-file") + .arg(&tests) + .output() + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), "stdout:\n{stdout}\nstderr:\n{stderr}"); + assert!(stdout.contains("2 tests: 2 passed, 0 failed"), "stdout:\n{stdout}\nstderr:\n{stderr}"); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_debugger_auto_signs_sig_args_after_covenant_prefix_args() { + let cli = env!("CARGO_BIN_EXE_cli-debugger"); + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = std::env::temp_dir().join(format!("silverscript-auto-sign-covenant-{nonce}")); + fs::create_dir_all(&dir).unwrap(); + let source = dir.join("auto-sign-covenant.sil"); + let tests = dir.join("auto-sign-covenant.test.json"); + + fs::write( + &source, + r#"pragma silverscript ^0.1.0; + +contract AutoSigCovenant(pubkey owner) { + int status = 0; + + #[covenant.singleton(mode = transition)] + function step(State prev_state, sig s) : (State) { + require(prev_state.status == 0); + require(checkSig(s, owner)); + return(prev_state); + } +} +"#, + ) + .unwrap(); + + fs::write( + &tests, + r#"{ + "tests": [ + { + "name": "covenant_sig_secret_key_arg_is_offset_past_synthesized_state", + "function": "step", + "constructor_args": ["0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"], + "args": ["0x0000000000000000000000000000000000000000000000000000000000000001"], + "expect": "pass", + "tx": { + "active_input_index": 0, + "inputs": [ + { + "utxo_value": 100000, + "covenant_id": "0x1111111111111111111111111111111111111111111111111111111111111111", + "state": { "status": 0 } + } + ], + "outputs": [ + { + "value": 99000, + "covenant_id": "0x1111111111111111111111111111111111111111111111111111111111111111", + "state": { "status": 0 } + } + ] + } + } + ] +} +"#, + ) + .unwrap(); + + let output = Command::new(cli) + .arg(&source) + .arg("--run-all") + .arg("--test-file") + .arg(&tests) + .output() + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), "stdout:\n{stdout}\nstderr:\n{stderr}"); + assert!(stdout.contains("1 tests: 1 passed, 0 failed"), "stdout:\n{stdout}\nstderr:\n{stderr}"); + + let _ = fs::remove_dir_all(&dir); +}