Bitcoin

p2tr – Non-required script validation flag (invalid Schnorr signature) error message appears when using taproot transactions

I’m trying to use Taproot addresses via the key spend path using the musig2 lib in Rust. This address is listed as 2 owners. However, I still get this message when broadcasting a hex transaction to https://blockstream.info/testnet/tx/push.

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

The logic to create this transaction is as follows: First, you need to create a deposit transaction that includes the aggregated addresses as output. The code to generate the aggregated address is as follows:

pub fn aggregate_pubkeys(
    owner_pubkey: PublicKey,
    se_pubkey: PublicKey,
) -> (PublicKey, PublicKey, Address, KeyAggContext) 
    let secp = Secp256k1::new();
    let mut pubkeys: Vec<PublicKey> = vec!();
    pubkeys.push(owner_pubkey);
    pubkeys.push(se_pubkey);
    let key_agg_ctx_tw = KeyAggContext::new(pubkeys.clone())
        .unwrap()
        .with_unspendable_taproot_tweak()
        .unwrap();

    let aggregated_pubkey: PublicKey = key_agg_ctx_tw.aggregated_pubkey_untweaked();
    let aggregated_pubkey_tw: PublicKey = key_agg_ctx_tw.aggregated_pubkey();

    let aggregated_address = Address::p2tr(
        &secp,
        aggregated_pubkey.x_only_public_key().0,
        None,
        Network::Testnet,
    );

    (
        aggregated_pubkey,
        aggregated_pubkey_tw,
        aggregated_address,
        key_agg_ctx_tw,
    )

We then use the aggregated pub key to generate a scriptpubkey for output about deposit transactions.

let agg_scriptpubkey = ScriptBuf::new_p2tr(&secp, agg_pubkey.x_only_public_key().0, None);
let utxo = TxOut 
    value: Amount::from_sat(amount),
    script_pubkey: agg_scriptpubkey,
;

Aggregated_pubkey_tw and key_agg_ctx_tw are used to generate aggregated signatures. I create a signature according to this document https://docs.rs/musig2/latest/musig2/

The code to create a spend transaction is as follows:


