btcsuite / btcd

An alternative full node bitcoin implementation written in Go (golang)
https://github.com/btcsuite/btcd/blob/master/README.md
ISC License
6.26k stars 2.37k forks source link

Problem about making modular transaction with psbt #2004

Closed Eoous closed 1 year ago

Eoous commented 1 year ago

I want to create 2 of 2 multisig tx with psbt. But got error: non-mandatory-script-verify-flag (Invalid Schnorr signature)

Here are steps, psbt1 and psbt2 are different instances:

  1. create input0 and output0 in psbt1
  2. call txscript.TaprootWitnessSignature with txscript.SigHashSingle|txscript.SigHashAnyOneCanPay and use private_key1
  3. updater.Upsbt.Inputs[0].TaprootKeySpendSig = witness from 2
  4. create input0 and output0 in psbt2
  5. append psbt1.input0 as psbt2.input1, append psbt1.output0 as psbt2.output1, also append into UnsignedTx
  6. call txscript.TaprootWitnessSignature with txscript.SigHashDefault and use another private_key
  7. updater.Upsbt.Inputs[0].TaprootKeySpendSig = witness from 6
  8. psbt2.Finalize(0) and psbt2.Finalize(1)
  9. Extract

Then got error.

Here is codes of 5:

psbt2.Inputs = append(psbt2.Inputs, psbt1.Inputs[0])
psbt2.UnsignedTx.TxIn = append(psbt2.UnsignedTx.TxIn, psbt1.UnsignedTx.TxIn[0])
psbt2.UnsignedTx.TxIn[1].SignatureScript = nil
psbt2.Outputs = append(psbt2.Outputs, psbt1.Outputs[0])
psbt2.UnsignedTx.TxOut = append(psbt2.UnsignedTx.TxOut, psbt1.UnsignedTx.TxOut[0])
Roasbeef commented 1 year ago

Look at the sighashes you're signing (you can insert print statements to see why they differ before the sig msg is hashed). If they're different, then that's why you get an invalid final signature. Also look at the ordering of the keys in the script as well: tapscript doesn't have a normal multi-sig op code, so you either need to use checksigadd or a normal checksig.

Eoous commented 1 year ago

Look at the sighashes you're signing (you can insert print statements to see why they differ before the sig msg is hashed). If they're different, then that's why you get an invalid final signature. Also look at the ordering of the keys in the script as well: tapscript doesn't have a normal multi-sig op code, so you either need to use checksigadd or a normal checksig.

Printed sighashes, they differ. But they generated from different UnsignedTx and OutFetcher, they should be same?

sighashes1:

3f1b201fa7d5793579c08ffb394f76132f2879f9fc4a4057d5263cb0a1abea87
0efd7b36e8b7ac90f08be47960f7a9a626cf4fb17f47afb1c099b7c01b1395ad
f022effa4e64b6a83efb44363f1823e297addbeabe420dfb81f4517e8862905c
b429a131e02198a3c0d252c4e7a1ff05c35fd982180ee5b158b8c5fd477d89e7
161ce34235a6d9212f1aac85096bacb3de11fec85967833b996f48d37614b04f

sighashes2:

f5ba73ad25860cbbbfb25d036a22cb7437900b09baf4d36bee1cab48e189714c
caf0acbb5b59fb577e5ef02af8c429ec2d36330d65d078ee5dce615644aea312
0e325a2c5b8ed4a7eb0e6efdbf97229331acd35235e0fca20a893eded16e42e5
44fb6442528119a34933596c1b5ecb49b0908fb14f7a887dfdc159c28c14a04d
b2df0d6cb74a90c9f73c4bafa597605438385ce86113ee0c6e58b287b5d03546

For result, there're 2 input(first from psbt2, second from psbt1) and 2 output(first from psbt2, second from psbt1).

Alice wants to sell something and receive bitcoin(psbt1, use Single|AnyOneCanPay). Then Bob pays(psbt2, Default) and gets change. They're both p2tr addresses. I want to figure out how psbt2 can merge pre-signed psbt1.

Perhaps this's not multi-sig? I don't know what to call it.

Eoous commented 1 year ago

