I'm trying to make a taproot HTLC using rust-bitcoin for an atomic swap protocol. My plan is that in the "happy case" of my protocol, the coins are spent with the key-path, and then there is a tapleaf for the "refund" case (after n blocks, the original owner can spend), and a tapleaf with a hashlock.

Assuming I have functions to build up the hashlock and timelock scripts, I'm building some TaprootSpendInfo and a receive address as so:

let taproot_spend_info = TaprootBuilder::new()
            build_hashlock_script(alice_hashlock.as_slice(), &bob.hl_keypair.x_only_public_key().0)
        .expect("couldn't add hashlock leaf")
            build_timelock_script(2, &alice.refund_keypair.x_only_public_key().0)
        .expect("Couldn't add timelock leaf")
        .expect("Could not finalize taproot spend info");

    let alice2bob_addr = Address::p2tr_tweaked(taproot_spend_info.output_key(), Network::Regtest);

I go and fund that address. Then when I want to spend from that address with the key-path, I make a transaction and get the sighash as so:

    let mut keypath_tx = Transaction {
        version: 1,
        lock_time: PackedLockTime::ZERO,
        input: vec![ TxIn {
            previous_output: OutPoint {
                txid: first_funding_txid,
                vout: 0
            script_sig: script::Builder::new().into_script(), // this might be wrong
            sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
            witness: Witness::new(),
        output: vec![ TxOut {
            value: Amount::from_btc(0.99).unwrap().to_sat(),
            script_pubkey: miner_address.script_pubkey()
    let sighash = keypath_tx.signature_hash(0, &script::Builder::new().into_script(), 0x00);
    let hash_to_be_signed = sighash.as_hash();

And then I take that sighash and sign it with libsecp:

    let message = secp256k1::Message::from_slice(&hash_to_be_signed).unwrap();
    let signature = secp.sign_schnorr(&message, &alice.escrow_keypair.secret_key().add_tweak(&taproot_spend_info.tap_tweak().to_scalar()).unwrap().keypair(&secp));

When I try to broadcast this transaction, I get an error Invalid Schnorr signature.

I think I'm adding the signature to the witness correctly: BIP341 says that if after removing the annex from the witness (if it's there), if there is one item left, it's interpreted as a key-path spend and the item is interpreted as a signature. So I think pushing the signature into the otherwise empty witness is the right thing to do. I also checked that the signature is valid. That makes me think that I'm either signing the wrong thing or that I'm not tweaking the private key correctly at signing time.

My only taproot experience so far has been with descriptor wallets, so any tips about what I'm doing wrong would be greatly appreciated. Thanks!

2 Answers 2


It looks like you're using the Transaction::signature_hash method to compute your signature hash -- this is used to compute legacy (pre-segwit!) signature hashes, and we've been unfortunately very slow in deprecating it. I believe the next release of rust-bitcoin will have better documentation and a deprecation notice.

The new way to compute sighashes, which supports segwit and taproot, is to put your transaction into a SighashCache which can then be used to compute sighashes for each input. This structure caches hashed data which is the same for every input -- something both segwit and taproot sighashes were designed for, but legacy sighashes were not -- which is why it has a less convenient API. But on the other hand, it should be much faster.

  • That's helpful. Thanks!
    – rijndael
    Jan 3, 2023 at 1:14
  • 1
    For any future devs who stumble upon this, the other bit I was missing is you need to make sure the schnorr sig is properly encoded like so: ``` let final_sig = schnorr::SchnorrSig { sig: signature, hash_ty: SchnorrSighashType::All }; ``` and then insert it into the witness like: ``` keypath_tx.input[0].witness.push(final_sig.serialize()); ```
    – rijndael
    Jan 3, 2023 at 1:28

the answer above combined with the comments should be enough to have it working, I wanted also to share it all together in case it may be easier for others to follow through on it.

serialize and Prevouts imports are from bitcoin::psbt

fn sign_key_tx(
    secp: &Secp256k1<All>,
    tx_ref: &Transaction,
    prevouts: &Prevouts<TxOut>,
    key_pair_internal: &KeyPair,
    tap_info: &TaprootSpendInfo,
) -> Transaction {
    let mut tx = tx_ref.clone();
    let sighash_sig = SighashCache::new(&mut tx.clone())
        .taproot_key_spend_signature_hash(0, prevouts, SchnorrSighashType::AllPlusAnyoneCanPay) // or All

    let tweak_key_pair = key_pair_internal.tap_tweak(secp, tap_info.merkle_root());

    // then sig
    let msg = Message::from_slice(&sighash_sig).unwrap();

    let sig = secp.sign_schnorr(&msg, &tweak_key_pair.to_inner());

    //verify sig
    secp.verify_schnorr(&sig, &msg, &tweak_key_pair.to_inner().x_only_public_key().0)

    // then witness
    let schnorr_sig = SchnorrSig {
        hash_ty: SchnorrSighashType::AllPlusAnyoneCanPay, // or All



Hope it helps!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.