pub async fn create_bk_tx(
    pool: &PoolWrapper,
    conn: &NodeConnector,
    key_agg_ctx: &KeyAggContext,
    agg_pubkey: &PublicKey,
    agg_pubkey_tw: &PublicKey,
    agg_address: &Address,
    receiver_address: &str,
    txid: &str,
    vout: u32,
    amount: u64,
    statechain_id: &str,
) -> Result<()> 
    let secp = Secp256k1::new();
    let seckey = pool
        .get_seckey_by_id(&statechain_id)
        .await
        .unwrap()
        .unwrap();
    let seckey = SecretKey::from_str(&seckey).unwrap();

    // CREATE UNSIGNED TRANSACTION
    let agg_scriptpubkey = ScriptBuf::new_p2tr(&secp, agg_pubkey.x_only_public_key().0, None);
   
    let prev_outpoint = OutPoint 
        txid: txid.parse().unwrap(),
        vout: vout.into(),
    ;

    let input = TxIn 
        previous_output: prev_outpoint,
        script_sig: ScriptBuf::default(),
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        witness: Witness::default(),
    ;

    let output_address = Address::from_str(receiver_address).unwrap();
    let checked_output_address = output_address.require_network(Network::Testnet).unwrap();
    let spend = TxOut 
        value: Amount::from_sat(amount - BASE_TX_FEE),
        script_pubkey: checked_output_address.script_pubkey(),
    ;

    let mut unsigned_tx = Transaction 
        version: transaction::Version::TWO,  // Post BIP-68.
        lock_time: absolute::LockTime::ZERO, // Ignore the locktime.
        input: vec!(input),                  // Input goes into index 0.
        output: vec!(spend),                 // Outputs, order does not matter.
    ;

    let utxo = TxOut 
        value: Amount::from_sat(amount),
        script_pubkey: agg_scriptpubkey,
    ;

    let prevouts = vec!(utxo);
    let prevouts = Prevouts::All(&prevouts);
    let mut sighasher = SighashCache::new(&mut unsigned_tx);

    let sighash_type = TapSighashType::All;
    let sighash = sighasher
        .taproot_key_spend_signature_hash(0, &prevouts, sighash_type)
        .expect("failed to construct sighash");

    let message = sighash.to_string();

    let parsed_msg = message.clone();
    let msg_clone = parsed_msg.clone();
    let msg = parsed_msg.clone();

    // MUSIG 2 TO CREATE SIGNATURE ON THE UNSIGNED TRANSACTION

    let get_nonce_res = statechain::get_nonce(&conn, statechain_id, &signed_statechain_id).await?; // API to get nonce from the server
    let server_pubnonce = get_nonce_res.server_nonce;

    let nonce_seed = (0xACu8; 32);

    let secnonce = SecNonce::build(nonce_seed).with_seckey(seckey).build();

    let our_public_nonce = secnonce.public_nonce();

    let public_nonces = (
        our_public_nonce,
        server_pubnonce.parse::<PubNonce>().unwrap(),
    );

    let agg_pubnonce: AggNonce = public_nonces.iter().sum();

    let agg_pubnonce_str = agg_pubnonce.to_string();

    let our_partial_signature: PartialSignature = musig2::sign_partial(
        &key_agg_ctx,
        seckey,
        secnonce,
        &agg_pubnonce,
        message,
    )
    .expect("error creating partial signature");

    let serialized_key_agg_ctx = key_agg_ctx
        .to_bytes()
        .to_hex_string(bitcoin::hex::Case::Lower);

    let get_sign_res = statechain::get_partial_signature(
        &conn,
        &serialized_key_agg_ctx,
        &statechain_id,
        &signed_statechain_id,
        &msg_clone,
        &agg_pubnonce_str,
    )
    .await?; // API to request the partial signature from the server

    let server_signature = get_sign_res.partial_signature;

    let partial_signatures = (
        our_partial_signature,
        PartialSignature::from_hex(&server_signature).unwrap(),
    );

    let final_signature: secp256k1::schnorr::Signature = musig2::aggregate_partial_signatures(
        &key_agg_ctx,
        &agg_pubnonce,
        partial_signatures,
        msg_clone,
    )
    .expect("error aggregating signatures");
    musig2::verify_single(*agg_pubkey_tw, final_signature, msg)
        .expect("aggregated signature must be valid");

    let signature = bitcoin::taproot::Signature 
        sig: final_signature,
        hash_ty: sighash_type,
    ;

    let mut wit = Witness::new();
    wit.push(signature.to_vec());
    *sighasher.witness_mut(0).unwrap() = wit;

    let tx = sighasher.into_transaction();

    let tx_hex = consensus::encode::serialize_hex(&tx); // the final transaction to broadcast
   
    Ok(())

The signature passes musig2 verification but not node verification. I think the problem may have to do with the sigh message I was asked to sign. This includes deposit and spend transactions generated by the code. Any thoughts or help would be greatly appreciated. Thank you in advance!

deposit tx hex : 
02000000000101adbc7f34e1e97d380be4056b77843588f0bf15549ee6044eec203d52108dd0c30000000000ffffffff02ec13000000000000225120d0d489a32e40e1fa4811aae37c0f1661b27f295bf7bb9d6725a80b17347306675b07000000000000160014c9da028a0c782cbd1c20210e6a80592210fb190c0247304402201633ac94ab0c6561cd48244129a59c03d1b0acfc59cf141ee723ba1755b1376d02202ba6003fd635dca60e68f03b5a01567d71c73eeac23fa5cf0d89d2ef359cde400121030253610ad0dd0958c56cd4a2865fb3c2b333c12eaa5a7c5986c324543e186b3800000000

Spend tx hex : 
020000000001010412ef1c75adf318b320982fb31824d9da843fc4ec3b80af12846ec0893b7b440000000000fdffffff016400000000000000160014c9da028a0c782cbd1c20210e6a80592210fb190c0141ca1633ea01751f2b5e66330cd54a2cae88a5e92901a2a3261e266a1392685780d48f511309b0d3d2e2828409948723180d1be3a597ca55df12aeaddbed2008280100000000

Related Articles

Back to top button