<p>You are creating a taproot address where the musig2 aggregate public key is the internal key. This means that the actual public key that appears in a transaction is not the aggregate pubkey - it's the aggregate pubkey plus a tweak. However, the signature you are creating only corresponds to the aggregate pubkey. Since it does not include the tweak, the signature is invalid for the taproot output pubkey.</p>
<p>Applying tweaks is specified in BIP 327, so your musig2 library should have functions for passing in the tweak. Computing the tweak is described in BIP 341, but rust-bitcoin should also have functionality for computing th tweak.</p>

CC BY-SA 4.0
2024-09-05T19:11:30.187Z
2024-09-05T19:11:30.187Z
Ava Chow
<p>I'm trying to understand the primitives of Taproot and Musig2 so I can implement them into my application. I'm using the rust-bitcoin library and have written a script to create a Taproot address from an aggregated Musig2 public key (i've funded it with sats on mutinynet), create a tx spending from it &amp; sign it, verify the signature, and then spend the tx. However, I'm getting the error mandatory-script-verify-flag-failed (Invalid Schnorr signature) when I try to spend the tx. The odd thing is that the in-script sig validation throws no issues.</p>
<p>Here is the relevant code:</p>
<pre class="lang-rust prettyprint-override"><code>use bitcoin::hex::DisplayHex;

use bitcoin::consensus::Encodable;

use bitcoin::hashes::Hash;
use bitcoin::key::{Keypair, UntweakedPublicKey};
use bitcoin::locktime::absolute;

use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Verification};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::{
    transaction, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction,
    TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
use musig2::secp::Point;
use musig2::{
    aggregate_partial_signatures, sign_partial, verify_single, AggNonce, BinaryEncoding,
    KeyAggContext, PubNonce, SecNonce,
};
use rand::thread_rng;

use std::str::FromStr;

const DUMMY_UTXO_AMOUNT: u64 = 100000;
const SPEND_AMOUNT: u64 = 90000;
const SK: &amp;str = &quot;e504905952697837ac0f0dc9e46deb0340ae6468b185ba79e4fa96ebda3964c2&quot;;
const SK2: &amp;str = &quot;6bb7bc5dd0c9db0ca8bdba4616ee92afceb432551e0f23b32ea7bcada18da696&quot;;
const SK3: &amp;str = &quot;57d44ecb8812680e871732233ed8e4d4e11a25f22aef6c81dbe35a424ad8db41&quot;;
const TXID: &amp;str = &quot;726499fb478422087e7a89a04f25de5ed98e87c9881e4a264e8f31143cdee17b&quot;;
const VOUT: u32 = 1;

fn main() {
    let keypair_array = vec![
        Keypair::from_secret_key(&amp;Secp256k1::new(), &amp;SecretKey::from_str(SK).unwrap()),
        Keypair::from_secret_key(&amp;Secp256k1::new(), &amp;SecretKey::from_str(SK2).unwrap()),
        Keypair::from_secret_key(&amp;Secp256k1::new(), &amp;SecretKey::from_str(SK3).unwrap()),
    ];

    let start = std::time::Instant::now();
    let tx = construct_refund_transaction(
        TXID,
        VOUT,
        DUMMY_UTXO_AMOUNT,
        SPEND_AMOUNT,
        &quot;tb1p6e4q26tvyc7dmxfnk5zzhkux9d6pqpn0k5qxlw4gvc90unc0m3rq93a0tl&quot;,
        keypair_array,
    );
    let duration = start.elapsed();

    println!(&quot;Time taken to construct refund transaction: {:?}&quot;, duration);

    match tx {
        Ok(hex) =&gt; {
            post_tx(hex);
        }
        Err(e) =&gt; {
            println!(&quot;Error constructing transaction: {:?}&quot;, e);
        }
    }
}

pub fn construct_refund_transaction(
    from_txid: &amp;str,
    from_vout: u32,
    from_amount: u64,
    spend_amount: u64,
    to_address: &amp;str,
    keypair_array: Vec&lt;Keypair&gt;,
) -&gt; Result&lt;String, Box&lt;dyn std::error::Error&gt;&gt; {
    // Step 1: Convert public keys to points for MuSig2
    let points_vec: Vec&lt;Point&gt; = keypair_array
        .iter()
        .map(|keypair| Point::from_hex(&amp;keypair.public_key().to_string()).unwrap())
        .collect();

    // Step 2: Create a KeyAggContext for MuSig2
    let context = KeyAggContext::new(points_vec).unwrap();

    // Step 3: Get the aggregated public key
    let agg_pubkey: [u8; 33] = context.aggregated_pubkey();
    // println!(&quot;Aggregate pubkey: {:?}&quot;, hex::encode(agg_pubkey));

    // Step 4: Create a P2TR address from the aggregated public key
    let pubkey = PublicKey::from_slice(&amp;agg_pubkey).unwrap();
    let xonly_pubkey = XOnlyPublicKey::from(pubkey);
    let secp = Secp256k1::new();
    // let address = Address::p2tr(&amp;secp, xonly_pubkey, None, Network::Signet);
    // println!(&quot;P2TR address: {}&quot;, address);

    // Step 5: Get the unspent transaction output (UTXO)
    let (out_point, utxo) = unspent_transaction_output(
        &amp;secp,
        xonly_pubkey,
        Txid::from_str(from_txid).unwrap(),
        from_vout,
        from_amount,
    );

    // Step 6: Get the receiver's address
    let address = receivers_address(to_address);

    // Step 7: Create the transaction input
    let input = TxIn {
        previous_output: out_point,
        script_sig: ScriptBuf::default(), // Empty for P2TR
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        witness: Witness::default(), // Will be filled after signing
    };

    // Step 8: Create the transaction output
    let spend = TxOut {
        value: Amount::from_sat(spend_amount),
        script_pubkey: address.script_pubkey(),
    };

    // Step 9: Construct the unsigned transaction
    let mut unsigned_tx = Transaction {
        version: transaction::Version::TWO,
        lock_time: absolute::LockTime::ZERO,
        input: vec![input],
        output: vec![spend],
    };
    let input_index = 0;

    // Step 10: Prepare for signing
    let sighash_type = TapSighashType::Default;
    let prevouts = vec![utxo];
    let prevouts = Prevouts::All(&amp;prevouts);

    // Step 11: Calculate the sighash
    let mut sighasher = SighashCache::new(&amp;mut unsigned_tx);
    let sighash = sighasher
        .taproot_key_spend_signature_hash(input_index, &amp;prevouts, sighash_type)
        .expect(&quot;failed to construct sighash&quot;);

    // Step 12: Prepare the message to be signed
    let msg = Message::from_digest(sighash.to_byte_array());

    // Step 13: Generate random nonces for MuSig2
    let mut rng = thread_rng();
    let n = keypair_array.len();
    let secret_nonces: Vec&lt;SecNonce&gt; = (0..n).map(|_| SecNonce::random(&amp;mut rng)).collect();
    let public_nonces: Vec&lt;PubNonce&gt; = secret_nonces.iter().map(|sn| sn.public_nonce()).collect();

    // Step 14: Aggregate nonces
    let agg_nonce = AggNonce::sum(&amp;public_nonces);

    // Step 15: Generate partial signatures
    let mut partial_sigs = Vec::new();
    for (i, keypair) in keypair_array.iter().enumerate() {
        let seckey =
            musig2::secp::Scalar::from_slice(&amp;keypair.secret_key().secret_bytes()).unwrap();
        let partial_sig: musig2::PartialSignature = sign_partial(
            &amp;context,
            seckey,
            secret_nonces[i].clone(),
            &amp;agg_nonce,
            msg.as_ref(),
        )
        .unwrap();
        partial_sigs.push(partial_sig);
        // println!(&quot;Partial signature {}: {:?}&quot;, i, partial_sig);
    }

    // Step 16: Aggregate partial signatures
    let aggregated_signature: musig2::CompactSignature =
        aggregate_partial_signatures(&amp;context, &amp;agg_nonce, partial_sigs, msg.as_ref()).unwrap();
    // println!(&quot;Aggregated signature: {:?}&quot;, aggregated_signature);

    // Step 17: Verify the aggregated signature
    let verification_result = verify_single(
        Point::from_slice(&amp;agg_pubkey).unwrap(),
        aggregated_signature,
        msg.as_ref(),
    );

    match verification_result {
        Ok(()) =&gt; println!(&quot;Signature is valid&quot;),
        Err(e) =&gt; println!(&quot;Signature verification failed: {:?}&quot;, e),
    }

    // Step 18: Add the signature to the transaction witness
    sighasher
        .witness_mut(input_index)
        .unwrap()
        .push(&amp;aggregated_signature.to_bytes());

    // Step 19: Finalize the signed transaction
    let tx = sighasher.into_transaction();

    // Step 20: Serialize and print the signed transaction
    let mut serialized = Vec::new();
    tx.consensus_encode(&amp;mut serialized).unwrap();
    let hex = serialized.as_hex();
    // println!(&quot;Transaction hex: {}&quot;, hex);

    Ok(hex.to_string())
}

// Helper function to parse and validate the receiver's address
fn receivers_address(address: &amp;str) -&gt; Address {
    address
        .parse::&lt;Address&lt;_&gt;&gt;()
        .expect(&quot;a valid address&quot;)
        .require_network(Network::Signet)
        .expect(&quot;valid address for signet&quot;)
}

// Helper function to create an unspent transaction output (UTXO)
fn unspent_transaction_output&lt;C: Verification&gt;(
    secp: &amp;Secp256k1&lt;C&gt;,
    internal_key: UntweakedPublicKey,
    txid: Txid,
    vout: u32,
    amount: u64,
) -&gt; (OutPoint, TxOut) {
    let script_pubkey = ScriptBuf::new_p2tr(secp, internal_key, None);
    let out_point = OutPoint { txid, vout };
    let utxo = TxOut {
        value: Amount::from_sat(amount),
        script_pubkey,
    };
    (out_point, utxo)
}

fn post_tx(hex: String) {
    let client = reqwest::blocking::Client::new();
    let res = client.post(&quot;https://mutinynet.com/api/tx&quot;).body(hex).send();

    match res {
        Ok(res) =&gt; {
            if res.status().is_success() {
                println!(&quot;Response: {:?}&quot;, res.text());
            } else {
                let error_text = res.text().unwrap_or_default();
                if let Some(message) = error_text.split(&quot;message\&quot;:\&quot;&quot;).nth(1) {
                    if let Some(error_message) = message.split(&quot;\&quot;&quot;).next() {
                        println!(&quot;Error: {}&quot;, error_message);
                    } else {
                        println!(&quot;Error: {}&quot;, error_text);
                    }
                } else {
                    println!(&quot;Error: {}&quot;, error_text);
                }
            }
        }
        Err(e) =&gt; {
            println!(&quot;Error sending request: {:?}&quot;, e);
        }
    }
}
</code></pre>
<p>Am I doing anything blatantly wrong?</p>
<p>(these are dummy SKs I generated for the purpose of this test)</p>

<taproot><schnorr-signatures><p2tr><musig><rust-bitcoin>
Musig signature validation mismatch
Ethan Marcus
# Musig signature validation mismatch

(Asked by: username_0 on 2024-09-04)

I'm trying to understand the primitives of Taproot and Musig2 so I can implement them into my application. I'm using the rust-bitcoin library and have written a script to create a Taproot address from an aggregated Musig2 public key (i've funded it with sats on mutinynet), create a tx spending from it & sign it, verify the signature, and then spend the tx. However, I'm getting the error mandatory-script-verify-flag-failed (Invalid Schnorr signature) when I try to spend the tx. The odd thing is that the in-script sig validation throws no issues.

Here is the relevant code:

```rust
use bitcoin::hex::DisplayHex;

use bitcoin::consensus::Encodable;

use bitcoin::hashes::Hash;
use bitcoin::key::{Keypair, UntweakedPublicKey};
use bitcoin::locktime::absolute;

use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Verification};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
use bitcoin::{
    transaction, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction,
    TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
use musig2::secp::Point;
use musig2::{
    aggregate_partial_signatures, sign_partial, verify_single, AggNonce, BinaryEncoding,
    KeyAggContext, PubNonce, SecNonce,
};
use rand::thread_rng;

use std::str::FromStr;

const DUMMY_UTXO_AMOUNT: u64 = 100000;
const SPEND_AMOUNT: u64 = 90000;
const SK: &str = "e504905952697837ac0f0dc9e46deb0340ae6468b185ba79e4fa96ebda3964c2";
const SK2: &str = "6bb7bc5dd0c9db0ca8bdba4616ee92afceb432551e0f23b32ea7bcada18da696";
const SK3: &str = "57d44ecb8812680e871732233ed8e4d4e11a25f22aef6c81dbe35a424ad8db41";
const TXID: &str = "726499fb478422087e7a89a04f25de5ed98e87c9881e4a264e8f31143cdee17b";
const VOUT: u32 = 1;

fn main() {
    let keypair_array = vec![
        Keypair::from_secret_key(&Secp256k1::new(), &SecretKey::from_str(SK).unwrap()),
        Keypair::from_secret_key(&Secp256k1::new(), &SecretKey::from_str(SK2).unwrap()),
        Keypair::from_secret_key(&Secp256k1::new(), &SecretKey::from_str(SK3).unwrap()),
    ];

    let start = std::time::Instant::now();
    let tx = construct_refund_transaction(
        TXID,
        VOUT,
        DUMMY_UTXO_AMOUNT,
        SPEND_AMOUNT,
        "tb1p6e4q26tvyc7dmxfnk5zzhkux9d6pqpn0k5qxlw4gvc90unc0m3rq93a0tl",
        keypair_array,
    );
    let duration = start.elapsed();

    println!("Time taken to construct refund transaction: {:?}", duration);

    match tx {
        Ok(hex) => {
            post_tx(hex);
        }
        Err(e) => {
            println!("Error constructing transaction: {:?}", e);
        }
    }
}

pub fn construct_refund_transaction(
    from_txid: &str,
    from_vout: u32,
    from_amount: u64,
    spend_amount: u64,
    to_address: &str,
    keypair_array: Vec<Keypair>,
) -> Result<String, Box<dyn std::error::Error>> {
    // Step 1: Convert public keys to points for MuSig2
    let points_vec: Vec<Point> = keypair_array
        .iter()
        .map(|keypair| Point::from_hex(&keypair.public_key().to_string()).unwrap())
        .collect();

    // Step 2: Create a KeyAggContext for MuSig2
    let context = KeyAggContext::new(points_vec).unwrap();

    // Step 3: Get the aggregated public key
    let agg_pubkey: [u8; 33] = context.aggregated_pubkey();
    // println!("Aggregate pubkey: {:?}", hex::encode(agg_pubkey));

    // Step 4: Create a P2TR address from the aggregated public key
    let pubkey = PublicKey::from_slice(&agg_pubkey).unwrap();
    let xonly_pubkey = XOnlyPublicKey::from(pubkey);
    let secp = Secp256k1::new();
    // let address = Address::p2tr(&secp, xonly_pubkey, None, Network::Signet);
    // println!("P2TR address: {}", address);

    // Step 5: Get the unspent transaction output (UTXO)
    let (out_point, utxo) = unspent_transaction_output(
        &secp,
        xonly_pubkey,
        Txid::from_str(from_txid).unwrap(),
        from_vout,
        from_amount,
    );

    // Step 6: Get the receiver's address
    let address = receivers_address(to_address);

    // Step 7: Create the transaction input
    let input = TxIn {
        previous_output: out_point,
        script_sig: ScriptBuf::default(), // Empty for P2TR
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        witness: Witness::default(), // Will be filled after signing
    };

    // Step 8: Create the transaction output
    let spend = TxOut {
        value: Amount::from_sat(spend_amount),
        script_pubkey: address.script_pubkey(),
    };

    // Step 9: Construct the unsigned transaction
    let mut unsigned_tx = Transaction {
        version: transaction::Version::TWO,
        lock_time: absolute::LockTime::ZERO,
        input: vec![input],
        output: vec![spend],
    };
    let input_index = 0;

    // Step 10: Prepare for signing
    let sighash_type = TapSighashType::Default;
    let prevouts = vec![utxo];
    let prevouts = Prevouts::All(&prevouts);

    // Step 11: Calculate the sighash
    let mut sighasher = SighashCache::new(&mut unsigned_tx);
    let sighash = sighasher
        .taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type)
        .expect("failed to construct sighash");

    // Step 12: Prepare the message to be signed
    let msg = Message::from_digest(sighash.to_byte_array());

    // Step 13: Generate random nonces for MuSig2
    let mut rng = thread_rng();
    let n = keypair_array.len();
    let secret_nonces: Vec<SecNonce> = (0..n).map(|_| SecNonce::random(&mut rng)).collect();
    let public_nonces: Vec<PubNonce> = secret_nonces.iter().map(|sn| sn.public_nonce()).collect();

    // Step 14: Aggregate nonces
    let agg_nonce = AggNonce::sum(&public_nonces);

    // Step 15: Generate partial signatures
    let mut partial_sigs = Vec::new();
    for (i, keypair) in keypair_array.iter().enumerate() {
        let seckey =
            musig2::secp::Scalar::from_slice(&keypair.secret_key().secret_bytes()).unwrap();
        let partial_sig: musig2::PartialSignature = sign_partial(
            &context,
            seckey,
            secret_nonces[i].clone(),
            &agg_nonce,
            msg.as_ref(),
        )
        .unwrap();
        partial_sigs.push(partial_sig);
        // println!("Partial signature {}: {:?}", i, partial_sig);
    }

    // Step 16: Aggregate partial signatures
    let aggregated_signature: musig2::CompactSignature =
        aggregate_partial_signatures(&context, &agg_nonce, partial_sigs, msg.as_ref()).unwrap();
    // println!("Aggregated signature: {:?}", aggregated_signature);

    // Step 17: Verify the aggregated signature
    let verification_result = verify_single(
        Point::from_slice(&agg_pubkey).unwrap(),
        aggregated_signature,
        msg.as_ref(),
    );

    match verification_result {
        Ok(()) => println!("Signature is valid"),
        Err(e) => println!("Signature verification failed: {:?}", e),
    }

    // Step 18: Add the signature to the transaction witness
    sighasher
        .witness_mut(input_index)
        .unwrap()
        .push(&aggregated_signature.to_bytes());

    // Step 19: Finalize the signed transaction
    let tx = sighasher.into_transaction();

    // Step 20: Serialize and print the signed transaction
    let mut serialized = Vec::new();
    tx.consensus_encode(&mut serialized).unwrap();
    let hex = serialized.as_hex();
    // println!("Transaction hex: {}", hex);

    Ok(hex.to_string())
}

// Helper function to parse and validate the receiver's address
fn receivers_address(address: &str) -> Address {
    address
        .parse::<Address<_>>()
        .expect("a valid address")
        .require_network(Network::Signet)
        .expect("valid address for signet")
}

// Helper function to create an unspent transaction output (UTXO)
fn unspent_transaction_output<C: Verification>(
    secp: &Secp256k1<C>,
    internal_key: UntweakedPublicKey,
    txid: Txid,
    vout: u32,
    amount: u64,
) -> (OutPoint, TxOut) {
    let script_pubkey = ScriptBuf::new_p2tr(secp, internal_key, None);
    let out_point = OutPoint { txid, vout };
    let utxo = TxOut {
        value: Amount::from_sat(amount),
        script_pubkey,
    };
    (out_point, utxo)
}

fn post_tx(hex: String) {
    let client = reqwest::blocking::Client::new();
    let res = client.post("https://mutinynet.com/api/tx").body(hex).send();

    match res {
        Ok(res) => {
            if res.status().is_success() {
                println!("Response: {:?}", res.text());
            } else {
                let error_text = res.text().unwrap_or_default();
                if let Some(message) = error_text.split("message\":\"").nth(1) {
                    if let Some(error_message) = message.split("\"").next() {
                        println!("Error: {}", error_message);
                    } else {
                        println!("Error: {}", error_text);
                    }
                } else {
                    println!("Error: {}", error_text);
                }
            }
        }
        Err(e) => {
            println!("Error sending request: {:?}", e);
        }
    }
}

```

Am I doing anything blatantly wrong?

(these are dummy SKs I generated for the purpose of this test)

**Tags:** taproot, schnorr-signatures, p2tr, musig, rust-bitcoin


---

## 1 answers

### Answer by username_1 on 2024-09-05. Score: 1

You are creating a taproot address where the musig2 aggregate public key is the internal key. This means that the actual public key that appears in a transaction is not the aggregate pubkey - it's the aggregate pubkey plus a tweak. However, the signature you are creating only corresponds to the aggregate pubkey. Since it does not include the tweak, the signature is invalid for the taproot output pubkey.

Applying tweaks is specified in BIP 327, so your musig2 library should have functions for passing in the tweak. Computing the tweak is described in BIP 341, but rust-bitcoin should also have functionality for computing th tweak.


---

