diff --git a/fuzzamoto-ir/src/compiler.rs b/fuzzamoto-ir/src/compiler.rs index ef673713..4935ff52 100644 --- a/fuzzamoto-ir/src/compiler.rs +++ b/fuzzamoto-ir/src/compiler.rs @@ -178,6 +178,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, @@ -410,6 +415,7 @@ impl Compiler { | Operation::BuildPayToPubKey | Operation::BuildPayToPubKeyHash | Operation::BuildPayToWitnessPubKeyHash + | Operation::BuildPayToBareMulti { .. } | Operation::BuildPayToTaproot => { self.handle_script_building_operations(instruction)?; } @@ -1061,6 +1067,38 @@ impl Compiler { requires_signing: None, }); } + Operation::BuildPayToBareMulti { + required, + private_keys, + } => { + let n = u8::try_from(private_keys.len()).map_err(|_| { + CompilerError::MiscError("too many keys in bare multisig".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: instruction.inputs[0], + }), + }); + } Operation::BuildPayToScriptHash => { let script = self.get_input::>(&instruction.inputs, 0)?; let witness_var = self.get_input::(&instruction.inputs, 1)?; @@ -2128,6 +2166,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, 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 6c975a56..1e26768c 100644 --- a/fuzzamoto-ir/src/instruction.rs +++ b/fuzzamoto-ir/src/instruction.rs @@ -159,6 +159,7 @@ impl Instruction { | Operation::TakeCoinbaseTxo | Operation::TaprootScriptsUseAnnex | Operation::TaprootTxoUseAnnex + | Operation::BuildPayToBareMulti { .. } | Operation::TakeTxo => true, Operation::Nop { .. } diff --git a/fuzzamoto-ir/src/operation.rs b/fuzzamoto-ir/src/operation.rs index 8f3b4e0a..c897dc87 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, @@ -115,6 +115,13 @@ pub enum Operation { BuildPayToAnchor, BuildPayToTaproot, + 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, @@ -214,6 +221,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(\"{}\")", @@ -584,6 +602,7 @@ impl Operation { | Operation::Probe | Operation::TaprootScriptsUseAnnex | Operation::TaprootTxoUseAnnex + | Operation::BuildPayToBareMulti { .. } | Operation::BuildTaprootTree { .. } => false, } } @@ -743,6 +762,7 @@ impl Operation { | Operation::BuildCoinbaseTxInput | Operation::AddCoinbaseTxOutput | Operation::SendBlockTxn + | Operation::BuildPayToBareMulti { .. } | Operation::Probe => false, } } @@ -799,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![], @@ -980,6 +1001,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], @@ -1142,6 +1164,7 @@ impl Operation { | Operation::BuildOpReturnScripts | Operation::BuildPayToAnchor | Operation::BuildPayToTaproot + | Operation::BuildPayToBareMulti { .. } | Operation::BuildPayToPubKey | Operation::BuildPayToPubKeyHash | Operation::BuildPayToWitnessPubKeyHash