Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions fuzzamoto-ir/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
selected_leaf: Option<TaprootLeaf>,
Expand Down Expand Up @@ -410,6 +415,7 @@ impl Compiler {
| Operation::BuildPayToPubKey
| Operation::BuildPayToPubKeyHash
| Operation::BuildPayToWitnessPubKeyHash
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildPayToTaproot => {
self.handle_script_building_operations(instruction)?;
}
Expand Down Expand Up @@ -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::<Vec<u8>>(&instruction.inputs, 0)?;
let witness_var = self.get_input::<Witness>(&instruction.inputs, 1)?;
Expand Down Expand Up @@ -2128,6 +2166,43 @@ impl Compiler {
_ => {}
}
}
SigningRequest::BareMulti {
required,
private_keys,
sighash_var,
} => {
let sighash_flag = *self.get_variable::<u8>(*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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this ever result in a p2ms that has less signatures than required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because of let required_clamped = (*required).min(n).max(1); which is the used required here. So it can never produce fewer signatures than required.

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,
Expand Down
25 changes: 24 additions & 1 deletion fuzzamoto-ir/src/generators/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ enum OutputType {
PayToPubKeyHash,
PayToWitnessPubKeyHash,
PayToTaproot,
PayToBareMulti,
OpReturn,
}

fn get_random_output_type<R: RngCore>(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,
3 => OutputType::PayToPubKey,
4 => OutputType::PayToPubKeyHash,
5 => OutputType::PayToWitnessPubKeyHash,
6 => OutputType::PayToTaproot,
7 => OutputType::PayToBareMulti,
_ => OutputType::OpReturn,
}
}
Expand Down Expand Up @@ -114,6 +116,7 @@ fn build_outputs<R: RngCore>(
)
}
OutputType::PayToTaproot => build_taproot_scripts(builder, rng),
OutputType::PayToBareMulti => build_bare_multi_scripts(builder, rng),
};

let amount_var =
Expand Down Expand Up @@ -546,6 +549,26 @@ fn random_merkle_path<R: RngCore>(rng: &mut R) -> Vec<[u8; 32]> {
(0..depth).map(|_| random_node_hash(rng)).collect()
}

fn build_bare_multi_scripts<R: RngCore>(
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<R: RngCore>(rng: &mut R) -> [u8; 32] {
loop {
let mut secret = [0u8; 32];
Expand Down
1 change: 1 addition & 0 deletions fuzzamoto-ir/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ impl Instruction {
| Operation::TakeCoinbaseTxo
| Operation::TaprootScriptsUseAnnex
| Operation::TaprootTxoUseAnnex
| Operation::BuildPayToBareMulti { .. }
| Operation::TakeTxo => true,

Operation::Nop { .. }
Expand Down
25 changes: 24 additions & 1 deletion fuzzamoto-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub enum Operation {
BuildRawScripts,
BuildPayToWitnessScriptHash,
// TODO: BuildPayToTaproot,
// TODO: BuildPayToBareMulti, BeginMultiSig, EndMultiSig
// TODO: BeginMultiSig, EndMultiSig
BuildPayToPubKey,
BuildPayToPubKeyHash,
BuildPayToWitnessPubKeyHash,
Expand All @@ -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,

Expand Down Expand Up @@ -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(\"{}\")",
Expand Down Expand Up @@ -584,6 +602,7 @@ impl Operation {
| Operation::Probe
| Operation::TaprootScriptsUseAnnex
| Operation::TaprootTxoUseAnnex
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildTaprootTree { .. } => false,
}
}
Expand Down Expand Up @@ -743,6 +762,7 @@ impl Operation {
| Operation::BuildCoinbaseTxInput
| Operation::AddCoinbaseTxOutput
| Operation::SendBlockTxn
| Operation::BuildPayToBareMulti { .. }
| Operation::Probe => false,
}
}
Expand Down Expand Up @@ -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![],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -1142,6 +1164,7 @@ impl Operation {
| Operation::BuildOpReturnScripts
| Operation::BuildPayToAnchor
| Operation::BuildPayToTaproot
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildPayToPubKey
| Operation::BuildPayToPubKeyHash
| Operation::BuildPayToWitnessPubKeyHash
Expand Down