All codes:

func TestModularTransfer(t *testing.T) {
    value := 7083
    c, err := rpcclient.New(&rpcclient.ConnConfig{
        Host:         "testnet",
        User:         "user",
        Pass:         "pass",
        HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode
        DisableTLS:   true,
    }, nil)
    if err != nil {
        log.Fatalln(err)
    }

    network := &chaincfg.TestNet3Params
    var sellerPrevOutputFetcher *txscript.MultiPrevOutFetcher
    b := func() string {
        var inputsTxInput []*TxInput
        inputsTxInput = append(inputsTxInput, &TxInput{
            TxId:       "dee67d15626a3f651236da375686ae3b3e14e2b514c5876653c8dbe67b9e2c11",
            VOut:       uint32(0),
            Amount:     int64(435),
            Address:    "tb1pt753u3nvyeny3609suzl0wc8cqdqx978qmp7pcasr6kvr3kluqhqaqyq8n",
            PrivateKey: "priv",
        })

        var outputsTxOutput []*TxOutput
        outputsTxOutput = append(outputsTxOutput, &TxOutput{
            Address: "tb1pt753u3nvyeny3609suzl0wc8cqdqx978qmp7pcasr6kvr3kluqhqaqyq8n",
            Amount:  int64(value - 1000),
        })

        var inputs []*wire.OutPoint
        var nSequences []uint32
        prevOuts := make(map[wire.OutPoint]*wire.TxOut)
        for _, in := range inputsTxInput {
            txHash, err := chainhash.NewHashFromStr(in.TxId)
            if err != nil {
                panic(err)
            }
            prevOut := wire.NewOutPoint(txHash, in.VOut)
            inputs = append(inputs, prevOut)

            prevPkScript, err := AddrToPkScript(in.Address, network)
            if err != nil {
                panic(err)
            }
            witnessUtxo := wire.NewTxOut(in.Amount, prevPkScript)
            prevOuts[*prevOut] = witnessUtxo

            nSequences = append(nSequences, wire.MaxTxInSequenceNum)
        }

        var outputs []*wire.TxOut
        for _, out := range outputsTxOutput {
            pkScript, err := AddrToPkScript(out.Address, network)
            if err != nil {
                panic(err)
            }
            outputs = append(outputs, wire.NewTxOut(out.Amount, pkScript))
        }

        bp, err := psbt.New(inputs, outputs, txVersion, nLockTime, nSequences)
        if err != nil {
            panic(err)
        }

        updater, err := psbt.NewUpdater(bp)
        sellerPrevOutputFetcher = txscript.NewMultiPrevOutFetcher(prevOuts)

        for i, in := range inputsTxInput {
            if err = signInput(updater, i, in, sellerPrevOutputFetcher, txscript.SigHashSingle|txscript.SigHashAnyOneCanPay, network); err != nil {
                panic(err)
            }
        }

        var buf bytes.Buffer
        err = bp.Serialize(&buf)
        if err != nil {
            panic(err)
        }
        buf.Reset()
        b, err := bp.B64Encode()
        fmt.Println("is completed: ", bp.IsComplete())

        return b
    }()

    preSignedpsbt, err := psbt.NewFromRawBytes(strings.NewReader(b), true)
    if err != nil {
        panic(err)
    }

    var inputsTxInput []*TxInput
    inputsTxInput = append(inputsTxInput, &TxInput{
        TxId:       "fa9b34b0b4bfd27910c2e337a31a54a1f8d7e91f0981b8a9cf0b9123753e441c",
        VOut:       uint32(1),
        Amount:     int64(value),
        Address:    "tb1pns05k7ts67gvnxmjvk6n4hkqx4ghpr7h6e7m0q6el8z89lny9tgs6z6dhx",
        PrivateKey: "priv",
    })

    var outputsTxOutput []*TxOutput

    outputsTxOutput = append(outputsTxOutput, &TxOutput{
        Address: "tb1pns05k7ts67gvnxmjvk6n4hkqx4ghpr7h6e7m0q6el8z89lny9tgs6z6dhx",
        Amount:  int64(435),
    })

    var inputs []*wire.OutPoint
    var nSequences []uint32
    prevOuts := make(map[wire.OutPoint]*wire.TxOut)
    for _, in := range inputsTxInput {
        txHash, err := chainhash.NewHashFromStr(in.TxId)
        if err != nil {
            panic(err)
        }
        prevOut := wire.NewOutPoint(txHash, in.VOut)
        inputs = append(inputs, prevOut)

        prevPkScript, err := AddrToPkScript(in.Address, network)
        if err != nil {
            panic(err)
        }
        witnessUtxo := wire.NewTxOut(in.Amount, prevPkScript)
        prevOuts[*prevOut] = witnessUtxo

        nSequences = append(nSequences, wire.MaxTxInSequenceNum)
    }

    var outputs []*wire.TxOut
    for _, out := range outputsTxOutput {
        pkScript, err := AddrToPkScript(out.Address, network)
        if err != nil {
            panic(err)
        }
        outputs = append(outputs, wire.NewTxOut(out.Amount, pkScript))
    }

    bp, err := psbt.New(inputs, outputs, txVersion, nLockTime, nSequences)

    bp.Inputs = append(bp.Inputs, preSignedpsbt.Inputs[0])
    bp.UnsignedTx.TxIn = append(bp.UnsignedTx.TxIn, preSignedpsbt.UnsignedTx.TxIn[0])
    bp.UnsignedTx.TxIn[1].SignatureScript = nil
    bp.Outputs = append(bp.Outputs, preSignedpsbt.Outputs[0])
    bp.UnsignedTx.TxOut = append(bp.UnsignedTx.TxOut, preSignedpsbt.UnsignedTx.TxOut[0])

    updater, err := psbt.NewUpdater(bp)
    if err != nil {
        panic(err)
    }

    prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
    prevOutputFetcher.Merge(sellerPrevOutputFetcher)

    for i, in := range inputsTxInput {
        if err = signInput(updater, i, in, prevOutputFetcher, txscript.SigHashDefault, network); err != nil {
            panic(err)
        }

        if err = psbt.Finalize(bp, i); err != nil {
            panic(err)
        }
        if err = psbt.Finalize(bp, i+1); err != nil {
            panic(err)
        }
    }

    buyerSignedTx, err := psbt.Extract(bp)
    if err != nil {
        panic(err)
    }

    var buf bytes.Buffer
    if err = buyerSignedTx.Serialize(&buf); err != nil {
        panic(err)
    }

    fmt.Println("is completed: ", bp.IsComplete())
    fmt.Println(hex.EncodeToString(buf.Bytes()))

    hash, err := c.SendRawTransaction(buyerSignedTx, false)
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(hash.String())
}

