I am attempting to know the primitives of Taproot and Musig2 so I can implement them into my utility. I am utilizing the rust-bitcoin library and have written a script to create a Taproot deal with from an aggregated Musig2 public key (i’ve funded it with sats on mutinynet), create a tx spending from it & signal it, confirm the signature, after which spend the tx. Nevertheless, I am getting the error mandatory-script-verify-flag-failed (Invalid Schnorr signature) when I attempt to spend the tx. The odd factor is that the in-script sig validation throws no points.
Right here is the related code:
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, Tackle, Quantity, Community, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction,
TxIn, TxOut, Txid, Witness, XOnlyPublicKey,
};
use musig2::secp::Level;
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 primary() {
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 begin = std::time::On the spot::now();
let tx = construct_refund_transaction(
TXID,
VOUT,
DUMMY_UTXO_AMOUNT,
SPEND_AMOUNT,
"tb1p6e4q26tvyc7dmxfnk5zzhkux9d6pqpn0k5qxlw4gvc90unc0m3rq93a0tl",
keypair_array,
);
let period = begin.elapsed();
println!("Time taken to assemble refund transaction: {:?}", period);
match tx {
Okay(hex) => {
post_tx(hex);
}
Err(e) => {
println!("Error establishing 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>,
) -> End result<String, Field<dyn std::error::Error>> {
// Step 1: Convert public keys to factors for MuSig2
let points_vec: Vec<Level> = keypair_array
.iter()
.map(|keypair| Level::from_hex(&keypair.public_key().to_string()).unwrap())
.accumulate();
// 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!("Mixture pubkey: {:?}", hex::encode(agg_pubkey));
// Step 4: Create a P2TR deal with from the aggregated public key
let pubkey = PublicKey::from_slice(&agg_pubkey).unwrap();
let xonly_pubkey = XOnlyPublicKey::from(pubkey);
let secp = Secp256k1::new();
// let deal with = Tackle::p2tr(&secp, xonly_pubkey, None, Community::Signet);
// println!("P2TR deal with: {}", deal with);
// 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 deal with
let deal with = receivers_address(to_address);
// Step 7: Create the transaction enter
let enter = TxIn {
previous_output: out_point,
script_sig: ScriptBuf::default(), // Empty for P2TR
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::default(), // Shall be stuffed after signing
};
// Step 8: Create the transaction output
let spend = TxOut {
worth: Quantity::from_sat(spend_amount),
script_pubkey: deal with.script_pubkey(),
};
// Step 9: Assemble the unsigned transaction
let mut unsigned_tx = Transaction {
model: transaction::Model::TWO,
lock_time: absolute::LockTime::ZERO,
enter: vec![input],
output: vec![spend],
};
let input_index = 0;
// Step 10: Put together 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)
.anticipate("didn't assemble sighash");
// Step 12: Put together 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)).accumulate();
let public_nonces: Vec<PubNonce> = secret_nonces.iter().map(|sn| sn.public_nonce()).accumulate();
// Step 14: Mixture 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: Mixture 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: Confirm the aggregated signature
let verification_result = verify_single(
Level::from_slice(&agg_pubkey).unwrap(),
aggregated_signature,
msg.as_ref(),
);
match verification_result {
Okay(()) => println!("Signature is legitimate"),
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);
Okay(hex.to_string())
}
// Helper operate to parse and validate the receiver's deal with
fn receivers_address(deal with: &str) -> Tackle {
deal with
.parse::<Tackle<_>>()
.anticipate("a sound deal with")
.require_network(Community::Signet)
.anticipate("legitimate deal with for signet")
}
// Helper operate to create an unspent transaction output (UTXO)
fn unspent_transaction_output<C: Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
txid: Txid,
vout: u32,
quantity: u64,
) -> (OutPoint, TxOut) {
let script_pubkey = ScriptBuf::new_p2tr(secp, internal_key, None);
let out_point = OutPoint { txid, vout };
let utxo = TxOut {
worth: Quantity::from_sat(quantity),
script_pubkey,
};
(out_point, utxo)
}
fn post_tx(hex: String) {
let consumer = reqwest::blocking::Consumer::new();
let res = consumer.publish("https://mutinynet.com/api/tx").physique(hex).ship();
match res {
Okay(res) => {
if res.standing().is_success() {
println!("Response: {:?}", res.textual content());
} else {
let error_text = res.textual content().unwrap_or_default();
if let Some(message) = error_text.cut up("message":"").nth(1) {
if let Some(error_message) = message.cut up(""").subsequent() {
println!("Error: {}", error_message);
} else {
println!("Error: {}", error_text);
}
} else {
println!("Error: {}", error_text);
}
}
}
Err(e) => {
println!("Error sending request: {:?}", e);
}
}
}
Am I doing something blatantly mistaken?
(these are dummy SKs I generated for the aim of this take a look at)