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
1 change: 1 addition & 0 deletions doc/src/design/ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
139 changes: 138 additions & 1 deletion fuzzamoto-ir/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
selected_leaf: Option<TaprootLeaf>,
Expand Down Expand Up @@ -424,6 +429,7 @@ impl Compiler {
| Operation::BuildPayToPubKey
| Operation::BuildPayToPubKeyHash
| Operation::BuildPayToWitnessPubKeyHash
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildPayToTaproot => {
self.handle_script_building_operations(instruction)?;
}
Expand Down Expand Up @@ -1054,6 +1060,49 @@ impl Compiler {
requires_signing: None,
});
}
Operation::BuildPayToBareMulti {
required,
private_keys,
} => {
let _sighash_flags_var = self.get_input::<u8>(&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::<Vec<u8>>(&instruction.inputs, 0)?;
let witness_var = self.get_input::<Witness>(&instruction.inputs, 1)?;
Expand Down Expand Up @@ -2198,6 +2247,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) {
Comment thread
brunoerg marked this conversation as resolved.
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 Expand Up @@ -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,
Expand Down Expand Up @@ -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<u8>) -> Program {
let mut builder = ProgramBuilder::new(ProgramContext {
num_nodes: 1,
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
4 changes: 3 additions & 1 deletion fuzzamoto-ir/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ impl Instruction {
| Operation::LoadTaprootAnnex { .. }
| Operation::BuildPayToTaproot
| Operation::TaprootScriptsUseAnnex
| Operation::TaprootTxoUseAnnex => true,
| Operation::TaprootTxoUseAnnex
| Operation::BuildPayToBareMulti { .. } => true,
_ => false,
}
}
Expand Down Expand Up @@ -163,6 +164,7 @@ impl Instruction {
| Operation::TakeCoinbaseTxo
| Operation::TaprootScriptsUseAnnex
| Operation::TaprootTxoUseAnnex
| Operation::BuildPayToBareMulti { .. }
| Operation::TakeTxo => true,

Operation::Nop { .. }
Expand Down
33 changes: 33 additions & 0 deletions fuzzamoto-ir/src/mutators/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,39 @@ impl<R: RngCore, M: OperationByteMutator> Mutator<R> for OperationMutator<M> {
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<u8> = 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(),
};

Expand Down
27 changes: 26 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 @@ -119,6 +119,15 @@ pub enum Operation {
BeginPrefillTransactions,
AddPrefillTx,
EndPrefillTransactions,

BuildPayToBareMulti {
Comment thread
brunoerg marked this conversation as resolved.
/// m-of-n: number of required signatures (1..=n)
required: u8,
/// Private keys for each pubkey in the script
private_keys: Vec<[u8; 32]>,
},
Comment thread
brunoerg marked this conversation as resolved.

// cmpctblock building operations
BuildCompactBlock,

// filterload building operations
Expand Down Expand Up @@ -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(\"{}\")",
Expand Down Expand Up @@ -575,6 +595,7 @@ impl Operation {
| Operation::Probe
| Operation::TaprootScriptsUseAnnex
| Operation::TaprootTxoUseAnnex
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildTaprootTree { .. } => false,
}
}
Expand Down Expand Up @@ -741,6 +762,7 @@ impl Operation {
| Operation::BuildCoinbaseTxInput
| Operation::AddCoinbaseTxOutput
| Operation::SendBlockTxn
| Operation::BuildPayToBareMulti { .. }
| Operation::Probe => false,
}
}
Expand Down Expand Up @@ -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![],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -1155,6 +1179,7 @@ impl Operation {
| Operation::BuildOpReturnScripts
| Operation::BuildPayToAnchor
| Operation::BuildPayToTaproot
| Operation::BuildPayToBareMulti { .. }
| Operation::BuildPayToPubKey
| Operation::BuildPayToPubKeyHash
| Operation::BuildPayToWitnessPubKeyHash
Expand Down
Loading