psbt: make Taproot PSBT finalizable

This commit is contained in:
Oliver Gugger 2022-05-02 16:25:17 +02:00
parent 0572702cec
commit 886a8f41db
No known key found for this signature in database
GPG key ID: 8E4256593F177720
2 changed files with 153 additions and 10 deletions

View file

@ -12,7 +12,10 @@ package psbt
// multisig and no other custom script. // multisig and no other custom script.
import ( import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
) )
// isFinalized considers this input finalized if it contains at least one of // isFinalized considers this input finalized if it contains at least one of
@ -32,16 +35,36 @@ func isFinalizableWitnessInput(pInput *PInput) bool {
// If this is a native witness output, then we require both // If this is a native witness output, then we require both
// the witness script, but not a redeem script. // the witness script, but not a redeem script.
case txscript.IsWitnessProgram(pkScript): case txscript.IsWitnessProgram(pkScript):
if txscript.IsPayToWitnessScriptHash(pkScript) { switch {
case txscript.IsPayToWitnessScriptHash(pkScript):
if pInput.WitnessScript == nil || if pInput.WitnessScript == nil ||
pInput.RedeemScript != nil { pInput.RedeemScript != nil {
return false return false
} }
} else {
case txscript.IsPayToTaproot(pkScript):
if pInput.TaprootKeySpendSig == nil &&
pInput.TaprootScriptSpendSig == nil {
return false
}
// For each of the script spend signatures we need a
// corresponding tap script leaf with the control block.
for _, sig := range pInput.TaprootScriptSpendSig {
_, err := FindLeafScript(pInput, sig.LeafHash)
if err != nil {
return false
}
}
default:
// A P2WKH output on the other hand doesn't need // A P2WKH output on the other hand doesn't need
// neither a witnessScript or redeemScript. // neither a witnessScript or redeemScript.
if pInput.WitnessScript != nil || if pInput.WitnessScript != nil ||
pInput.RedeemScript != nil { pInput.RedeemScript != nil {
return false return false
} }
} }
@ -67,8 +90,8 @@ func isFinalizableWitnessInput(pInput *PInput) bool {
return false return false
} }
// If this isn't a nested nested P2SH output or a native witness // If this isn't a nested P2SH output or a native witness output, then
// output, then we can't finalize this input as we don't understand it. // we can't finalize this input as we don't understand it.
default: default:
return false return false
} }
@ -106,8 +129,10 @@ func isFinalizableLegacyInput(p *Packet, pInput *PInput, inIndex int) bool {
func isFinalizable(p *Packet, inIndex int) bool { func isFinalizable(p *Packet, inIndex int) bool {
pInput := p.Inputs[inIndex] pInput := p.Inputs[inIndex]
// The input cannot be finalized without any signatures // The input cannot be finalized without any signatures.
if pInput.PartialSigs == nil { if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil &&
pInput.TaprootScriptSpendSig == nil {
return false return false
} }
@ -159,7 +184,6 @@ func MaybeFinalize(p *Packet, inIndex int) (bool, error) {
// MaybeFinalizeAll attempts to finalize all inputs of the psbt.Packet that are // MaybeFinalizeAll attempts to finalize all inputs of the psbt.Packet that are
// not already finalized, and returns an error if it fails to do so. // not already finalized, and returns an error if it fails to do so.
func MaybeFinalizeAll(p *Packet) error { func MaybeFinalizeAll(p *Packet) error {
for i := range p.UnsignedTx.TxIn { for i := range p.UnsignedTx.TxIn {
success, err := MaybeFinalize(p, i) success, err := MaybeFinalize(p, i)
if err != nil || !success { if err != nil || !success {
@ -184,9 +208,19 @@ func Finalize(p *Packet, inIndex int) error {
// witness or legacy UTXO. // witness or legacy UTXO.
switch { switch {
case pInput.WitnessUtxo != nil: case pInput.WitnessUtxo != nil:
pkScript := pInput.WitnessUtxo.PkScript
switch {
case txscript.IsPayToTaproot(pkScript):
if err := finalizeTaprootInput(p, inIndex); err != nil {
return err
}
default:
if err := finalizeWitnessInput(p, inIndex); err != nil { if err := finalizeWitnessInput(p, inIndex); err != nil {
return err return err
} }
}
case pInput.NonWitnessUtxo != nil: case pInput.NonWitnessUtxo != nil:
if err := finalizeNonWitnessInput(p, inIndex); err != nil { if err := finalizeNonWitnessInput(p, inIndex); err != nil {
@ -460,3 +494,87 @@ func finalizeWitnessInput(p *Packet, inIndex int) error {
p.Inputs[inIndex] = *newInput p.Inputs[inIndex] = *newInput
return nil return nil
} }
// finalizeTaprootInput attempts to create PsbtInFinalScriptWitness field for
// input at index inIndex, and removes all other fields except for the utxo
// field, for an input of type p2tr, or returns an error.
func finalizeTaprootInput(p *Packet, inIndex int) error {
// If this input has already been finalized, then we'll return an error
// as we can't proceed.
if checkFinalScriptSigWitness(p, inIndex) {
return ErrInputAlreadyFinalized
}
// Any p2tr input will only have a witness script, no sig script.
var (
serializedWitness []byte
err error
pInput = &p.Inputs[inIndex]
)
// What spend path did we take?
switch {
// Key spend path.
case len(pInput.TaprootKeySpendSig) > 0:
serializedWitness, err = writeWitness(pInput.TaprootKeySpendSig)
// Script spend path.
case len(pInput.TaprootScriptSpendSig) > 0:
var witnessStack wire.TxWitness
// If there are multiple script spend signatures, we assume they
// are from multiple signing participants for the same leaf
// script that uses OP_CHECKSIGADD for multi-sig. Signing
// multiple possible execution paths at the same time is
// currently not supported by this library.
targetLeafHash := pInput.TaprootScriptSpendSig[0].LeafHash
leafScript, err := FindLeafScript(pInput, targetLeafHash)
if err != nil {
return fmt.Errorf("control block for script spend " +
"signature not found")
}
// The witness stack will contain all signatures, followed by
// the script itself and then the control block.
for idx, scriptSpendSig := range pInput.TaprootScriptSpendSig {
// Make sure that if there are indeed multiple
// signatures, they all reference the same leaf hash.
if !bytes.Equal(scriptSpendSig.LeafHash, targetLeafHash) {
return fmt.Errorf("script spend signature %d "+
"references different target leaf "+
"hash than first signature; only one "+
"script path is supported", idx)
}
sig := append([]byte{}, scriptSpendSig.Signature...)
if scriptSpendSig.SigHash != txscript.SigHashDefault {
sig = append(sig, byte(scriptSpendSig.SigHash))
}
witnessStack = append(witnessStack, sig)
}
// Complete the witness stack with the executed script and the
// serialized control block.
witnessStack = append(witnessStack, leafScript.Script)
witnessStack = append(witnessStack, leafScript.ControlBlock)
serializedWitness, err = writeWitness(witnessStack...)
default:
return ErrInvalidPsbtFormat
}
if err != nil {
return err
}
// At this point, a witness has been constructed. Remove all fields
// other than witness utxo (01) and finalscriptsig (07),
// finalscriptwitness (08).
newInput := NewPsbtInput(nil, pInput.WitnessUtxo)
newInput.FinalScriptWitness = serializedWitness
// Finally, we overwrite the entry in the input list at the correct
// index.
p.Inputs[inIndex] = *newInput
return nil
}

View file

@ -35,9 +35,14 @@ func WriteTxWitness(w io.Writer, wit [][]byte) error {
// writePKHWitness writes a witness for a p2wkh spending input // writePKHWitness writes a witness for a p2wkh spending input
func writePKHWitness(sig []byte, pub []byte) ([]byte, error) { func writePKHWitness(sig []byte, pub []byte) ([]byte, error) {
return writeWitness(sig, pub)
}
// writeWitness serializes a witness stack from the given items.
func writeWitness(stackElements ...[]byte) ([]byte, error) {
var ( var (
buf bytes.Buffer buf bytes.Buffer
witnessItems = [][]byte{sig, pub} witnessItems = append([][]byte{}, stackElements...)
) )
if err := WriteTxWitness(&buf, witnessItems); err != nil { if err := WriteTxWitness(&buf, witnessItems); err != nil {
@ -420,3 +425,23 @@ func NewFromSignedTx(tx *wire.MsgTx) (*Packet, [][]byte,
} }
return unsignedPsbt, scriptSigs, witnesses, nil return unsignedPsbt, scriptSigs, witnesses, nil
} }
// FindLeafScript attempts to locate the leaf script of a given target Tap Leaf
// hash in the list of leaf scripts of the given input.
func FindLeafScript(pInput *PInput,
targetLeafHash []byte) (*TaprootTapLeafScript, error) {
for _, leaf := range pInput.TaprootLeafScript {
leafHash := txscript.TapLeaf{
LeafVersion: leaf.LeafVersion,
Script: leaf.Script,
}.TapHash()
if bytes.Equal(targetLeafHash, leafHash[:]) {
return leaf, nil
}
}
return nil, fmt.Errorf("leaf script for target leaf hash %x not "+
"found in input", targetLeafHash)
}