btcsuite / btcwallet

A secure bitcoin wallet daemon written in Go (golang)
ISC License
1.15k stars 590 forks source link

Ask for example for simple P2TR transaction #868

Closed lenhatquang97 closed 1 year ago

lenhatquang97 commented 1 year ago

Situation

Hello everyone, I am doing thesis related to putting data into the transaction and I do not understand the mechanics of P2TR. Can everyone give me a sample code about simple P2TR transaction?

Why I stuck?

I do not understand how it works in P2TR transaction. Maybe I sign in a wrong way!....

Source code

func CommitTx(wif *btcutil.WIF, fundTx *btcutil.Tx, hash *chainhash.Hash, idx int32) (*wire.MsgTx, error) {
    tweakedPubKey, err := schnorr.ParsePubKey(wif.PrivKey.PubKey().X().Bytes())
    if err != nil {
        return nil, err
    }
    fmt.Println(wif.PrivKey.PubKey())
    fmt.Println(tweakedPubKey)

    tapKey := txscript.ComputeTaprootKeyNoScript(wif.PrivKey.PubKey())
    p2pktr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(tapKey), &chaincfg.SimNetParams)
    if err != nil {
        return nil, err
    }
    p2pktrAddr := p2pktr.EncodeAddress()
    hashLockKeypair, err := RandomWIF()
    if err != nil {
        return nil, err
    }
    builder := txscript.NewScriptBuilder()
    builder.AddData(schnorr.SerializePubKey(hashLockKeypair.PrivKey.PubKey()))
    builder.AddOp(txscript.OP_CHECKSIG)
    hashLockScript, err := builder.Script()
    if err != nil {
        return nil, err
    }

    tapLeaf := txscript.NewBaseTapLeaf(hashLockScript)
    tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)
    tapScriptRootHash := tapScriptTree.RootNode.TapHash()
    outputKey := txscript.ComputeTaprootOutputKey(
        wif.PrivKey.PubKey(), tapScriptRootHash[:],
    )
    scriptAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(outputKey), &chaincfg.SimNetParams)
    if err != nil {
        return nil, fmt.Errorf("error building script: %v", err)
    }
    fmt.Println(p2pktrAddr)
    fmt.Println(scriptAddr)

    /* ============================= COMMIT TX ================================== */
    commitTx, err := NewTx()
    if err != nil {
        return nil, err
    }
    outPoint := wire.NewOutPoint(hash, uint32(idx))
    txIn := wire.NewTxIn(outPoint, nil, nil)
    commitTx.AddTxIn(txIn)
    scriptAddrScript, _ := txscript.PayToAddrScript(scriptAddr)
    commitTxOut := wire.NewTxOut(fundTx.MsgTx().TxOut[0].Value*80/100, scriptAddrScript)
    commitTx.AddTxOut(commitTxOut)

    //After witness
    ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(wif.PrivKey.PubKey())
    ctrlBlockBytes, _ := ctrlBlock.ToBytes()

    inputFetcher := txscript.NewCannedPrevOutputFetcher(fundTx.MsgTx().TxOut[0].PkScript, fundTx.MsgTx().TxOut[0].Value)
    sigHashes := txscript.NewTxSigHashes(commitTx, inputFetcher)
    sig, err := txscript.RawTxInTaprootSignature(
        commitTx, sigHashes, 0, fundTx.MsgTx().TxOut[0].Value,
        fundTx.MsgTx().TxOut[0].PkScript, tapScriptRootHash[:], txscript.SigHashDefault,
        wif.PrivKey,
    )
    if err != nil {
        return nil, err
    }

    commitTx.TxIn[0].Witness = wire.TxWitness{sig, hashLockScript, ctrlBlockBytes}

    return commitTx, nil
}
guggero commented 1 year ago
// instead of
tweakedPubKey, err := schnorr.ParsePubKey(wif.PrivKey.PubKey().X().Bytes())

// can just use
tweakedPubKey, err := schnorr.ParsePubKey(schnorr.SerializePubKey(wif.PrivKey.PubKey()))
// this is for a key-only spend. so p2pktrAddr will give you the wrong address if you're attempting to include a script.
    tapKey := txscript.ComputeTaprootKeyNoScript(wif.PrivKey.PubKey())
// this looks wrong...
    ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(wif.PrivKey.PubKey())

See https://github.com/lightningnetwork/lnd/blob/db73e640d935060e98803f2cbbbf748e41a75826/input/taproot.go#L61 for an example of a correct control block.

