rust-bitcoin / rust-bitcoin

Rust Bitcoin library
Creative Commons Zero v1.0 Universal
2.06k stars 668 forks source link

Taproot script spend : non-mandatory-script-verify-flag (Invalid Schnorr signature) #2241

Closed 3ierratango closed 9 months ago

3ierratango commented 9 months ago

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

Protocol(String("sendrawtransaction RPC error: {\"code\":-26,\"message\":\"non-mandatory-script-verify-flag (Invalid Schnorr signature)\"}"))

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

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
    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_CHECKSIGADD)
        .push_x_only_key(&charlie.public_key().into())
        .push_opcode(all::OP_CHECKSIGADD)
        .push_int(2)
        .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_sig1.to_vec(),
        schnorr_sig2.to_vec(),
        schnorr_sig3.to_vec(),
        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());
}
tcharding commented 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 :)

junderw commented 9 months ago

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();
junderw commented 9 months ago

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(),
    ]);
junderw commented 9 months ago

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.

3ierratango commented 9 months ago

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());
}
0xfinetuned commented 9 months ago

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());

}`

junderw commented 9 months ago

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.

junderw commented 9 months ago

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.

0xfinetuned commented 9 months ago

@junderw Thank you soo much I've spent the whole day today trying to figure it out but couldn't. Really appreciate it.

Kixunil commented 9 months ago

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()?

apoelstra commented 9 months ago

In this case it's just 7. 7u8 if you want to be explicit.

Kixunil commented 9 months ago

OK, myself missing this is probably enough reason to add a method. :)

0xfinetuned commented 4 months ago

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());

}
jolestar commented 1 week ago

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:

  1. The order of signatures written to the witness is inverse to the order of corresponding public keys in the script.

  2. 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()
}

https://github.com/rooch-network/rooch/blob/main/crates/rooch-types/src/bitcoin/multisign_account.rs#L134-L150

Kixunil commented 1 week ago

@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 commented 1 week ago

@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