Closed 3ierratango closed 9 months ago
Thanks for the issue, I'll try and help you out. It looks like you are using an old version of rust-bitcoin
. It would be easier if the issue was re-written to use the latest version. If you have to use an old version for some other reason then just please include the version in use and if you don't mind all the import statements (because I cannot easily remember where everything was in the old versions, we have done many improvements lately :)
OP_CHECKSIGVERIFY
doesn't leave anything on the stack, so you need to give CHECKSIGADD a number, but OP_CHECKSIGVERIFY doesn't leave a number on the stack. OP_CHECKSIG does.
Since Bob is required, this is probably the best way to do it.
Change it to this:
// 2 -of -3 signer script
let wallet_script = Builder::new()
.push_x_only_key(&bob.public_key().into())
.push_opcode(all::OP_CHECKSIGVERIFY) // Bob key is necessary
.push_x_only_key(&alice.public_key().into())
.push_opcode(all::OP_CHECKSIG)
.push_x_only_key(&charlie.public_key().into())
.push_opcode(all::OP_CHECKSIGADD)
.push_int(1) // since the number we are comparing is the valid sigs from alice and charlie, it should be 1
.push_opcode(all::OP_GREATERTHANOREQUAL) // atleast one sig
.into_script();
Also the sig order is wrong.
Use this:
let wit = Witness::from_vec(vec![
schnorr_sig3.to_vec(), // Charlie
schnorr_sig1.to_vec(), // Alice
schnorr_sig2.to_vec(), // Bob (it's a stack, so this is Last In First Out, and will be consumed by the first CHECKSIGVERIFY)
wallet_script.to_bytes(),
actual_control.serialize(),
]);
In regards to the rust-bitcoin specific parts using SigCache etc. I can't really speak to the correctness as I've yet to use rust-bitcoin in a wallet context before.
Someone else will have to speak to those parts. As said before by @tcharding, it would be nice if you could upgrade to the latest version, since I'm sure some of the APIs you're using are much better UX to use in later versions.
Thankyou @junderw The issue was indeed the script setup and signature ordering, fixing that solved the error and the transaction was broadcasted.
This is the working code for anyone stumbling into this issue later
fn main() {
println!("Generating a new taproot transaction");
let secp = Secp256k1::new();
let alice_secret =
SecretKey::from_str("2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90")
.unwrap();
let bob_secret =
SecretKey::from_str("81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce9")
.unwrap();
let charlie_secret =
SecretKey::from_str("81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce8")
.unwrap();
let internal_secret =
SecretKey::from_str("1229101a0fcf2104e8808dab35661134aa5903867d44deb73ce1c7e4eb925be8")
.unwrap();
let alice = KeyPair::from_secret_key(&secp, &alice_secret);
let bob = KeyPair::from_secret_key(&secp, &bob_secret);
let charlie = KeyPair::from_secret_key(&secp, &charlie_secret);
let internal = KeyPair::from_secret_key(&secp, &internal_secret);
println!("alice public key {}", alice.public_key());
println!("bob public key {}", bob.public_key());
println!("internal public key {}", internal.public_key());
// 2 -of -3 signer script
// 2 -of -3 signer script
let wallet_script = Builder::new()
.push_x_only_key(&bob.public_key().into())
.push_opcode(all::OP_CHECKSIGVERIFY) // Bob key is necessary
.push_x_only_key(&alice.public_key().into())
.push_opcode(all::OP_CHECKSIG)
.push_x_only_key(&charlie.public_key().into())
.push_opcode(all::OP_CHECKSIGADD)
.push_int(1) // since the number we are comparing is the valid sigs from alice and charlie, it should be 1
.push_opcode(all::OP_GREATERTHANOREQUAL) // atleast one sig
.into_script();
println!("Script {:?}", wallet_script);
let builder = TaprootBuilder::with_huffman_tree(vec![(1, wallet_script.clone())]).unwrap();
let tap_tree = TapTree::from_builder(builder).unwrap();
let tap_info = tap_tree.into_builder().finalize(&secp, internal.public_key().into()).unwrap();
let merkle_root = tap_info.merkle_root();
let tweak_key_pair = internal.tap_tweak(&secp, merkle_root).into_inner();
let address = Address::p2tr(
&secp,
tap_info.internal_key(),
tap_info.merkle_root(),
bitcoin::Network::Testnet,
);
println!("Taproot wallet address {:?}", address);
// start preparing an output transaction from the wallet address
// fetch the utxos for the address
let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap();
let vec_tx_in = client
.script_list_unspent(&address.script_pubkey())
.unwrap()
.iter()
.map(|l| {
return TxIn {
previous_output: OutPoint::new(l.tx_hash, l.tx_pos.try_into().unwrap()),
script_sig: Script::new(),
sequence: bitcoin::Sequence(0xFFFFFFFF),
witness: Witness::default(),
}
})
.collect::<Vec<TxIn>>();
println!("Found UTXOS {:?}", vec_tx_in);
let prev_tx = vec_tx_in
.iter()
.map(|tx_id| client.transaction_get(&tx_id.previous_output.txid).unwrap())
.collect::<Vec<Transaction>>();
let mut tx = Transaction {
version: 2,
lock_time: bitcoin::PackedLockTime(0),
input: vec![TxIn {
previous_output: vec_tx_in[0].previous_output.clone(),
script_sig: Script::new(),
sequence: bitcoin::Sequence(0xFFFFFFFF),
witness: Witness::default(),
}],
output: vec![TxOut {
value: 10,
script_pubkey: Address::from_str(
"tb1p5kaqsuted66fldx256lh3en4h9z4uttxuagkwepqlqup6hw639gskndd0z",
)
.unwrap()
.script_pubkey(),
}],
};
let binding = vec![prev_tx[0].output[0].clone()];
let prevouts = Prevouts::All(&binding);
let sighash_sig = SighashCache::new(&mut tx.clone())
.taproot_script_spend_signature_hash(
0,
&prevouts,
ScriptPath::with_defaults(&wallet_script),
SchnorrSighashType::Default,
)
.unwrap();
let key_sig = SighashCache::new(&mut tx.clone())
.taproot_key_spend_signature_hash(0, &prevouts, SchnorrSighashType::Default)
.unwrap();
println!("key signing sighash {} ", key_sig);
println!("script sighash {} ", sighash_sig);
let actual_control = tap_info
.control_block(&(wallet_script.clone(), LeafVersion::TapScript))
.unwrap();
let res = actual_control.verify_taproot_commitment(
&secp,
tweak_key_pair.public_key().into(),
&wallet_script,
);
println!("is taproot committed? {} ", res);
println!("control block {} ", actual_control.serialize().to_hex());
let msg = Message::from_slice(&sighash_sig).unwrap();
let sig1 = secp.sign_schnorr(&msg, &alice);
let sig2 = secp.sign_schnorr(&msg, &bob);
let sig3 = secp.sign_schnorr(&msg, &charlie);
let schnorr_sig1 = SchnorrSig { sig: sig1, hash_ty: SchnorrSighashType::Default };
let schnorr_sig2 = SchnorrSig { sig: sig2, hash_ty: SchnorrSighashType::Default };
let schnorr_sig3 = SchnorrSig { sig: sig3, hash_ty: SchnorrSighashType::Default };
let wit = Witness::from_vec(vec![
schnorr_sig3.to_vec(), // Charlie
schnorr_sig1.to_vec(), // Alice
schnorr_sig2.to_vec(), // Bob (it's a stack, so this is Last In First Out, and will be consumed by the first CHECKSIGVERIFY)
wallet_script.to_bytes(),
actual_control.serialize(),
]);
tx.input[0].witness = wit.clone();
println!("Final transaction {:?}", tx);
// Broadcast tx
let tx_id = client.transaction_broadcast(&tx).unwrap();
println!("transaction hash: {}", tx_id.to_string());
}
I've been trying to do something to do but can't figure out how to make it work.
I get this error : called Result::unwrap()
on an Err
value: JsonRpc(Rpc(RpcError { code: -26, message: "mandatory-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)", data: None }))
Can anyone help me ?
Code:
`fn main() { let rpc = Client::new("http://127.0.0.1:18332", Auth::UserPass("amine".to_string(), "password".to_string())).unwrap(); let best_block_hash = rpc.get_best_block_hash().unwrap(); println!("best block hash: {}", best_block_hash);
println!("Generating a new taproot transaction");
let secp = Secp256k1::new();
let alice_secret =
SecretKey::from_str("2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90")
.unwrap();
let bob_secret =
SecretKey::from_str("81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce9")
.unwrap();
let charlie_secret =
SecretKey::from_str("81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce8")
.unwrap();
let internal_secret =
SecretKey::from_str("1229101a0fcf2104e8808dab35661134aa5903867d44deb73ce1c7e4eb925be8")
.unwrap();
let alice = Keypair::from_secret_key(&secp, &alice_secret);
let bob = Keypair::from_secret_key(&secp, &bob_secret);
let charlie = Keypair::from_secret_key(&secp, &charlie_secret);
let internal = Keypair::from_secret_key(&secp, &internal_secret);
println!("alice public key {}", alice.public_key());
println!("bob public key {}", bob.public_key());
println!("internal public key {}", internal.public_key());
let bob_script: ScriptBuf = Builder::new()
.push_int(7)
.push_opcode(all::OP_EQUALVERIFY)
.push_x_only_key(&bob.public_key().into())
.push_opcode(all::OP_CHECKSIGVERIFY)
.into_script();
let alice_script = Builder::new()
.push_int(7)
.push_opcode(all::OP_EQUALVERIFY)
.push_x_only_key(&alice.public_key().into())
.push_opcode(all::OP_CHECKSIGVERIFY)
.into_script();
println!("Bob Script {:?}", bob_script);
println!("Alice Script {:?}", alice_script);
let builder = TaprootBuilder::with_huffman_tree(vec![(1, bob_script.clone()), (1, alice_script.clone())]).unwrap();
//let tap_tree = builder.try_into_taptree().unwrap();
let tap_info = builder.finalize(&secp, internal.public_key().into()).unwrap();
let merkle_root = tap_info.merkle_root();
let tweak_key_pair = internal.tap_tweak(&secp, merkle_root).to_inner();
let address = Address::p2tr(
&secp,
tap_info.internal_key(),
tap_info.merkle_root(),
bitcoin::Network::Testnet,
);
println!("Taproot wallet address {:?}", address);
// start preparing an output transaction from the wallet address
// fetch the utxos for the address
let tx_in = TxIn {
previous_output: OutPoint::new(
Txid::from_str("f203702a3d4eace002fc30b89943a4351a4d2fb87a17ff7feb7bbd41ebe4fa98").unwrap(),
1
),
script_sig: ScriptBuf::new(),
sequence: Sequence(0xFFFFFFFF),
witness: Witness::default(),
};
println!("Found UTXOS {:?}", tx_in);
let prev_tx = rpc.get_raw_transaction(&tx_in.previous_output.txid, None).unwrap();
let mut tx = Transaction {
version: bitcoin::transaction::Version(2),
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: tx_in.previous_output.clone(),
script_sig: ScriptBuf::new(),
sequence: Sequence(0xFFFFFFFF),
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(333),
script_pubkey: Address::from_str(
"tb1p5kaqsuted66fldx256lh3en4h9z4uttxuagkwepqlqup6hw639gskndd0z",
)
.unwrap()
.require_network(Network::Testnet)
.unwrap()
.script_pubkey(),
}],
};
let binding = vec![prev_tx.output[0].clone()];
let prevouts = Prevouts::All(&binding);
let sighash_sig = SighashCache::new(&mut tx.clone())
.taproot_script_spend_signature_hash(
0,
&prevouts,
ScriptPath::with_defaults(&bob_script),
TapSighashType::Default,
)
.unwrap();
let key_sig = SighashCache::new(&mut tx.clone())
.taproot_key_spend_signature_hash(0, &prevouts, TapSighashType::Default)
.unwrap();
println!("key signing sighash {} ", key_sig);
println!("script sighash {} ", sighash_sig);
let actual_control = tap_info
.control_block(&(bob_script.clone(), LeafVersion::TapScript))
.unwrap();
let res = actual_control.verify_taproot_commitment(
&secp,
tweak_key_pair.public_key().into(),
&bob_script,
);
println!("is taproot committed? {} ", res);
println!("control block {} ", hex::encode(actual_control.serialize()));
let msg = Message::from_digest_slice(sighash_sig.as_ref()).unwrap();
let sig1 = secp.sign_schnorr(&msg, &alice);
let sig2 = secp.sign_schnorr(&msg, &bob);
let sig3 = secp.sign_schnorr(&msg, &charlie);
let schnorr_sig1 = Signature { sig: sig1, hash_ty: TapSighashType::Default };
let schnorr_sig2 = Signature { sig: sig2, hash_ty: TapSighashType::Default };
let schnorr_sig3 = Signature { sig: sig3, hash_ty: TapSighashType::Default };
let wit = Witness::from_slice(&vec![
//schnorr_sig3.to_vec(), // Charlie
//schnorr_sig1.to_vec(), // Alice
schnorr_sig2.to_vec(), // Bob (it's a stack, so this is Last In First Out, and will be consumed by the first CHECKSIGVERIFY)
Builder::new().push_int(7).into_script().into_bytes(),
bob_script.to_bytes(),
actual_control.serialize(),
]);
tx.input[0].witness = wit.clone();
println!("Final transaction {:?} {:?}", tx.input[0].witness, hex::encode(schnorr_sig2.to_vec()));
// Broadcast tx
let tx_id = rpc.send_raw_transaction(&tx).unwrap();
println!("transaction hash: {}", tx_id.to_string());
}`
Witness::from_slice
will view the Vec of [0x57 (OP_7)]
as a PUSHDATA of 0x57, which means the script interpreter will view it as pushing the number 0x57 (87) on the stack. and since 87 != 7 you get an error.
Since it looks like Witness only allows PUSHDATA (even though OP_1 - OP_16 are considered pushes and are allowed) the way you fix this is to use vec![7]
instead of the Builder::new().push_int(7) stuff.
@junderw Thank you soo much I've spent the whole day today trying to figure it out but couldn't. Really appreciate it.
I don't think it's strictly a problem with this library since the witness elements are not scripts. Manually casting a script to bytes is something like transmute (just not UB) here and gets you into trouble.
That being said, we could some helper for adding integers to witnesses so people are not tempted to do this. But isn't it simply 7i32.to_le_bytes()
?
In this case it's just 7
. 7u8
if you want to be explicit.
OK, myself missing this is probably enough reason to add a method. :)
hey there, I'm trying to figure out why i am getting this error
called Result::unwrap()
on an Err
value: JsonRpc(Rpc(RpcError { code: -26, message: "mandatory-script-verify-flag-failed (Invalid Schnorr signature)", data: None }))
use bitcoincore_rpc::{Auth, Client, RawTx, RpcApi};
use bitcoin::{
key::{Keypair, Secp256k1}, opcodes, script::Builder, secp256k1::{Message, SecretKey}, sighash::{Prevouts, ScriptPath, SighashCache}, taproot::{LeafVersion, Signature, TaprootBuilder}, Address, Amount, Network, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey};
use std::str::FromStr;
use bitcoin::key::TapTweak;
fn get_multisig_script(serialized_x_only_public_keys: Vec<[u8; 32]>) -> ScriptBuf {
let mut script_builder = Builder::new()
.push_int(0);
for serialized_x_only_public_key in serialized_x_only_public_keys.iter() {
script_builder = script_builder
.push_x_only_key(&XOnlyPublicKey::from_slice(serialized_x_only_public_key).unwrap())
.push_opcode(opcodes::all::OP_CHECKSIGADD);
}
let script = script_builder
.push_int((serialized_x_only_public_keys.len() as f64 / 2.0).ceil() as i64)
.push_opcode(opcodes::all::OP_GREATERTHANOREQUAL)
.into_script();
script
}
fn main() {
let rpc = Client::new("https://bitcoin-node.dev.aws.archnetwork.xyz:18443",
Auth::UserPass("bitcoin".to_string(),
"428bae8f3c94f8c39c50757fc89c39bc7e6ebc70ebf8f618".to_string())).unwrap();
let best_block_hash = rpc.get_best_block_hash().unwrap();
println!("best block hash: {}", best_block_hash);
println!("Generating a new taproot transaction");
let secp = Secp256k1::new();
let alice_secret =
SecretKey::from_str("a60ba45039868e6fdd7fb936b5beba8a1b7d65c1e40b77b835d26a2e64729375")
.unwrap();
let bob_secret =
SecretKey::from_str("66157045fcec83bf294ff1a697402e12a9e3b404373d6a7c13ada7d01d780713")
.unwrap();
let charlie_secret =
SecretKey::from_str("66157045fcec83bf294ff1a697402e12a9e3b404373d6a7c13ada7d01d780713")
.unwrap();
let alice = Keypair::from_secret_key(&secp, &alice_secret);
let bob = Keypair::from_secret_key(&secp, &bob_secret);
let charlie = Keypair::from_secret_key(&secp, &charlie_secret);
println!("alice public key {}", alice.public_key());
println!("bob public key {}", bob.public_key());
let script = get_multisig_script(vec![Into::<XOnlyPublicKey>::into(alice.public_key()).serialize(), Into::<XOnlyPublicKey>::into(bob.public_key()).serialize(), Into::<XOnlyPublicKey>::into(charlie.public_key()).serialize()]);
let builder = TaprootBuilder::with_huffman_tree(vec![(1, script.clone())]).unwrap();
//let tap_tree = builder.try_into_taptree().unwrap();
let tap_info = builder.finalize(&secp, alice.public_key().into()).unwrap();
let merkle_root = tap_info.merkle_root();
let tweak_key_pair = alice.tap_tweak(&secp, merkle_root).to_inner();
let address = Address::p2tr(
&secp,
tap_info.internal_key(),
tap_info.merkle_root(),
bitcoin::Network::Testnet,
);
println!("Taproot wallet address {:?}", address);
// start preparing an output transaction from the wallet address
// fetch the utxos for the address
let tx_in = TxIn {
previous_output: OutPoint::new(
Txid::from_str("44808d2a5ce15eae4511b3fed4daf146ce47ff827987d3206616bf3361f1c523").unwrap(),
1
),
script_sig: ScriptBuf::new(),
sequence: Sequence(0xFFFFFFFF),
witness: Witness::default(),
};
println!("Found UTXOS {:?}", tx_in);
let prev_tx: Transaction = bitcoin::consensus::deserialize(&hex::decode(rpc.get_raw_transaction(&bitcoincore_rpc::bitcoin::Txid::from_str(&tx_in.previous_output.txid.to_string()).unwrap(), None).unwrap().raw_hex()).unwrap()).unwrap();
let mut tx = Transaction {
version: bitcoin::transaction::Version(2),
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![TxIn {
previous_output: tx_in.previous_output.clone(),
script_sig: ScriptBuf::new(),
sequence: Sequence(0xFFFFFFFF),
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(333),
script_pubkey: Address::from_str(
"tb1p5kaqsuted66fldx256lh3en4h9z4uttxuagkwepqlqup6hw639gskndd0z",
)
.unwrap()
.require_network(Network::Testnet)
.unwrap()
.script_pubkey(),
}],
};
let binding = vec![prev_tx.output[0].clone()];
let prevouts = Prevouts::All(&binding);
let sighash_sig = SighashCache::new(&mut tx.clone())
.taproot_script_spend_signature_hash(
0,
&prevouts,
ScriptPath::with_defaults(&script),
TapSighashType::Default,
)
.unwrap();
let key_sig = SighashCache::new(&mut tx.clone())
.taproot_key_spend_signature_hash(0, &prevouts, TapSighashType::Default)
.unwrap();
println!("key signing sighash {} ", key_sig);
println!("script sighash {} ", sighash_sig);
let actual_control = tap_info
.control_block(&(script.clone(), LeafVersion::TapScript))
.unwrap();
let res = actual_control.verify_taproot_commitment(
&secp,
tweak_key_pair.public_key().into(),
&script,
);
println!("is taproot committed? {} ", res);
println!("control block {} ", hex::encode(actual_control.serialize()));
let msg = Message::from_digest_slice(sighash_sig.as_ref()).unwrap();
let sig1 = secp.sign_schnorr(&msg, &alice);
let sig2 = secp.sign_schnorr(&msg, &bob);
let sig3 = secp.sign_schnorr(&msg, &charlie);
let schnorr_sig1 = Signature { sig: sig1, hash_ty: TapSighashType::Default };
let schnorr_sig2 = Signature { sig: sig2, hash_ty: TapSighashType::Default };
let schnorr_sig3 = Signature { sig: sig3, hash_ty: TapSighashType::Default };
let wit = Witness::from_slice(&vec![
schnorr_sig3.to_vec(), // Charlie
schnorr_sig2.to_vec(), // Bob
schnorr_sig1.to_vec(), // Alice
script.to_bytes(),
actual_control.serialize(),
]);
tx.input[0].witness = wit.clone();
println!("Final transaction {:?} {:?}", tx.input[0].witness, hex::encode(schnorr_sig2.to_vec()));
// Broadcast tx
let tx_id = rpc.send_raw_transaction(&tx).unwrap();
println!("transaction hash: {}", tx_id.to_string());
}
Thank @3ierratango @junderw for providing this solution.
I've successfully implemented a command-line tool for Bitcoin Taproot multi-signature(https://github.com/rooch-network/rooch/pull/2595) based on the method outlined in this issue. I'd like to share two important observations that might be helpful for others working on similar implementations:
The order of signatures written to the witness is inverse to the order of corresponding public keys in the script.
Use OP_CHECKSIG
to replace OP_CHECKSIGVERIFY
in the script, which offers greater adaptability for various use cases. Here's the script:
fn create_multisig_script(threshold: usize, public_keys: &Vec<XOnlyPublicKey>) -> ScriptBuf {
let mut builder = bitcoin::script::Builder::new();
for pubkey in public_keys {
if builder.is_empty() {
builder = builder.push_x_only_key(pubkey);
builder = builder.push_opcode(bitcoin::opcodes::all::OP_CHECKSIG);
} else {
builder = builder.push_x_only_key(pubkey);
builder = builder.push_opcode(bitcoin::opcodes::all::OP_CHECKSIGADD);
}
}
builder = builder.push_int(threshold as i64);
builder = builder.push_opcode(bitcoin::opcodes::all::OP_GREATERTHANOREQUAL);
builder.into_script()
}
@jolestar your code is inefficient in the case when threshold == public_keys.len()
In that case the most efficient solution is to make every operation OP_CHECKSIG_VERIFY
except the last one which is just OP_CHECKSIG
and not have the count check at the end. (One could also argue that single-sig should just degrade to key spend and also that you could use MuSig2/FROST but the additional cryptographic complexity may not be worth it in your case.)
I've also noticed your function doesn't check that threshold <= public_keys.len()
which may be a footgun as it allows accidentally creating unspendable scripts.
@jolestar I've also noticed your function doesn't check that
threshold <= public_keys.len()
which may be a footgun as it allows accidentally creating unspendable scripts.
Thank you, fixed at https://github.com/rooch-network/rooch/pull/2615
Hi, I'm working on creating a taproot transaction to spend from a multisig account with 2-of-3 signatures. I run into this error when broadcasting transaction
I see threads saying this is probably from the script or signature contruction but I cannot figure out the root cause, any advice appreciated, thanks
Minimal reproducible example