// It looks like you want to sign for the script spend path, in which case you need to use txscript.RawTxInTapscriptSignature instead
sig, err := txscript.RawTxInTaprootSignature(
lenhatquang97 commented 1 year ago

So I want to do this following:

  1. Send A (address P2PKH) to B (Taproot)
  2. Send B (Taproot) to C (Taproot) (For commit transaction)
  3. Send C to B (For reveal transaction) So what kind of B script I want to include? maybe scriptPubKey OP_CHECKSIG? And what kind of C script I want to include?

Thank you very much for your help!

lenhatquang97 commented 1 year ago

Also, I don't know how to use RawTxInTapscriptSignature and NewCannedPrevOutputFetcher. How do I use this?

guggero commented 1 year ago

You can skip step 1, you can send directly from the p2wpkh to the commitment transaction. And then the reveal transaction spends from the commitment transaction, and the output can be a p2wpkh address again. So p2wpkh -> p2tr -> p2wpkh (just to keep things simple to understand). Let's call the first TX (p2wpkh -> p2tr) COMMIT and the second TX (p2tr -> p2wpkh) REVEAL.

I think what you're struggling with is that the output script in the COMMIT must already be the script you want to reveal. So the taproot output key of COMMIT must be constructed the same way as the input witness script and control block in the REVEAL transaction.

So what kind of B script I want to include? maybe scriptPubKey OP_CHECKSIG?

I don't know, what kind of script do you want to reveal?

Also, I don't know how to use RawTxInTapscriptSignature and NewCannedPrevOutputFetcher. How do I use this?

Here's an example: https://github.com/lightningnetwork/lnd/blob/4da26fb65a669fbee68fa36e60259a8da8ef6d3b/lnwallet/btcwallet/psbt.go#L410

lenhatquang97 commented 1 year ago

Ah, sorry. A is P2PKH because it uses SignatureScript in simnet with address SeZdpbs8WBuPHMZETPWajMeXZt1xzCJNAJ

lenhatquang97 commented 1 year ago

In fact, I am struggling in verifying the witness in the commit transaction. I think that I am wrong in signing part but I don't know how to fix this problem.

func StartTapTree(client *rpcclient.Client, keyPair *btcutil.WIF, data []byte, hash *chainhash.Hash, index uint32, commitOutput *wire.TxOut) error {
    hashLockKeypair, err := MakeRandomKeyPair()
    if err != nil {
        return err
    }

    builder := txscript.NewScriptBuilder()
    builder.AddData(hashLockKeypair.PrivKey.PubKey().X().Bytes())
    builder.AddOp(txscript.OP_CHECKSIG)
    hashLockScript, err := builder.Script()
    if err != nil {
        return err
    }

    var allTreeLeaves []txscript.TapLeaf
    tapLeaf := txscript.NewBaseTapLeaf(hashLockScript)
    allTreeLeaves = append(allTreeLeaves, tapLeaf)
    tapTree := TapscriptFullTree(keyPair.PrivKey.PubKey(), allTreeLeaves...)
    taprootKey, err := tapTree.TaprootKey()
    if err != nil {
        return err
    }
    scriptAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(taprootKey), &chaincfg.SimNetParams)
    if err != nil {
        return err
    }

    //Commit transaction
    commitTx, err := NewTx()
    if err != nil {
        return err
    }
    commitTx.AddTxIn(&wire.TxIn{
        PreviousOutPoint: *wire.NewOutPoint(hash, index),
    })
    scriptAddrScript, _ := txscript.PayToAddrScript(scriptAddr)
    commitTx.AddTxOut(&wire.TxOut{
        Value:    commitOutput.Value * 80 / 100,
        PkScript: scriptAddrScript,
    })

    inputFetcher := txscript.NewCannedPrevOutputFetcher(scriptAddrScript, commitOutput.Value*80/100)
    sigHashes := txscript.NewTxSigHashes(commitTx, inputFetcher)

    sig, err := txscript.RawTxInTapscriptSignature(commitTx, sigHashes, 0, commitOutput.Value*80/100, scriptAddrScript, tapLeaf, txscript.SigHashDefault, keyPair.PrivKey)
    if err != nil {
        return err
    }

    controlBlock, err := tapTree.ControlBlock.ToBytes()
    if err != nil {
        return err
    }
    commitTx.TxIn[0].Witness = wire.TxWitness{sig, hashLockScript, controlBlock}

    finalHash, err := client.SendRawTransaction(commitTx, false)
    if err != nil {
        return err
    }
    fmt.Println(finalHash)
    return nil
}

func MakeRandomKeyPair() (*btcutil.WIF, error) {
    randPriv, err := btcec.NewPrivateKey()
    if err != nil {
        return nil, err
    }
    wif, err := btcutil.NewWIF(randPriv, &chaincfg.SimNetParams, true)
    if err != nil {
        return nil, err
    }
    return wif, nil
}
guggero commented 1 year ago

How are you calling StartTapTree? And what does the pkScript of the output look like that you're spending here? Meaning: How do you create the output that you are referencing here with the PreviousOutPoint? You need to send to the same address as you're creating here (scriptAddr).

lenhatquang97 commented 1 year ago

So I have the solution there, thank you for your help. A (any address) -> B -> C (any address). So B must be the taproot address. In A -> B, I create script so that I have B address with script OP_1 schnorrPubKey. In B -> C, I create the script to unlock with schnorrPubKey OP_CHECKSIG. The key to this solution is that B in two phases must be the same address, only different from the script. Details of the source are available here: https://github.com/lenhatquang97/bitcoin_nft_v2