diff --git a/doc/src/design/ir.md b/doc/src/design/ir.md index a8bc86a1..101def6f 100644 --- a/doc/src/design/ir.md +++ b/doc/src/design/ir.md @@ -199,6 +199,7 @@ containing the correctly serialized transactions `v15` and `v30`. | `BuildPayToScriptHash` | Creates a P2SH script. | | `BuildOpReturnScripts`| Creates an OP_RETURN script. | | `BuildPayToAnchor` | Creates a P2A (pay-to-anchor) script for CPFP. | +| `BuildPayToBareMulti` | Creates a bare m-of-n multisig script. | | **Witness stack**| **Construct a witness stack.** | | `BeginWitnessStack`| Begins building a witness stack. | | `AddWitness` | Adds an item to the witness stack. | diff --git a/fuzzamoto-ir/src/compiler.rs b/fuzzamoto-ir/src/compiler.rs index e355bb31..93c16d5f 100644 --- a/fuzzamoto-ir/src/compiler.rs +++ b/fuzzamoto-ir/src/compiler.rs @@ -191,6 +191,11 @@ enum SigningRequest { private_key_var: usize, sighash_var: usize, }, + BareMulti { + required: u8, + private_keys: Vec<[u8; 32]>, + sighash_var: usize, + }, Taproot { spend_info_var: Option, selected_leaf: Option, @@ -424,6 +429,7 @@ impl Compiler { | Operation::BuildPayToPubKey | Operation::BuildPayToPubKeyHash | Operation::BuildPayToWitnessPubKeyHash + | Operation::BuildPayToBareMulti { .. } | Operation::BuildPayToTaproot => { self.handle_script_building_operations(instruction)?; } @@ -1054,6 +1060,49 @@ impl Compiler { requires_signing: None, }); } + Operation::BuildPayToBareMulti { + required, + private_keys, + } => { + let _sighash_flags_var = self.get_input::(&instruction.inputs, 0)?; + let sighash_var = instruction + .inputs + .first() + .copied() + .ok_or(CompilerError::IncorrectNumberOfInputs)?; + let n = u8::try_from(private_keys.len()).map_err(|_| { + CompilerError::MiscError("too many keys in bare multisig".to_string()) + })?; + if n == 0 { + return Err(CompilerError::MiscError( + "bare multisig requires at least one private key".to_string(), + )); + } + let required_clamped = (*required).min(n).max(1); + + let mut spk_builder = ScriptBuf::builder().push_int(i64::from(required_clamped)); + for sk_bytes in private_keys { + let pk = PrivateKey::from_slice(sk_bytes, NetworkKind::Main).map_err(|_| { + CompilerError::MiscError("invalid bare multisig private key".to_string()) + })?; + spk_builder = spk_builder.push_key(&pk.public_key(&self.secp_ctx)); + } + let script_pubkey = spk_builder + .push_int(i64::from(n)) + .push_opcode(bitcoin::opcodes::all::OP_CHECKMULTISIG) + .into_script(); + + self.append_variable(Scripts { + script_pubkey: script_pubkey.into(), + script_sig: vec![], + witness: Witness { stack: Vec::new() }, + requires_signing: Some(SigningRequest::BareMulti { + required: required_clamped, + private_keys: private_keys.clone(), + sighash_var, + }), + }); + } Operation::BuildPayToScriptHash => { let script = self.get_input::>(&instruction.inputs, 0)?; let witness_var = self.get_input::(&instruction.inputs, 1)?; @@ -2198,6 +2247,43 @@ impl Compiler { _ => {} } } + SigningRequest::BareMulti { + required, + private_keys, + sighash_var, + } => { + let sighash_flag = *self.get_variable::(*sighash_var).unwrap(); + let sighash_type = + EcdsaSighashType::from_consensus(u32::from(sighash_flag)); + + // scriptPubKey is already on the txo; use it for signing + let script_code = Script::from_bytes(&txo_var.scripts.script_pubkey); + + // Collect `required` signatures (first `required` keys) + let mut sig_script_builder = + ScriptBuf::builder().push_opcode(bitcoin::opcodes::OP_0); + + for sk_bytes in private_keys.iter().take(*required as usize) { + if let Ok(hash) = cache.legacy_signature_hash( + idx, + script_code, + u32::from(sighash_flag), + ) { + let signature = ecdsa::Signature { + signature: self.secp_ctx.sign_ecdsa( + &secp256k1::Message::from_digest(*hash.as_byte_array()), + &SecretKey::from_slice(sk_bytes.as_slice()).unwrap(), + ), + sighash_type, + }; + sig_script_builder = sig_script_builder.push_slice( + PushBytesBuf::try_from(signature.to_vec()).unwrap(), + ); + } + } + + tx_var.tx.input[idx].script_sig = sig_script_builder.into_script(); + } SigningRequest::Taproot { spend_info_var, selected_leaf, @@ -2331,7 +2417,8 @@ impl Compiler { mod tests { use super::*; use crate::{ - IndexedVariable, Operation, Program, ProgramBuilder, ProgramContext, TaprootLeafSpec, + IndexedVariable, Instruction, Operation, Program, ProgramBuilder, ProgramContext, + TaprootLeafSpec, }; use bitcoin::{ Transaction, consensus::Decodable, opcodes::all::OP_PUSHNUM_1, taproot::LeafVersion, @@ -2636,6 +2723,56 @@ mod tests { assert_eq!(&control_block[33..], &HIDDEN_HASH); } + #[test] + fn compile_bare_multisig_without_sighash_input_returns_error() { + let program = Program::unchecked_new( + test_context(), + vec![Instruction { + inputs: vec![], + operation: Operation::BuildPayToBareMulti { + required: 1, + private_keys: vec![[1u8; 32]], + }, + }], + ); + + let mut compiler = Compiler::new(); + let err = compiler + .compile(&program) + .expect_err("expected compile error"); + assert!(matches!(err, CompilerError::IncorrectNumberOfInputs)); + } + + #[test] + fn compile_bare_multisig_without_private_keys_returns_error() { + let program = Program::unchecked_new( + test_context(), + vec![ + Instruction { + inputs: vec![], + operation: Operation::LoadSigHashFlags(0), + }, + Instruction { + inputs: vec![0], + operation: Operation::BuildPayToBareMulti { + required: 1, + private_keys: vec![], + }, + }, + ], + ); + + let mut compiler = Compiler::new(); + let err = compiler + .compile(&program) + .expect_err("expected compile error"); + assert!(matches!( + err, + CompilerError::MiscError(ref msg) + if msg == "bare multisig requires at least one private key" + )); + } + fn build_annex_program(annex: Vec) -> Program { let mut builder = ProgramBuilder::new(ProgramContext { num_nodes: 1, diff --git a/fuzzamoto-ir/src/generators/tx.rs b/fuzzamoto-ir/src/generators/tx.rs index 6af2381c..10fd4dd7 100644 --- a/fuzzamoto-ir/src/generators/tx.rs +++ b/fuzzamoto-ir/src/generators/tx.rs @@ -21,11 +21,12 @@ enum OutputType { PayToPubKeyHash, PayToWitnessPubKeyHash, PayToTaproot, + PayToBareMulti, OpReturn, } fn get_random_output_type(rng: &mut R) -> OutputType { - match rng.gen_range(0..8) { + match rng.gen_range(0..9) { 0 => OutputType::PayToWitnessScriptHash, 1 => OutputType::PayToAnchor, 2 => OutputType::PayToScriptHash, @@ -33,6 +34,7 @@ fn get_random_output_type(rng: &mut R) -> OutputType { 4 => OutputType::PayToPubKeyHash, 5 => OutputType::PayToWitnessPubKeyHash, 6 => OutputType::PayToTaproot, + 7 => OutputType::PayToBareMulti, _ => OutputType::OpReturn, } } @@ -114,6 +116,7 @@ fn build_outputs( ) } OutputType::PayToTaproot => build_taproot_scripts(builder, rng), + OutputType::PayToBareMulti => build_bare_multi_scripts(builder, rng), }; let amount_var = @@ -546,6 +549,26 @@ fn random_merkle_path(rng: &mut R) -> Vec<[u8; 32]> { (0..depth).map(|_| random_node_hash(rng)).collect() } +fn build_bare_multi_scripts( + builder: &mut ProgramBuilder, + rng: &mut R, +) -> IndexedVariable { + let n = rng.gen_range(1u8..=3u8); + let required = rng.gen_range(1u8..=n); + let private_keys: Vec<[u8; 32]> = (0..n).map(|_| gen_secret_key_bytes(rng)).collect(); + + let sighash_flags_var = + builder.force_append_expect_output(vec![], &Operation::LoadSigHashFlags(0)); + + builder.force_append_expect_output( + vec![sighash_flags_var.index], + &Operation::BuildPayToBareMulti { + required, + private_keys, + }, + ) +} + fn gen_secret_key_bytes(rng: &mut R) -> [u8; 32] { loop { let mut secret = [0u8; 32]; diff --git a/fuzzamoto-ir/src/instruction.rs b/fuzzamoto-ir/src/instruction.rs index e99d7f54..41b7c370 100644 --- a/fuzzamoto-ir/src/instruction.rs +++ b/fuzzamoto-ir/src/instruction.rs @@ -69,7 +69,8 @@ impl Instruction { | Operation::LoadTaprootAnnex { .. } | Operation::BuildPayToTaproot | Operation::TaprootScriptsUseAnnex - | Operation::TaprootTxoUseAnnex => true, + | Operation::TaprootTxoUseAnnex + | Operation::BuildPayToBareMulti { .. } => true, _ => false, } } @@ -163,6 +164,7 @@ impl Instruction { | Operation::TakeCoinbaseTxo | Operation::TaprootScriptsUseAnnex | Operation::TaprootTxoUseAnnex + | Operation::BuildPayToBareMulti { .. } | Operation::TakeTxo => true, Operation::Nop { .. } diff --git a/fuzzamoto-ir/src/mutators/operation.rs b/fuzzamoto-ir/src/mutators/operation.rs index cbb87d56..d2031238 100644 --- a/fuzzamoto-ir/src/mutators/operation.rs +++ b/fuzzamoto-ir/src/mutators/operation.rs @@ -318,6 +318,39 @@ impl Mutator for OperationMutator { self.byte_array_mutator.mutate_bytes(bytes); Operation::LoadBytes(bytes.clone()) // TODO this clone is not needed } + Operation::BuildPayToBareMulti { + required, + private_keys, + } => { + let n = private_keys.len() as u8; + let new_required = if n > 0 { + rng.gen_range(1..=n) + } else { + *required + }; + let mut new_keys = private_keys.clone(); + if !new_keys.is_empty() { + let idx = rng.gen_range(0..new_keys.len()); + let mut key_bytes: Vec = new_keys[idx].into(); + let mut valid_key = false; + for _ in 1..=10 { + self.byte_array_mutator.mutate_bytes(&mut key_bytes); + key_bytes.resize(32, 0); + if PrivateKey::from_slice(&key_bytes, NetworkKind::Main).is_ok() { + valid_key = true; + break; + } + } + if !valid_key { + key_bytes = vec![0x1u8; 32]; + } + new_keys[idx] = key_bytes.try_into().unwrap(); + } + Operation::BuildPayToBareMulti { + required: new_required, + private_keys: new_keys, + } + } op => op.clone(), }; diff --git a/fuzzamoto-ir/src/operation.rs b/fuzzamoto-ir/src/operation.rs index 54160992..e382e1a2 100644 --- a/fuzzamoto-ir/src/operation.rs +++ b/fuzzamoto-ir/src/operation.rs @@ -106,7 +106,7 @@ pub enum Operation { BuildRawScripts, BuildPayToWitnessScriptHash, // TODO: BuildPayToTaproot, - // TODO: BuildPayToBareMulti, BeginMultiSig, EndMultiSig + // TODO: BeginMultiSig, EndMultiSig BuildPayToPubKey, BuildPayToPubKeyHash, BuildPayToWitnessPubKeyHash, @@ -119,6 +119,15 @@ pub enum Operation { BeginPrefillTransactions, AddPrefillTx, EndPrefillTransactions, + + BuildPayToBareMulti { + /// m-of-n: number of required signatures (1..=n) + required: u8, + /// Private keys for each pubkey in the script + private_keys: Vec<[u8; 32]>, + }, + + // cmpctblock building operations BuildCompactBlock, // filterload building operations @@ -217,6 +226,17 @@ impl fmt::Display for Operation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Operation::Nop { .. } => write!(f, "Nop"), + Operation::BuildPayToBareMulti { + required, + private_keys, + } => { + write!( + f, + "BuildPayToBareMulti({}-of-{})", + required, + private_keys.len() + ) + } Operation::LoadBytes(bytes) => write!( f, "LoadBytes(\"{}\")", @@ -575,6 +595,7 @@ impl Operation { | Operation::Probe | Operation::TaprootScriptsUseAnnex | Operation::TaprootTxoUseAnnex + | Operation::BuildPayToBareMulti { .. } | Operation::BuildTaprootTree { .. } => false, } } @@ -741,6 +762,7 @@ impl Operation { | Operation::BuildCoinbaseTxInput | Operation::AddCoinbaseTxOutput | Operation::SendBlockTxn + | Operation::BuildPayToBareMulti { .. } | Operation::Probe => false, } } @@ -797,6 +819,7 @@ impl Operation { Operation::LoadConnectionType(_) => vec![Variable::ConnectionType], Operation::LoadDuration(_) => vec![Variable::Duration], Operation::LoadAddr(_) => vec![Variable::AddrRecord], + Operation::BuildPayToBareMulti { .. } => vec![Variable::Scripts], Operation::LoadBlockHeight(_) => vec![Variable::BlockHeight], Operation::LoadCompactFilterType(_) => vec![Variable::CompactFilterType], Operation::SendRawMessage => vec![], @@ -981,6 +1004,7 @@ impl Operation { Variable::Scripts, Variable::ConstAmount, ], + Operation::BuildPayToBareMulti { .. } => vec![Variable::SigHashFlags], Operation::TakeTxo => vec![Variable::ConstTx], Operation::TakeCoinbaseTxo => vec![Variable::ConstCoinbaseTx], Operation::AddWitness => vec![Variable::MutWitnessStack, Variable::Bytes], @@ -1155,6 +1179,7 @@ impl Operation { | Operation::BuildOpReturnScripts | Operation::BuildPayToAnchor | Operation::BuildPayToTaproot + | Operation::BuildPayToBareMulti { .. } | Operation::BuildPayToPubKey | Operation::BuildPayToPubKeyHash | Operation::BuildPayToWitnessPubKeyHash