func signInput(updater *psbt.Updater, i int, in *TxInput, prevOutFetcher *txscript.MultiPrevOutFetcher, hashType txscript.SigHashType, network *chaincfg.Params) error {
    wif, err := btcutil.DecodeWIF(in.PrivateKey)
    if err != nil {
        return err
    }
    privKey := wif.PrivKey

    prevPkScript, err := AddrToPkScript(in.Address, network)
    if err != nil {
        return err
    }

    witnessUtxo := wire.NewTxOut(in.Amount, prevPkScript)
    err = updater.AddInWitnessUtxo(witnessUtxo, i)
    if err != nil {
        return err
    }

    if err = updater.AddInSighashType(hashType, i); err != nil {
        return err
    }

    internalPubKey := schnorr.SerializePubKey(privKey.PubKey())
    updater.Upsbt.Inputs[i].TaprootInternalKey = internalPubKey

    sigHashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevOutFetcher)
    if hashType == txscript.SigHashAll {
        hashType = txscript.SigHashDefault
    }

    witness, err := txscript.TaprootWitnessSignature(updater.Upsbt.UnsignedTx, sigHashes,
        i, in.Amount, prevPkScript, hashType, privKey)
    if err != nil {
        return err
    }

    updater.Upsbt.Inputs[i].TaprootKeySpendSig = witness[0]
    return nil
}

