banner

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:

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)

banner

Converter

Source: CurrencyRate
Top Selling Multipurpose WP Theme

Newsletter

Subscribe my Newsletter for new blog posts, tips & new photos. Let's stay updated!

banner

Leave a Comment

Layer 1
Your Crypto & Blockchain Beacon

CryptoInsightful

Welcome to CryptoInsightful.com, your trusted source for in-depth analysis, news, and insights into the world of cryptocurrencies, blockchain technology, NFTs (Non-Fungible Tokens), and cybersecurity. Our mission is to empower you with the knowledge and understanding you need to navigate the rapidly evolving landscape of digital assets and emerging technologies.