func AddrToPkScript(addr string, network *chaincfg.Params) ([]byte, error) {
    address, err := btcutil.DecodeAddress(addr, network)
    if err != nil {
        return nil, err
    }

    return txscript.PayToAddrScript(address)
}
guggero commented 1 year ago

Can you print the two extracted raw transactions please? Did you try if things work if you use txscript.SigHashDefault for both PSBTs?

Eoous commented 1 year ago

Can you print the two extracted raw transactions please? Did you try if things work if you use txscript.SigHashDefault for both PSBTs?

Only extracted second psbt: 020000000001021c443e7523910bcfa9b881091fe9d7f8a1541aa337e3c21079d2bfb4b0349bfa0100000000ffffffff112c9e7be6dbc8536687c514b5e2143e3bae865637da3612653f6a62157de6de0000000000ffffffff02b3010000000000002251209c1f4b7970d790c99b7265b53adec03551708fd7d67db78359f9c472fe642ad1d5000000000000002251205fa91e466c266648e9e58705f7bb07c01a0317c706c3e0e3b01eacc1c6dfe02e0140e013c9225b2b406cefc2ed981d88468173189c7b1f9710154e5e5a60fcfd48300cd52bdb24fcf241f0949ed0b4d302a21ca3716a97e5e6709f9a320c51e83752014051fc7ce9402fab8633f200bb37277f8363467dfa57f0006eeae388c6d76e35608745e3948c9ffadd545e99ba63498823c8669f2b1ab62a0ca9c1877680b25b7600000000

Got same error by using txscript.SigHashDefault for both PSBTs. For first psbt, I need to finalize and extract it?

If i finalize and extract first psbt(use txscript.SigHashDefault) and boardcast it, got error: bad-txns-in-belowout, value in (0.00000435) < value out (0.00006083). That's right.

guggero commented 1 year ago

Ah, I see what you're attempting to do now. And looking at the TX you sent, it is missing the sighash flag at the end of the signature. So that gets lost somewhere when splicing together the two transactions.

Eoous commented 1 year ago

Ah, I see what you're attempting to do now. And looking at the TX you sent, it is missing the sighash flag at the end of the signature. So that gets lost somewhere when splicing together the two transactions.

I tried to add sighash flag,but not working:

hashtype := txscript.SigHashSingle | txscript.SigHashAnyOneCanPay
updater.AddInSighashType(hashtype, 1)

I changed value of output and tried to add sighash flag, here is new extracted tx: 020000000001021c443e7523910bcfa9b881091fe9d7f8a1541aa337e3c21079d2bfb4b0349bfa0100000000ffffffff112c9e7be6dbc8536687c514b5e2143e3bae865637da3612653f6a62157de6de0000000000ffffffff02b3010000000000002251209c1f4b7970d790c99b7265b53adec03551708fd7d67db78359f9c472fe642ad1c3170000000000002251205fa91e466c266648e9e58705f7bb07c01a0317c706c3e0e3b01eacc1c6dfe02e0140cc92b428180daa19c892becbbd9ae691023f377c17795709daee820485609448e9d22cff2e25409faa6e8afaf6f977ca05d0fe3504ef982660fcd8c2ee2f57730140892b736b69f534d74268f2cf47ce193361e5b3dc5c64ff1a8f8a0d8a5fbb2adafa405f9d3ab2aab5e65d8e3199aec63d39140c4eeb6c212b2764024741061ab300000000

Looks that also there isn't a sighash flag at end of signature.

guggero commented 1 year ago

Try adding it to the witness manually. I think this might be a bug in the PSBT library that the sighash is ignored when finalizing.

EDIT: This here should take into account the sighash flag: https://github.com/btcsuite/btcd/blob/2dbc98bdf3dc3f72a749c246cd0171bdd7abafd2/btcutil/psbt/finalizer.go#L522

Eoous commented 1 year ago

Try adding it to the witness manually. I think this might be a bug in the PSBT library that the sighash is ignored when finalizing.

EDIT: This here should take into account the sighash flag:

https://github.com/btcsuite/btcd/blob/2dbc98bdf3dc3f72a749c246cd0171bdd7abafd2/btcutil/psbt/finalizer.go#L522

It worked, thank you very much!