From 27416caca98517b6ef4b5fd3a25eafbbb4610a80 Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 29 Apr 2026 13:02:04 +0200 Subject: [PATCH 1/7] psbt: add new PSBT_GLOBAL_GENERIC_SINGED_MESSAGE field This new field is required to signal to signers that the PSBT being signed is actually for producing a BIP-0322 generic message signature. --- psbt/psbt.go | 51 ++++++++++++++++++++++++++++++++++++++++------- psbt/psbt_test.go | 4 ++++ psbt/types.go | 7 +++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/psbt/psbt.go b/psbt/psbt.go index d8e39f8ce5..3f69c21691 100644 --- a/psbt/psbt.go +++ b/psbt/psbt.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "errors" "io" + "unicode/utf8" "github.com/btcsuite/btcd/btcutil/v2" "github.com/btcsuite/btcd/wire/v2" @@ -142,6 +143,11 @@ type Packet struct { // derived. XPubs []XPub + // GenericSignedMessage contains a BIP-0322 generic message to be + // signed. An empty message is a valid message, that's why this field + // is a nillable string. + GenericSignedMessage *string + // Unknowns are the set of custom types (global only) within this PSBT. Unknowns []*Unknown } @@ -238,8 +244,9 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) { // Next we parse any unknowns that may be present, making sure that we // break at the separator. var ( - xPubSlice []XPub - unknownSlice []*Unknown + xPubSlice []XPub + genericSignedMessage *string + unknownSlice []*Unknown ) for { keyint, keydata, err := getKey(r) @@ -273,6 +280,24 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) { xPubSlice = append(xPubSlice, *xPub) + case GenericSignedMessageType: + // Validate that there is no keydata and no duplicate + // message as mandated by BIP322. + if len(keydata) > 0 || genericSignedMessage != nil { + return nil, ErrInvalidPsbtFormat + } + + // Golang's default string encoding is UTF-8, so we + // don't need to convert anything. But we need to + // validate that the string only contains valid UTF-8 + // sequences. + if !utf8.Valid(value) { + return nil, ErrInvalidPsbtFormat + } + + messageString := string(value) + genericSignedMessage = &messageString + default: keyintanddata := []byte{byte(keyint)} keyintanddata = append(keyintanddata, keydata...) @@ -311,11 +336,12 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) { // Populate the new Packet object. newPsbt := Packet{ - UnsignedTx: msgTx, - Inputs: inSlice, - Outputs: outSlice, - XPubs: xPubSlice, - Unknowns: unknownSlice, + UnsignedTx: msgTx, + Inputs: inSlice, + Outputs: outSlice, + XPubs: xPubSlice, + GenericSignedMessage: genericSignedMessage, + Unknowns: unknownSlice, } // Extended sanity checking is applied here to make sure the @@ -394,6 +420,17 @@ func (p *Packet) Serialize(w io.Writer) error { } } + // Serialize the generic signed message. + if p.GenericSignedMessage != nil { + msgBytes := []byte(*p.GenericSignedMessage) + err := serializeKVPairWithType( + w, uint8(GenericSignedMessageType), nil, msgBytes, + ) + if err != nil { + return err + } + } + // Unknown is a special case; we don't have a key type, only a key and // a value field for _, kv := range p.Unknowns { diff --git a/psbt/psbt_test.go b/psbt/psbt_test.go index 8b8b3a6241..fe7fc8ede0 100644 --- a/psbt/psbt_test.go +++ b/psbt/psbt_test.go @@ -111,6 +111,8 @@ var validPsbtBase64 = map[int]string{ 7: "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA", // Case: PSBT with `PSBT_GLOBAL_XPUB`. 8: "cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA", + // Case: PSBT wich `PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE`. + 9: "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwABCS1VVEYtOCBzdXBwb3J0OiDDtsOkw7zDqcOgw6gg5rWL6K+V5paH5pysIPCfmIQAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA", } // These are all invalid PSBTs for the indicated reasons. @@ -185,6 +187,8 @@ var invalidPsbtBase64 = map[int]string{ 9: "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJjFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgAIyAssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20qzAAAA=", // Invalid input leaf script type control block. 10: "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJhFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4SMgLLE6xoJI3oBqpqNlnPPAPraCHQnIEUpOho/r3oZbttKswAAA", + // Invalid UTF-8 sequence in PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE. + 11: "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwABCQLDKAABAP2lAQEAAAAAAQKJo8ceq00g4Dcbu6TMaY+ilclGOvouOX+FM8y2L5Vn5QEAAAAXFgAUvhjRUqmwEgOdrz2n3k9TNJ7suYX/////hviqQ6cd/xRIiTpTCnI372tGCLuy3S0BceY67GpIkLQBAAAAFxYAFP4+nvGnRel02QLENVlDq8s0vVNT/////wIAwusLAAAAABl2qRSFz/EJf9ngCLs0r3CcYhl7OJeKSIiscv74TiwAAAAXqRQzlyW6Ie/WKsdTqbzQZ9bHpqOdBYcCRzBEAiAnEr4i4CcPOU9WgxHcfKmmiXC4Al/dOyQCKfB/il86JAIgAYs419zTFOc0ySdr1vtA9nMyW8S6oUTIANLy8C2ydlwBIQPS4VZ0lButSpljcsuH4YVtNlJgbZhWL+OcXp5+QT8hBQJIMEUCIQDRK4UthdzZYdL19KtmBlTfbu3MeUwMM85cwwn/tfzljQIgZzOKjg4XJcGX+xqIr1n1HkTkJVsgFnyGhAMcBdHyWSoBIQIjtyvu8JZdEL4HeO/s1h/KxveaTqFpOTOAc0Rk+E8qswAAAAAAAAA=", } // This tests that valid PSBT serializations can be parsed diff --git a/psbt/types.go b/psbt/types.go index ca555101b9..807b26265d 100644 --- a/psbt/types.go +++ b/psbt/types.go @@ -30,6 +30,13 @@ const ( // extended public key. XPubType GlobalType = 1 + // GenericSignedMessageType is used to house a BIP-0322 generic signed + // message, encoded as UTF-8. + // + // The key is {0x09} with no key data, and the value is the UTF-8 + // encoded message. + GenericSignedMessageType GlobalType = 0x09 + // VersionType houses the global version number of this PSBT. There is // no key (only contains the byte type), then the value if omitted, is // assumed to be zero. From 6093d81e5c4c96d5182f19b1ae5f9b10dc507515 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 3 Jul 2026 09:05:33 +0200 Subject: [PATCH 2/7] psbt: add Copy method, make serialization non-mutating Adds a convenience Copy() method to the Packet struct that creates a deep copy by serializing and deserializing it. Which means the packet must be valid before it can be copied. To avoid a packet being mutated by the serialization, we also make sure that any sorting of member fields only happens on copies of the slices. --- psbt/partial_input.go | 45 ++++++++++++-------- psbt/partial_output.go | 25 +++++++----- psbt/psbt.go | 15 +++++++ psbt/psbt_test.go | 10 +++++ psbt/serialize_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 29 deletions(-) create mode 100644 psbt/serialize_test.go diff --git a/psbt/partial_input.go b/psbt/partial_input.go index 8ac5d7d3ea..8ac136fbaf 100644 --- a/psbt/partial_input.go +++ b/psbt/partial_input.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "io" + "slices" "sort" "github.com/btcsuite/btcd/txscript/v2" @@ -423,8 +424,11 @@ func (pi *PInput) serialize(w io.Writer) error { } if pi.FinalScriptSig == nil && pi.FinalScriptWitness == nil { - sort.Sort(PartialSigSorter(pi.PartialSigs)) - for _, ps := range pi.PartialSigs { + // Sort a copy so that serialization does not mutate the + // caller's packet by reordering its signature slice. + partialSigs := slices.Clone(pi.PartialSigs) + sort.Sort(PartialSigSorter(partialSigs)) + for _, ps := range partialSigs { err := serializeKVPairWithType( w, uint8(PartialSigType), ps.PubKey, ps.Signature, @@ -468,11 +472,11 @@ func (pi *PInput) serialize(w io.Writer) error { } } - sort.Sort(Bip32Sorter(pi.Bip32Derivation)) - for _, kd := range pi.Bip32Derivation { + bip32Derivation := slices.Clone(pi.Bip32Derivation) + sort.Sort(Bip32Sorter(bip32Derivation)) + for _, kd := range bip32Derivation { err := serializeKVPairWithType( - w, - uint8(Bip32DerivationInputType), kd.PubKey, + w, uint8(Bip32DerivationInputType), kd.PubKey, SerializeBIP32Derivation( kd.MasterKeyFingerprint, kd.Bip32Path, ), @@ -492,12 +496,13 @@ func (pi *PInput) serialize(w io.Writer) error { } } - sort.Slice(pi.TaprootScriptSpendSig, func(i, j int) bool { - return pi.TaprootScriptSpendSig[i].SortBefore( - pi.TaprootScriptSpendSig[j], + taprootScriptSpendSig := slices.Clone(pi.TaprootScriptSpendSig) + sort.Slice(taprootScriptSpendSig, func(i, j int) bool { + return taprootScriptSpendSig[i].SortBefore( + taprootScriptSpendSig[j], ) }) - for _, scriptSpend := range pi.TaprootScriptSpendSig { + for _, scriptSpend := range taprootScriptSpendSig { keyData := append([]byte{}, scriptSpend.XOnlyPubKey...) keyData = append(keyData, scriptSpend.LeafHash...) value := append([]byte{}, scriptSpend.Signature...) @@ -513,12 +518,13 @@ func (pi *PInput) serialize(w io.Writer) error { } } - sort.Slice(pi.TaprootLeafScript, func(i, j int) bool { - return pi.TaprootLeafScript[i].SortBefore( - pi.TaprootLeafScript[j], + taprootLeafScript := slices.Clone(pi.TaprootLeafScript) + sort.Slice(taprootLeafScript, func(i, j int) bool { + return taprootLeafScript[i].SortBefore( + taprootLeafScript[j], ) }) - for _, leafScript := range pi.TaprootLeafScript { + for _, leafScript := range taprootLeafScript { value := append([]byte{}, leafScript.Script...) value = append(value, byte(leafScript.LeafVersion)) err := serializeKVPairWithType( @@ -530,12 +536,15 @@ func (pi *PInput) serialize(w io.Writer) error { } } - sort.Slice(pi.TaprootBip32Derivation, func(i, j int) bool { - return pi.TaprootBip32Derivation[i].SortBefore( - pi.TaprootBip32Derivation[j], + taprootBip32Derivation := slices.Clone( + pi.TaprootBip32Derivation, + ) + sort.Slice(taprootBip32Derivation, func(i, j int) bool { + return taprootBip32Derivation[i].SortBefore( + taprootBip32Derivation[j], ) }) - for _, derivation := range pi.TaprootBip32Derivation { + for _, derivation := range taprootBip32Derivation { value, err := SerializeTaprootBip32Derivation( derivation, ) diff --git a/psbt/partial_output.go b/psbt/partial_output.go index 94b5d33f42..743399c70f 100644 --- a/psbt/partial_output.go +++ b/psbt/partial_output.go @@ -3,6 +3,7 @@ package psbt import ( "bytes" "io" + "slices" "sort" "github.com/btcsuite/btcd/wire/v2" @@ -190,14 +191,15 @@ func (po *POutput) serialize(w io.Writer) error { } } - sort.Sort(Bip32Sorter(po.Bip32Derivation)) - for _, kd := range po.Bip32Derivation { - err := serializeKVPairWithType(w, - uint8(Bip32DerivationOutputType), - kd.PubKey, + // Sort a copy so that serialization does not mutate the caller's packet + // by reordering its derivation slice. + bip32Derivation := slices.Clone(po.Bip32Derivation) + sort.Sort(Bip32Sorter(bip32Derivation)) + for _, kd := range bip32Derivation { + err := serializeKVPairWithType( + w, uint8(Bip32DerivationOutputType), kd.PubKey, SerializeBIP32Derivation( - kd.MasterKeyFingerprint, - kd.Bip32Path, + kd.MasterKeyFingerprint, kd.Bip32Path, ), ) if err != nil { @@ -225,12 +227,13 @@ func (po *POutput) serialize(w io.Writer) error { } } - sort.Slice(po.TaprootBip32Derivation, func(i, j int) bool { - return po.TaprootBip32Derivation[i].SortBefore( - po.TaprootBip32Derivation[j], + taprootBip32Derivation := slices.Clone(po.TaprootBip32Derivation) + sort.Slice(taprootBip32Derivation, func(i, j int) bool { + return taprootBip32Derivation[i].SortBefore( + taprootBip32Derivation[j], ) }) - for _, derivation := range po.TaprootBip32Derivation { + for _, derivation := range taprootBip32Derivation { value, err := SerializeTaprootBip32Derivation( derivation, ) diff --git a/psbt/psbt.go b/psbt/psbt.go index 3f69c21691..d56f486d42 100644 --- a/psbt/psbt.go +++ b/psbt/psbt.go @@ -528,3 +528,18 @@ func (p *Packet) GetTxFee() (btcutil.Amount, error) { fee := sumInputs - sumOutputs return btcutil.Amount(fee), nil } + +// Copy creates a deep copy of the packet by serializing and deserializing it. +func (p *Packet) Copy() (*Packet, error) { + if p == nil { + return nil, nil + } + + var buf bytes.Buffer + err := p.Serialize(&buf) + if err != nil { + return nil, err + } + + return NewFromRawBytes(&buf, false) +} diff --git a/psbt/psbt_test.go b/psbt/psbt_test.go index fe7fc8ede0..5ee01b4e8b 100644 --- a/psbt/psbt_test.go +++ b/psbt/psbt_test.go @@ -214,6 +214,11 @@ func TestReadValidPsbtAndReserialize(t *testing.T) { } require.Equal(t, psbtBytes, b.Bytes(), "%d: serialized", key) + + // Make sure Copy creates a deep copy. + copiedPsbt, err := testPsbt.Copy() + require.NoError(t, err) + require.Equal(t, testPsbt, copiedPsbt) } for key, v := range validPsbtBase64 { @@ -234,6 +239,11 @@ func TestReadValidPsbtAndReserialize(t *testing.T) { base64Packet := base64.StdEncoding.EncodeToString(b.Bytes()) require.Equal(t, v, base64Packet, "%d: serialized", key) + + // Make sure Copy creates a deep copy. + copiedPsbt, err := testPsbt.Copy() + require.NoError(t, err) + require.Equal(t, testPsbt, copiedPsbt) } } diff --git a/psbt/serialize_test.go b/psbt/serialize_test.go new file mode 100644 index 0000000000..5a9a671ab3 --- /dev/null +++ b/psbt/serialize_test.go @@ -0,0 +1,93 @@ +package psbt + +import ( + "bytes" + "slices" + "testing" + + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +// TestSerializeDoesNotMutate asserts that serializing a packet has no side +// effect on it. Serialization sorts several input and output fields (partial +// sigs, BIP32 derivations, taproot data) to produce deterministic output; it +// must do so on copies, leaving the caller's packet untouched. +func TestSerializeDoesNotMutate(t *testing.T) { + t.Parallel() + + // buildPacket returns a packet whose every sortable input and output + // field is keyed by the given bytes, in the given order. + buildPacket := func(order []byte) *Packet { + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + tx.AddTxOut(&wire.TxOut{ + Value: 1000, PkScript: []byte{txscript.OP_1}, + }) + + var ( + partialSigs []*PartialSig + inBip32 []*Bip32Derivation + inTapBip32 []*TaprootBip32Derivation + outBip32 []*Bip32Derivation + ) + for _, b := range order { + compressed := bytes.Repeat([]byte{b}, 33) + xOnly := bytes.Repeat([]byte{b}, 32) + + partialSigs = append(partialSigs, &PartialSig{ + PubKey: compressed, Signature: []byte{b}, + }) + inBip32 = append(inBip32, &Bip32Derivation{ + PubKey: compressed, Bip32Path: []uint32{0}, + }) + inTapBip32 = append(inTapBip32, &TaprootBip32Derivation{ + XOnlyPubKey: xOnly, Bip32Path: []uint32{0}, + }) + outBip32 = append(outBip32, &Bip32Derivation{ + PubKey: compressed, Bip32Path: []uint32{0}, + }) + } + + return &Packet{ + UnsignedTx: tx, + Inputs: []PInput{{ + PartialSigs: partialSigs, + Bip32Derivation: inBip32, + TaprootBip32Derivation: inTapBip32, + }}, + Outputs: []POutput{{Bip32Derivation: outBip32}}, + } + } + + // The fields are deliberately in descending key order, the opposite of + // the ascending order that serialization sorts them into. + packet := buildPacket([]byte{0x03, 0x02, 0x01}) + + // Snapshot the field orderings before serializing. slices.Clone copies + // the slice headers into fresh backing arrays, so an in-place sort of + // the packet's own slices would show up as a difference below. + wantPartialSigs := slices.Clone(packet.Inputs[0].PartialSigs) + wantInBip32 := slices.Clone(packet.Inputs[0].Bip32Derivation) + wantInTapBip32 := slices.Clone(packet.Inputs[0].TaprootBip32Derivation) + wantOutBip32 := slices.Clone(packet.Outputs[0].Bip32Derivation) + + var buf bytes.Buffer + require.NoError(t, packet.Serialize(&buf)) + + // Serialization must not have reordered any of the packet's fields. + require.Equal(t, wantPartialSigs, packet.Inputs[0].PartialSigs) + require.Equal(t, wantInBip32, packet.Inputs[0].Bip32Derivation) + require.Equal( + t, wantInTapBip32, packet.Inputs[0].TaprootBip32Derivation, + ) + require.Equal(t, wantOutBip32, packet.Outputs[0].Bip32Derivation) + + // It must still produce deterministic, sorted output: the same data + // already in ascending order must serialize to identical bytes. + sorted := buildPacket([]byte{0x01, 0x02, 0x03}) + var sortedBuf bytes.Buffer + require.NoError(t, sorted.Serialize(&sortedBuf)) + require.Equal(t, sortedBuf.Bytes(), buf.Bytes()) +} From 2e91ffb3b114e36be1104b425fe5b95c9f5c453f Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 29 May 2026 18:12:14 +0200 Subject: [PATCH 3/7] txscript: add new ScriptVerifyRestrictSigHash flag This commit adds a new ScriptVerifyRestrictSigHash flag to the script engine that allows restricting signature checking to SIGHASH_ALL or SIGHASH_DEFAULT (for taproot) only. The script is _NOT_ part of the StandardVerifyFlags and needs to be specifically activated by a caller. --- txscript/engine.go | 69 ++++++++ txscript/error.go | 7 + txscript/error_test.go | 1 + txscript/opcode.go | 3 + txscript/restrict_sighash_test.go | 253 ++++++++++++++++++++++++++++++ txscript/sigvalidate.go | 10 ++ 6 files changed, 343 insertions(+) create mode 100644 txscript/restrict_sighash_test.go diff --git a/txscript/engine.go b/txscript/engine.go index 682924f791..8f0e5e4f62 100644 --- a/txscript/engine.go +++ b/txscript/engine.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/wire/v2" ) @@ -118,6 +119,17 @@ const ( // ScriptVerifyConstScriptCode fails non-segwit scripts if a signature // match is found in the script code or if OP_CODESEPARATOR is used. ScriptVerifyConstScriptCode + + // ScriptVerifyRestrictSigHash requires every signature checked during + // script execution to use SIGHASH_ALL or, for BIP-341 taproot + // signatures, the implicit SIGHASH_DEFAULT. A signature using any other + // sighash type fails the script with ErrDisallowedSigHashType. + // + // This is NOT a consensus rule and is not part of StandardVerifyFlags. + // It is an opt-in policy for applications such as BIP-322 message + // verification, which require that every signature commit to the entire + // transaction. + ScriptVerifyRestrictSigHash ) const ( @@ -698,6 +710,26 @@ func (vm *Engine) verifyWitnessProgram(witness wire.TxWitness) error { // removing the annex), we'll do normal taproot // keyspend validation. rawSig := witness[0] + + // Enforce the restricted-sighash policy (if active) + // before verifying the signature, so a disallowed + // sighash type is reported as such regardless of + // signature validity. This mirrors how the ECDSA, + // multisig, and tapscript paths check the policy at + // parse time. A well-formed key-spend signature is 64 + // bytes (the implicit SIGHASH_DEFAULT) or 65 bytes (an + // explicit sighash byte); any other length is left for + // VerifyTaprootKeySpend to reject. + keySpendSigHash := SigHashDefault + if len(rawSig) == schnorr.SignatureSize+1 { + keySpendSigHash = SigHashType( + rawSig[schnorr.SignatureSize], + ) + } + if err := vm.checkSigHashType(keySpendSigHash); err != nil { + return err + } + err := VerifyTaprootKeySpend( vm.witnessProgram, rawSig, &vm.tx, vm.txIdx, vm.prevOutFetcher, vm.hashCache, vm.sigCache, @@ -1222,6 +1254,43 @@ func (vm *Engine) checkHashTypeEncoding(hashType SigHashType) error { return nil } +// checkSigHashType enforces the ScriptVerifyRestrictSigHash policy. When the +// flag is set, only SIGHASH_DEFAULT (taproot) and SIGHASH_ALL are permitted; +// any other sighash type results in a script error. When the flag is not set +// this is a no-op. +// +// This is called from every signature-verification context (legacy and segwit +// ECDSA, multisig, taproot key-spend, and tapscript) right after the signature +// hash type becomes known, so it constrains every signature the engine +// actually verifies. +func (vm *Engine) checkSigHashType(hashType SigHashType) error { + if !vm.hasFlag(ScriptVerifyRestrictSigHash) { + return nil + } + + switch hashType { + // SIGHASH_DEFAULT is the implicit (64-byte) taproot form and is treated + // as SIGHASH_ALL, but is only required in a taproot context. + case SigHashDefault: + if vm.taprootCtx == nil { + str := fmt.Sprintf("SIGHASH_DEFAULT requires taproot") + return scriptError(ErrInvalidSigHashType, str) + } + + return nil + + // Explicit SIGHASH_ALL is permitted for all signature types. + case SigHashAll: + return nil + + default: + str := fmt.Sprintf("sighash type 0x%02x is not allowed; only "+ + "SIGHASH_DEFAULT and SIGHASH_ALL are permitted by "+ + "ScriptVerifyRestrictSigHash flag", hashType) + return scriptError(ErrDisallowedSigHashType, str) + } +} + // isStrictPubKeyEncoding returns whether or not the passed public key adheres // to the strict encoding requirements. func isStrictPubKeyEncoding(pubKey []byte) bool { diff --git a/txscript/error.go b/txscript/error.go index a5aaab1571..91dd02c1a0 100644 --- a/txscript/error.go +++ b/txscript/error.go @@ -416,6 +416,12 @@ const ( // non-segwit script. ErrCodeSeparator + // ErrDisallowedSigHashType is returned when the + // ScriptVerifyRestrictSigHash flag is active and a signature uses a + // sighash type other than SIGHASH_ALL (or SIGHASH_DEFAULT for taproot + // signatures). + ErrDisallowedSigHashType + // numErrorCodes is the maximum error code number used in tests. This // entry MUST be the last entry in the enum. numErrorCodes @@ -504,6 +510,7 @@ var errorCodeStrings = map[ErrorCode]string{ ErrTaprootMaxSigOps: "ErrTaprootMaxSigOps", ErrNonConstScriptCode: "ErrNonConstScriptCode", ErrCodeSeparator: "ErrCodeSeparator", + ErrDisallowedSigHashType: "ErrDisallowedSigHashType", } // String returns the ErrorCode as a human-readable name. diff --git a/txscript/error_test.go b/txscript/error_test.go index bb1f73e92e..28f5493fda 100644 --- a/txscript/error_test.go +++ b/txscript/error_test.go @@ -98,6 +98,7 @@ func TestErrorCodeStringer(t *testing.T) { {ErrTaprootMaxSigOps, "ErrTaprootMaxSigOps"}, {ErrNonConstScriptCode, "ErrNonConstScriptCode"}, {ErrCodeSeparator, "ErrCodeSeparator"}, + {ErrDisallowedSigHashType, "ErrDisallowedSigHashType"}, {0xffff, "Unknown ErrorCode (65535)"}, } diff --git a/txscript/opcode.go b/txscript/opcode.go index 2a1253910c..35bea9a5b1 100644 --- a/txscript/opcode.go +++ b/txscript/opcode.go @@ -2363,6 +2363,9 @@ func opcodeCheckMultiSig(op *opcode, data []byte, vm *Engine) error { if err := vm.checkHashTypeEncoding(hashType); err != nil { return err } + if err := vm.checkSigHashType(hashType); err != nil { + return err + } if err := vm.checkSignatureEncoding(signature); err != nil { return err } diff --git a/txscript/restrict_sighash_test.go b/txscript/restrict_sighash_test.go new file mode 100644 index 0000000000..8c6395b27f --- /dev/null +++ b/txscript/restrict_sighash_test.go @@ -0,0 +1,253 @@ +// Copyright (c) 2013-2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package txscript + +import ( + "crypto/sha256" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +// spendBuilder builds and executes a single-input spend of the given sighash +// type under the given script flags, returning the engine's Execute error. It +// is used to exercise the ScriptVerifyRestrictSigHash policy across the +// different signature-verification contexts. +type spendBuilder func(t *testing.T, hashType SigHashType, + flags ScriptFlags) error + +// newRestrictSigHashTx returns a fresh single-input, single-output transaction +// spending the given pkScript, along with a prevout fetcher and sighash cache. +// The single output makes SIGHASH_SINGLE valid for input 0. +func newRestrictSigHashTx(pkScript []byte, value int64) (*wire.MsgTx, + *CannedPrevOutputFetcher, *TxSigHashes) { + + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Index: 0}, + }) + tx.AddTxOut(&wire.TxOut{Value: value, PkScript: pkScript}) + + prevFetcher := NewCannedPrevOutputFetcher(pkScript, value) + sigHashes := NewTxSigHashes(tx, prevFetcher) + + return tx, prevFetcher, sigHashes +} + +// taprootKeySpendBuilder spends a BIP-86 taproot output via the key path. +func taprootKeySpendBuilder(t *testing.T, hashType SigHashType, + flags ScriptFlags) error { + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + outputKey := ComputeTaprootKeyNoScript(privKey.PubKey()) + pkScript, err := PayToTaprootScript(outputKey) + require.NoError(t, err) + + const value = 1e8 + tx, prevFetcher, sigHashes := newRestrictSigHashTx(pkScript, value) + + witness, err := TaprootWitnessSignature( + tx, sigHashes, 0, value, pkScript, hashType, privKey, + ) + require.NoError(t, err) + tx.TxIn[0].Witness = witness + + vm, err := NewEngine( + pkScript, tx, 0, flags, nil, sigHashes, value, prevFetcher, + ) + require.NoError(t, err) + + return vm.Execute() +} + +// tapscriptBuilder spends a taproot output via a script path whose leaf is a +// single OP_CHECKSIG. +func tapscriptBuilder(t *testing.T, hashType SigHashType, + flags ScriptFlags) error { + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + internalKey := privKey.PubKey() + leafScript, err := NewScriptBuilder(). + AddData(schnorr.SerializePubKey(internalKey)). + AddOp(OP_CHECKSIG). + Script() + require.NoError(t, err) + + tapLeaf := NewBaseTapLeaf(leafScript) + tree := AssembleTaprootScriptTree(tapLeaf) + ctrlBlock := tree.LeafMerkleProofs[0].ToControlBlock(internalKey) + rootHash := tree.RootNode.TapHash() + outputKey := ComputeTaprootOutputKey(internalKey, rootHash[:]) + pkScript, err := PayToTaprootScript(outputKey) + require.NoError(t, err) + + const value = 1e8 + tx, prevFetcher, sigHashes := newRestrictSigHashTx(pkScript, value) + + sig, err := RawTxInTapscriptSignature( + tx, sigHashes, 0, value, pkScript, tapLeaf, hashType, privKey, + ) + require.NoError(t, err) + + ctrlBlockBytes, err := ctrlBlock.ToBytes() + require.NoError(t, err) + tx.TxIn[0].Witness = wire.TxWitness{sig, leafScript, ctrlBlockBytes} + + vm, err := NewEngine( + pkScript, tx, 0, flags, nil, sigHashes, value, prevFetcher, + ) + require.NoError(t, err) + + return vm.Execute() +} + +// p2wshECDSABuilder spends a P2WSH output whose witness script is a single +// OP_CHECKSIG, exercising the segwit-v0 ECDSA verification path. +func p2wshECDSABuilder(t *testing.T, hashType SigHashType, + flags ScriptFlags) error { + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + witnessScript, err := NewScriptBuilder(). + AddData(privKey.PubKey().SerializeCompressed()). + AddOp(OP_CHECKSIG). + Script() + require.NoError(t, err) + + scriptHash := sha256.Sum256(witnessScript) + pkScript, err := NewScriptBuilder(). + AddOp(OP_0). + AddData(scriptHash[:]). + Script() + require.NoError(t, err) + + const value = 1e8 + tx, prevFetcher, sigHashes := newRestrictSigHashTx(pkScript, value) + + sig, err := RawTxInWitnessSignature( + tx, sigHashes, 0, value, witnessScript, hashType, privKey, + ) + require.NoError(t, err) + tx.TxIn[0].Witness = wire.TxWitness{sig, witnessScript} + + vm, err := NewEngine( + pkScript, tx, 0, flags, nil, sigHashes, value, prevFetcher, + ) + require.NoError(t, err) + + return vm.Execute() +} + +// TestDisallowSighashDefaultNonTaproot verifies that SIGHASH_DEFAULT (taproot) +// is not allowed for non-taproot scripts. +func TestDisallowSighashDefaultNonTaproot(t *testing.T) { + err := p2wshECDSABuilder( + t, SigHashDefault, ScriptBip16| + ScriptVerifyWitness|ScriptVerifyTaproot| + ScriptVerifyRestrictSigHash, + ) + require.Error(t, err) + require.True(t, IsErrorCode(err, ErrInvalidSigHashType)) +} + +// TestScriptVerifyRestrictSigHash verifies the ScriptVerifyRestrictSigHash +// policy across the taproot key-spend, tapscript, and segwit-v0 ECDSA +// signature-verification contexts. With the flag unset every sighash type is +// accepted (proving the policy is strictly opt-in); with the flag set only +// SIGHASH_DEFAULT (taproot) and SIGHASH_ALL are accepted, and any other type +// fails with ErrDisallowedSigHashType. +func TestScriptVerifyRestrictSigHash(t *testing.T) { + t.Parallel() + + // allowedSigHash reports whether a sighash type is permitted by the + // policy: SIGHASH_DEFAULT (taproot implicit) and SIGHASH_ALL only. + allowedSigHash := func(hashType SigHashType) bool { + return hashType == SigHashDefault || hashType == SigHashAll + } + + contexts := []struct { + name string + + // supportsDefault indicates whether the context can carry an + // implicit SIGHASH_DEFAULT (only taproot signatures can). + supportsDefault bool + + build spendBuilder + }{ + { + name: "taproot-keyspend", + supportsDefault: true, + build: taprootKeySpendBuilder, + }, + { + name: "tapscript", + supportsDefault: true, + build: tapscriptBuilder, + }, + { + name: "p2wsh-ecdsa", + supportsDefault: false, + build: p2wshECDSABuilder, + }, + } + + // The set of sighash types exercised. SIGHASH_DEFAULT is only valid for + // taproot signatures and is filtered out for the ECDSA context. + sigHashTypes := []SigHashType{ + SigHashDefault, + SigHashAll, + SigHashNone, + SigHashSingle, + SigHashAll | SigHashAnyOneCanPay, + SigHashNone | SigHashAnyOneCanPay, + SigHashSingle | SigHashAnyOneCanPay, + } + + for _, ctx := range contexts { + for _, hashType := range sigHashTypes { + if hashType == SigHashDefault && !ctx.supportsDefault { + continue + } + + name := fmt.Sprintf("%s/sighash=0x%02x", ctx.name, + hashType) + t.Run(name, func(t *testing.T) { + t.Parallel() + + // Without the policy flag, every sighash type + // must be accepted. + err := ctx.build(t, hashType, StandardVerifyFlags) + require.NoError(t, err, "spend should be valid "+ + "without the restrict-sighash flag") + + // With the policy flag, only SIGHASH_DEFAULT and + // SIGHASH_ALL are accepted. + err = ctx.build( + t, hashType, StandardVerifyFlags| + ScriptVerifyRestrictSigHash, + ) + if allowedSigHash(hashType) { + require.NoError(t, err, "allowed sighash "+ + "type must pass with the flag") + } else { + require.True(t, IsErrorCode( + err, ErrDisallowedSigHashType, + ), "disallowed sighash type must fail "+ + "with ErrDisallowedSigHashType, "+ + "got: %v", err) + } + }) + } + } +} diff --git a/txscript/sigvalidate.go b/txscript/sigvalidate.go index c21171eca5..a751708f43 100644 --- a/txscript/sigvalidate.go +++ b/txscript/sigvalidate.go @@ -75,6 +75,9 @@ func parseBaseSigAndPubkey(pkBytes, fullSigBytes []byte, if err := vm.checkHashTypeEncoding(hashType); err != nil { return nil, nil, 0, err } + if err := vm.checkSigHashType(hashType); err != nil { + return nil, nil, 0, err + } if err := vm.checkSignatureEncoding(sigBytes); err != nil { return nil, nil, 0, err } @@ -427,6 +430,13 @@ func newBaseTapscriptSigVerifier(pkBytes, rawSig []byte, return nil, err } + // Enforce the restricted-sighash policy (if active) on the + // tapscript signature's hash type. + err = vm.checkSigHashType(baseTaprootVerifier.hashType) + if err != nil { + return nil, err + } + return &baseTapscriptSigVerifier{ taprootSigVerifier: baseTaprootVerifier, vm: vm, From c8c72277ff9d8e67bcdee967b71fb800d41c18f0 Mon Sep 17 00:00:00 2001 From: Oli Date: Thu, 9 Apr 2026 13:59:31 +0200 Subject: [PATCH 4/7] bip322: add new bip322 submodule This commit adds a new Golang submodule that implements BIP-0322 generic message signing. This first commit adds helper methods for producing a PSBT packet that, when signed, can be turned into a BIP-0322 valid "signature" (which, depending on the variant "simple" vs. "full" is either just the serialized witness stack or the full serialized to_sign transaction). Co-Authored-By: mohamedmohey2352@gmail.com --- bip322/bip322.go | 359 ++++++++++++++++++++++++ bip322/bip322_test.go | 124 ++++++++ bip322/go.mod | 33 +++ bip322/go.sum | 34 +++ bip322/testdata/basic-test-vectors.json | 67 +++++ bip322/validation.go | 79 ++++++ 6 files changed, 696 insertions(+) create mode 100644 bip322/bip322.go create mode 100644 bip322/bip322_test.go create mode 100644 bip322/go.mod create mode 100644 bip322/go.sum create mode 100644 bip322/testdata/basic-test-vectors.json create mode 100644 bip322/validation.go diff --git a/bip322/bip322.go b/bip322/bip322.go new file mode 100644 index 0000000000..5d3ff30cb6 --- /dev/null +++ b/bip322/bip322.go @@ -0,0 +1,359 @@ +// Package bip322 implements generic message signing. For more details on +// BIP-322 see: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +package bip322 + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "strings" + "unicode/utf8" + + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" +) + +const ( + // TagBIP0322SignedMsg is the BIP-0322 tag for a signed message. It is + // declared as an untyped string constant so it cannot be mutated at + // runtime (which would silently break all sign/verify operations in + // the same process). + TagBIP0322SignedMsg = "BIP0322-signed-message" + + // PrefixSimple is the signature prefix for the "simple" variant of + // BIP-322. + PrefixSimple = "smp" + + // PrefixFull is the signature prefix for the "full" variant of BIP-322. + PrefixFull = "ful" + + // PrefixProofOfFunds is the signature prefix for the "proof of funds" + // variant of BIP-322. + PrefixProofOfFunds = "pof" + + // maxWitnessItems is the maximum number of witness items allowed in a + // witness stack (and also the maximum length of a stack item). + maxWitnessItems = 4_000_000 + + // maxTxBaseSize is the maximum size of a stripped (no witness data) TX + // according to consensus rules. + maxTxBaseSize = 1_000_000 +) + +var ( + // ErrInvalidToSign is returned when the to_sign transaction structure + // does not match the BIP-322 specification (wrong output count, wrong + // output script, wrong first input outpoint, etc.). + ErrInvalidToSign = errors.New("invalid to_sign transaction") + + // errSimpleSegWitOnly is returned when a non-native SegWit output is + // attempted to be signed with the "simple" variant of BIP-322. + errSimpleSegWitOnly = errors.New( + "only native SegWit outputs (P2WPKH, P2WSH, P2TR) are " + + "supported for the simple variant", + ) + + // errInvalidMessage is returned if the message is not valid according + // to the UTF-8 encoding rules. + errInvalidMessage = errors.New("message must be valid UTF-8") + + // b64Encode is a shortcut for the standard base64 encoding function. + b64Encode = base64.StdEncoding.EncodeToString +) + +// b64StrictDecode is a wrapper around base64.StdEncoding.DecodeString that +// rejects any newline or carriage return characters. +func b64StrictDecode(s string) ([]byte, error) { + if strings.ContainsAny(s, "\r\n") { + return nil, errors.New("contains newline characters") + } + + return base64.StdEncoding.Strict().DecodeString(s) +} + +// buildToSpendTx constructs a transaction to spend an output using the +// specified message and output script. It computes the message hash, +// constructs the scriptSig, and creates the to_spend transaction, according to +// BIP-322. +func buildToSpendTx(message, outPkScript []byte) *wire.MsgTx { + // Compute the message tagged hash: + // SHA256(SHA256(tag) || SHA256(tag) || message). + messageHash := *chainhash.TaggedHash( + []byte(TagBIP0322SignedMsg), message, + ) + + // Construct the scriptSig - OP_0 PUSH32[ message_hash ]. + scriptSig := append( + []byte{txscript.OP_0, txscript.OP_DATA_32}, messageHash[:]..., + ) + + // Create to_spend transaction in accordance to BIP-322: + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + return &wire.MsgTx{ + Version: 0, + LockTime: 0, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Index: wire.MaxPrevOutIndex, + }, + Sequence: 0, + SignatureScript: scriptSig, + Witness: wire.TxWitness{}, + }}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: outPkScript, + }}, + } +} + +// isNativeSegWitPkScript returns true iff pkScript is one of the native SegWit +// script types (P2WPKH, P2WSH, P2TR) supported by the BIP-322 "simple" variant. +func isNativeSegWitPkScript(pkScript []byte) bool { + return txscript.IsPayToWitnessPubKeyHash(pkScript) || + txscript.IsPayToWitnessScriptHash(pkScript) || + txscript.IsPayToTaproot(pkScript) +} + +// deduplicateNonWitnessUtxos deduplicates a PSBT packet's input NonWitnessUtxo +// fields for the special case allowed in BIP-322 for Proof of Funds packets. +func deduplicateNonWitnessUtxos(packet *psbt.Packet) { + for idx := range packet.Inputs { + if idx == 0 || packet.Inputs[idx].NonWitnessUtxo == nil { + continue + } + + currentTx := packet.Inputs[idx].NonWitnessUtxo + for inner := 0; inner < idx; inner++ { + if packet.Inputs[inner].NonWitnessUtxo == nil { + continue + } + + prevTx := packet.Inputs[inner].NonWitnessUtxo + if prevTx.TxHash() == currentTx.TxHash() { + // We already have this TX in a previous input, + // so we can deduplicate it. + packet.Inputs[idx].NonWitnessUtxo = nil + break + } + } + } +} + +// BuildToSignPacketSimple constructs a transaction template PSBT packet to +// prepare for signing, using the message and spend pkScript for the "simple" +// variant of the BIP-322 specification. It creates the to_sign transaction +// template, according to BIP-322. This can only be used for native SegWit +// outputs (P2WPKH, P2WSH, P2TR), and the spend pkScript is validated +// accordingly. +func BuildToSignPacketSimple(message, pkScript []byte) (*psbt.Packet, error) { + // Enforce an inclusion list: only native SegWit outputs are valid for + // the simple variant. Any other script type (legacy P2PKH/P2SH, bare + // multisig, OP_RETURN, unknown future witness versions, etc.) must use + // the full variant. + if !isNativeSegWitPkScript(pkScript) { + return nil, errSimpleSegWitOnly + } + + return BuildToSignPacketFull(message, pkScript, 0, 0, 0) +} + +// BuildToSignPacketFull constructs a transaction template PSBT packet to +// prepare for signing, using the message, spend pkScript, and tx parameters for +// the "full" variant of the BIP-322 specification. It creates the to_sign +// transaction template, according to BIP-322. +func BuildToSignPacketFull(message, spendPkScript []byte, + txVersion int32, lockTime, sequence uint32) (*psbt.Packet, error) { + + if !utf8.Valid(message) { + return nil, errInvalidMessage + } + + msg := string(message) + spendTx := buildToSpendTx(message, spendPkScript) + + // Create to_sign transaction in accordance to BIP-322: + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + packet := &psbt.Packet{ + UnsignedTx: &wire.MsgTx{ + Version: txVersion, + LockTime: lockTime, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: spendTx.TxHash(), + Index: 0, + }, + Sequence: sequence, + }}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }}, + }, + Inputs: []psbt.PInput{{ + WitnessUtxo: &wire.TxOut{ + Value: 0, + PkScript: spendPkScript, + }, + NonWitnessUtxo: spendTx, + }}, + Outputs: []psbt.POutput{{}}, + GenericSignedMessage: &msg, + } + + // Legacy scripts can't have a witness UTXO, otherwise the PSBT + // extraction will fail. We also exclude the WitnessUtxo field for a + // P2SH script, as it could be a legacy script spend as well. A nested + // segwit spend (called NP2WKH or P2SH-P2WPKH) is allowed to _only_ have + // a NonWitnessUtxo. But an actual legacy P2SH script is _not_ allowed + // to have a WitnessUtxo field. So we avoid producing an invalid packet + // as we simply don't know what kind of P2SH we're dealing with. + if txscript.IsPayToPubKey(spendPkScript) || + txscript.IsPayToPubKeyHash(spendPkScript) || + txscript.IsPayToScriptHash(spendPkScript) { + + packet.Inputs[0].WitnessUtxo = nil + } + + return packet, nil +} + +// SerializeTxWitness returns the wire witness stack as raw bytes. +func SerializeTxWitness(txWitness wire.TxWitness) ([]byte, error) { + var witnessBytes bytes.Buffer + err := psbt.WriteTxWitness(&witnessBytes, txWitness) + if err != nil { + return nil, fmt.Errorf("error serializing witness: %w", err) + } + + return witnessBytes.Bytes(), nil +} + +// SerializeSignature serializes the signature of a finalized PSBT packet of a +// BIP-322 to_sign transaction. According to the rules described in the BIP, +// this writes the signature as one of the three formats: +// 1. Simple (smp): Version and LockTime are 0, only one input with Sequence 0 +// and native SegWit input (P2WPKH, P2WSH, P2TR). +// 2. Full (ful): Version or LockTime or Sequence are non-zero or non-native +// SegWit input, only a single input. +// 3. Proof of Funds (pof): Multiple inputs provided. +func SerializeSignature(packet *psbt.Packet) (string, error) { + if packet == nil || packet.UnsignedTx == nil { + return "", errors.New("nil or incomplete packet") + } + + // Prevent us from panicking on either if the length doesn't match. + if len(packet.Inputs) != len(packet.UnsignedTx.TxIn) { + return "", errors.New("input and txin length mismatch") + } + + // At this point at least one input must be provided. + if len(packet.Inputs) == 0 { + return "", errors.New("missing inputs") + } + + tx := packet.UnsignedTx + strippedTxSize := tx.SerializeSizeStripped() + if strippedTxSize > maxTxBaseSize { + str := fmt.Sprintf("serialized transaction is too big - got "+ + "%d, max %d", strippedTxSize, maxTxBaseSize) + return "", fmt.Errorf("%w: %s", ErrInvalidToSign, str) + } + + utxo := packet.Inputs[0].WitnessUtxo + if utxo == nil { + if packet.Inputs[0].NonWitnessUtxo == nil { + return "", errors.New("missing utxo") + } + + // The to_spend transaction must have exactly one output to be + // a valid BIP-322 previous transaction to the to_sign's first + // input. + prevTx := packet.Inputs[0].NonWitnessUtxo + if len(prevTx.TxOut) != 1 { + return "", errors.New("invalid non witness UTXO") + } + + utxo = prevTx.TxOut[0] + } + + // The IsComplete should rather be called IsFinalized, as it checks each + // input if it has a witness or scriptSig. + if !packet.IsComplete() { + return "", fmt.Errorf("packet must be finalized") + } + + // We also check that the witness UTXOs for any additional inputs are + // set appropriately for their script type (so we only serialize + // something that we would also accept ourselves). + if err := validateUtxoCorrectness(packet); err != nil { + return "", err + } + + // Now that we've done the basic validation, we create a deep copy of + // the packet because the next step would otherwise potentially mutate + // the caller's packet. + finalizedPacket, err := packet.Copy() + if err != nil { + return "", fmt.Errorf("error copying packet: %w", err) + } + + // Detect the variant of the signature. + switch { + // Proof of Funds (pof) variant has multiple inputs. + case len(finalizedPacket.Inputs) > 1: + // Collapse duplicated NonWitnessUtxo fields into a single one + // to save space (special case allowed by BIP-322). + deduplicateNonWitnessUtxos(finalizedPacket) + + content, err := finalizedPacket.B64Encode() + if err != nil { + return "", fmt.Errorf("error encoding packet: %w", err) + } + + return PrefixProofOfFunds + content, nil + + // Full (ful) variant has non-zero version, locktime, or sequence or a + // non-native SegWit input. + case tx.Version != 0 || tx.LockTime != 0 || tx.TxIn[0].Sequence != 0 || + !isNativeSegWitPkScript(utxo.PkScript): + + signedTx, err := psbt.Extract(finalizedPacket) + if err != nil { + return "", fmt.Errorf("error extracting packet: %w", + err) + } + + var signedTxBytes bytes.Buffer + err = signedTx.Serialize(&signedTxBytes) + if err != nil { + return "", fmt.Errorf("error serializing signed tx: "+ + "%w", err) + } + + content := b64Encode(signedTxBytes.Bytes()) + return PrefixFull + content, nil + + // The simple (smp) variant is used if the above cases don't match. + default: + signedTx, err := psbt.Extract(finalizedPacket) + if err != nil { + return "", fmt.Errorf("error extracting packet: %w", + err) + } + + witnessBytes, err := SerializeTxWitness( + signedTx.TxIn[0].Witness, + ) + if err != nil { + return "", fmt.Errorf("error serializing witness: %w", + err) + } + + content := b64Encode(witnessBytes) + return PrefixSimple + content, nil + } +} diff --git a/bip322/bip322_test.go b/bip322/bip322_test.go new file mode 100644 index 0000000000..edea2e6a66 --- /dev/null +++ b/bip322/bip322_test.go @@ -0,0 +1,124 @@ +package bip322 + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/stretchr/testify/require" +) + +var ( + hexEncode = hex.EncodeToString +) + +// testVectors is the top-level structure of the test data file. +type testVectors struct { + TxHashes []txHashVector `json:"tx_hashes,omitempty"` + Simple []simpleSignatureVector `json:"simple"` +} + +// txHashVector contains expected transaction hashes for a given message. +type txHashVector struct { + Message string `json:"message"` + Address string `json:"address"` + MessageHash string `json:"message_hash"` + ToSpendTxHash string `json:"to_spend_tx_hash"` + ToSignTxHash string `json:"to_sign_tx_hash"` +} + +// simpleSignatureVector contains BIP-322 signature data for a given "simple" +// variant test case. +type simpleSignatureVector struct { + Message string `json:"message"` + PrivateKeys []string `json:"private_keys"` + Address string `json:"address"` + Type string `json:"type"` + WitnessScript string `json:"witness_script"` + Bip322Signatures []string `json:"bip322_signatures"` +} + +// fullSignatureVector contains BIP-322 signature data for a given "full" +// variant test case. +type fullSignatureVector struct { + Message string `json:"message"` + PrivateKeys []string `json:"private_keys"` + Address string `json:"address"` + Type string `json:"type"` + WitnessScript string `json:"witness_script"` + TxVersion int32 `json:"tx_version"` + LockTime uint32 `json:"lock_time"` + Sequence uint32 `json:"sequence"` + Bip322Signatures []string `json:"bip322_signatures"` +} + +// loadTestVectors reads and parses a test data file. +func loadTestVectors(t *testing.T, fileName string) *testVectors { + t.Helper() + + data, err := os.ReadFile(filepath.Join("testdata", fileName)) + require.NoError(t, err) + + var vectors testVectors + err = json.Unmarshal(data, &vectors) + require.NoError(t, err) + + return &vectors +} + +// testName returns a human-readable sub-test name for a given message. +func testName(message string) string { + if message == "" { + return "msg=" + } + + return fmt.Sprintf("msg=%s", message) +} + +// TestTxHashes tests the tx_hashes test vectors as mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestTxHashes(t *testing.T) { + vectors := loadTestVectors(t, "basic-test-vectors.json") + + for _, tc := range vectors.TxHashes { + t.Run(testName(tc.Message), func(t *testing.T) { + addr, err := address.DecodeAddress( + tc.Address, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + toSpendTx := buildToSpendTx( + []byte(tc.Message), scriptPubKey, + ) + + // The message hash must be set as the OP_PUSH of the + // first input's scriptSig. + msgHash := toSpendTx.TxIn[0].SignatureScript[2:] + require.Equal(t, tc.MessageHash, hexEncode(msgHash)) + + require.Equal( + t, tc.ToSpendTxHash, + toSpendTx.TxHash().String(), + ) + + toSignTx, err := BuildToSignPacketSimple( + []byte(tc.Message), scriptPubKey, + ) + require.NoError(t, err) + + require.Equal( + t, tc.ToSignTxHash, + toSignTx.UnsignedTx.TxHash().String(), + ) + }) + } +} diff --git a/bip322/go.mod b/bip322/go.mod new file mode 100644 index 0000000000..b8fafa1359 --- /dev/null +++ b/bip322/go.mod @@ -0,0 +1,33 @@ +module github.com/btcsuite/btcd/bip322 + +go 1.25.0 + +require ( + github.com/btcsuite/btcd/address/v2 v2.0.0 + github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 + github.com/btcsuite/btcd/chainhash/v2 v2.0.0 + github.com/btcsuite/btcd/psbt/v2 v2.0.0 + github.com/btcsuite/btcd/txscript/v2 v2.0.0 + github.com/btcsuite/btcd/wire/v2 v2.0.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect + github.com/btcsuite/btcd/btcutil/v2 v2.0.0 // indirect + github.com/btcsuite/btclog v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/sys v0.44.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +// TODO(guggero): Remove this once the BIP-322 PR is merged. +replace ( + github.com/btcsuite/btcd/psbt/v2 => ../psbt + github.com/btcsuite/btcd/txscript/v2 => ../txscript +) diff --git a/bip322/go.sum b/bip322/go.sum new file mode 100644 index 0000000000..7aef1c7b68 --- /dev/null +++ b/bip322/go.sum @@ -0,0 +1,34 @@ +github.com/btcsuite/btcd/address/v2 v2.0.0 h1:UVu8Hal6Siu4XastFe+JX5JkeBYONbDUIY5E+SVTs6I= +github.com/btcsuite/btcd/address/v2 v2.0.0/go.mod h1:htJK1AtaeK3bKNfZY63ep2oN8LbrI6qvmPGe1vekb3I= +github.com/btcsuite/btcd/btcec/v2 v2.5.0 h1:KioMXOWa76b86sTZZOmbzv/ldaQCmB8KFAyn5PbB8E8= +github.com/btcsuite/btcd/btcec/v2 v2.5.0/go.mod h1:+K/MYXcLBtHEQjRbjHuJChuybk4LCgjdjgRwil+e+Kk= +github.com/btcsuite/btcd/btcutil/v2 v2.0.0 h1:77pgf/4tjWaSBLdos8yiWVWL3rSphxWNqkLwcyONExA= +github.com/btcsuite/btcd/btcutil/v2 v2.0.0/go.mod h1:ZF8MMdsx1JGgvHJUanxbigekSO+8bN/ai34LBk/lg3c= +github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 h1:M/RTtXfXA9odC1RUEOyZFXj/NXKVHPYZXVjb60xTOok= +github.com/btcsuite/btcd/chaincfg/v2 v2.0.0/go.mod h1:rHgHIXYYfn70m25a+BJ9f9z7VZAsTiDQGB2XYaippGQ= +github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl6YHZ6YsEuoA= +github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8= +github.com/btcsuite/btcd/wire/v2 v2.0.0 h1:mYSKzZZ0a1sK+aMhXzfDSVsSzRkWkU3x2U04TFRS2z8= +github.com/btcsuite/btcd/wire/v2 v2.0.0/go.mod h1:bGxkPkk8IiDvUo1D96wE03llBIk7p2MdWYRyAQwLmqM= +github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= +github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk= +github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/bip322/testdata/basic-test-vectors.json b/bip322/testdata/basic-test-vectors.json new file mode 100644 index 0000000000..6f835176ca --- /dev/null +++ b/bip322/testdata/basic-test-vectors.json @@ -0,0 +1,67 @@ +{ + "tx_hashes": [ + { + "message": "", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "message_hash": "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1", + "to_spend_tx_hash": "c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7", + "to_sign_tx_hash": "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6" + }, + { + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "message_hash": "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a", + "to_spend_tx_hash": "b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b", + "to_sign_tx_hash": "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf" + }, + { + "message": "UTF-8 support: öäüéàè 测试文本 \uD83D\uDE04", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "message_hash": "43936b237ea38c7794eb5d755e0d220b6db92ebfc5c8f482759d22b1286376d7", + "to_spend_tx_hash": "c8f4f525fe8afb1bc09b44175bd2096f079c98425e8a1be676b712add1fb62f0", + "to_sign_tx_hash": "8f488e06b89eafd019ec528109eafaf7f1d1811fd617aa1eeb9658f1c1be6586" + } + ], + "simple": [ + { + "message": "", + "private_keys": [ + "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k" + ], + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "type": "p2wpkh", + "witness_script": "", + "bip322_signatures": [ + "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + "smpAkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" + ] + }, + { + "message": "Hello World", + "private_keys": [ + "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k" + ], + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "type": "p2wpkh", + "witness_script": "", + "bip322_signatures": [ + "smpAkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + "smpAkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" + ] + }, + { + "message": "This will be a p2wsh 3-of-3 multisig BIP 322 signed message", + "private_keys": [ + "L4DksdGZ4KQJfcLHD5Dv25fu8Rxyv7hHi2RjZR4TYzr8c6h9VNrp", + "KzSRqnCVwjzY8id2X5oHEJWXkSHwKUYaAXusjwgkES8BuQPJnPNu", + "L1zt9Rw7HrU7jaguMbVzhiX8ffuVkmMis5wLHddXYuHWYf8u8uRj" + ], + "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9", + "type": "p2wsh-multisig-3of3", + "witness_script": "5321027568b11f122ff8a7bc1c57e5c7642055bc618967b2f7bfe8e11fe99903c94dd321020a8bdf79cfa421d9655e9282800f115ff1d9db1e721ceb4248a3fcfec7faa67c21030c529e0ea40a00975d202624e39915daf7bdd2b71f31aa08596838781ce5f33a53ae", + "bip322_signatures": [ + "smpBQBHMEQCIFX9aaqPJWq2Ff2kpen5bFDTid+ehgUOpHV0LfjncXy4AiA3GNicF7aKPzdpa9PCpmaYQs3pHd+qbvvhXdxOCKCAMAFIMEUCIQD/ELXg6CNYyUQijCg96JtgvgjZb9dsl1Ctof4QAeyTcQIgVM/1AAblFl/DCt6A1gJg+T/i2qU5SQD09+chFJzolRwBSDBFAiEAlqRfSFyWNVQhvaCnmeV5tyneiCWMTcFbuujoD/pFa3wCIGnZjfQb8NolSYq9asV+ZeBSkCGHJcqnaV4JYS5MYPEGAWlTIQJ1aLEfEi/4p7wcV+XHZCBVvGGJZ7L3v+jhH+mZA8lN0yECCovfec+kIdllXpKCgA8RX/HZ2x5yHOtCSKP8/sf6pnwhAwxSng6kCgCXXSAmJOOZFdr3vdK3HzGqCFloOHgc5fM6U64=" + ] + } + ] +} \ No newline at end of file diff --git a/bip322/validation.go b/bip322/validation.go new file mode 100644 index 0000000000..c0601d0604 --- /dev/null +++ b/bip322/validation.go @@ -0,0 +1,79 @@ +package bip322 + +import ( + "fmt" + + "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/txscript/v2" +) + +// validateUtxoCorrectness enforces the BIP-174 rule that an input spending a +// non-segwit (legacy) output must not have a witness UTXO field +// (PSBT_IN_WITNESS_UTXO) present. +func validateUtxoCorrectness(packet *psbt.Packet) error { + for idx := range packet.Inputs { + in := &packet.Inputs[idx] + + // We just check for legacy inputs that might incorrectly have a + // WitnessUtxo field set. If there is no such field, that's + // okay. A witness input is allowed to only have the + // NonWitnessUtxo field set. But we can't check for that here + // as things might need to be de-duplicated first. + if in.WitnessUtxo == nil { + continue + } + + // Only a witness UTXO is present, sufficient only for a + // (possibly P2SH-nested) segwit input. Its PkScript is the + // scriptPubKey of the output the input spends. + _, _, isSegWit := inputWitnessProgram( + in.WitnessUtxo.PkScript, in.FinalScriptSig, + ) + if !isSegWit { + return fmt.Errorf("input %d presents a witness UTXO "+ + "for spending a non-segwit script, but a "+ + "non-segwit input requires a non-witness UTXO "+ + "(BIP-174)", idx) + } + } + + return nil +} + +// inputWitnessProgram returns the witness version and program that govern an +// input's execution, resolving a nested P2SH redeem script if present. ok is +// false when the input is not a (possibly nested) segwit spend. +func inputWitnessProgram(pkScript, sigScript []byte) (version int, + program []byte, ok bool) { + + script := pkScript + if txscript.IsPayToScriptHash(pkScript) { + redeem, err := redeemScript(sigScript) + if err != nil || len(redeem) == 0 { + return 0, nil, false + } + script = redeem + } + + if !txscript.IsWitnessProgram(script) { + return 0, nil, false + } + + v, prog, err := txscript.ExtractWitnessProgramInfo(script) + if err != nil { + return 0, nil, false + } + + return v, prog, true +} + +// redeemScript returns the redeem script of a P2SH input, which is the last +// data push of the scriptSig. +func redeemScript(sigScript []byte) ([]byte, error) { + pushes, err := txscript.PushedData(sigScript) + if err != nil || len(pushes) == 0 { + return nil, err + } + + return pushes[len(pushes)-1], nil +} From 0c7c166363e45984c81df3300d9f2a56fd021a5d Mon Sep 17 00:00:00 2001 From: Oli Date: Wed, 15 Apr 2026 14:04:12 +0200 Subject: [PATCH 5/7] bip322: add message signing --- bip322/go.mod | 2 +- bip322/signing.go | 349 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 bip322/signing.go diff --git a/bip322/go.mod b/bip322/go.mod index b8fafa1359..c81ebdf046 100644 --- a/bip322/go.mod +++ b/bip322/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/btcsuite/btcd/address/v2 v2.0.0 + github.com/btcsuite/btcd/btcec/v2 v2.5.0 github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 github.com/btcsuite/btcd/chainhash/v2 v2.0.0 github.com/btcsuite/btcd/psbt/v2 v2.0.0 @@ -13,7 +14,6 @@ require ( ) require ( - github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect github.com/btcsuite/btcd/btcutil/v2 v2.0.0 // indirect github.com/btcsuite/btclog v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/bip322/signing.go b/bip322/signing.go new file mode 100644 index 0000000000..51e8115734 --- /dev/null +++ b/bip322/signing.go @@ -0,0 +1,349 @@ +package bip322 + +import ( + "bytes" + "errors" + "fmt" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/txscript/v2" +) + +var ( + // errPrivateKeyNil is returned if a nil private key is passed to one of + // the signing functions. + errPrivateKeyNil = errors.New("private key cannot be nil") +) + +// PsbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + + if prevIndex >= uint32(len(in.NonWitnessUtxo.TxOut)) { + continue + } + + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} + +// addPartialSignature adds a signature to the given input's PartialSigs and +// checks for duplicate pubkeys. +func addPartialSignature(in *psbt.PInput, sig []byte, pubKey []byte) error { + for _, existingSig := range in.PartialSigs { + if bytes.Equal(existingSig.PubKey, pubKey) { + return fmt.Errorf("duplicate signature for pubkey %x", + pubKey) + } + } + + in.PartialSigs = append(in.PartialSigs, &psbt.PartialSig{ + Signature: sig, + PubKey: pubKey, + }) + + return nil +} + +// signInputTaprootKeySpend signs the P2TR input at the given index in the PSBT +// packet using the given private key. +func signInputTaprootKeySpend(packet *psbt.Packet, idx int, + privateKey *btcec.PrivateKey) error { + + if idx >= len(packet.Inputs) { + return fmt.Errorf("invalid input index %d", idx) + } + + in := &packet.Inputs[idx] + utxo := in.WitnessUtxo + if utxo == nil { + return fmt.Errorf("input %d has no witness UTXO", idx) + } + + prevOutFetcher := PsbtPrevOutputFetcher(packet) + sigHashes := txscript.NewTxSigHashes(packet.UnsignedTx, prevOutFetcher) + sig, err := txscript.RawTxInTaprootSignature( + packet.UnsignedTx, sigHashes, idx, utxo.Value, utxo.PkScript, + []byte{}, txscript.SigHashDefault, privateKey, + ) + if err != nil { + return fmt.Errorf("error signing: %w", err) + } + + in.TaprootKeySpendSig = sig + + return nil +} + +// signInputWitness signs a SegWit v0 input at the given index in the PSBT +// packet using the given private key. The script must be the UTXO's pkScript +// for P2WPKH, the redeem script for P2SH, or the witness program for P2WSH. +func signInputWitness(packet *psbt.Packet, idx int, script []byte, + privateKey *btcec.PrivateKey) error { + + if idx >= len(packet.Inputs) { + return fmt.Errorf("invalid input index %d", idx) + } + + in := &packet.Inputs[idx] + txIn := packet.UnsignedTx.TxIn[idx] + utxo := in.WitnessUtxo + if utxo == nil { + prevTx := in.NonWitnessUtxo + if prevTx == nil { + return fmt.Errorf("input %d has no UTXO", idx) + } + + if txIn.PreviousOutPoint.Index >= uint32(len(prevTx.TxOut)) { + return fmt.Errorf("input %d has no UTXO", idx) + } + + if prevTx.TxHash() != txIn.PreviousOutPoint.Hash { + return fmt.Errorf("non witness utxo does not match " + + "input prevout") + } + + utxo = prevTx.TxOut[txIn.PreviousOutPoint.Index] + + // Since we know this is a witness input, we should set the + // WitnessUtxo field to avoid issues with finalizing the packet. + in.WitnessUtxo = utxo + } + + prevOutFetcher := PsbtPrevOutputFetcher(packet) + sigHashes := txscript.NewTxSigHashes(packet.UnsignedTx, prevOutFetcher) + sig, err := txscript.RawTxInWitnessSignature( + packet.UnsignedTx, sigHashes, idx, utxo.Value, script, + txscript.SigHashAll, privateKey, + ) + if err != nil { + return fmt.Errorf("error signing: %w", err) + } + + return addPartialSignature( + in, sig, privateKey.PubKey().SerializeCompressed(), + ) +} + +// signInputLegacy signs a legacy input at the given index in the PSBT packet +// using the given private key. The script must be the UTXO's pkScript for +// P2PKH or the redeem script for P2SH. +func signInputLegacy(packet *psbt.Packet, idx int, script []byte, + privateKey *btcec.PrivateKey) error { + + if idx >= len(packet.Inputs) { + return fmt.Errorf("invalid input index %d", idx) + } + + in := &packet.Inputs[idx] + + sig, err := txscript.RawTxInSignature( + packet.UnsignedTx, idx, script, txscript.SigHashAll, privateKey, + ) + if err != nil { + return fmt.Errorf("error signing: %w", err) + } + + return addPartialSignature( + in, sig, privateKey.PubKey().SerializeCompressed(), + ) +} + +// payToTaprootScript creates a new script to pay to a version 1 Taproot key +// spend address. +func payToTaprootScript(privateKey *btcec.PrivateKey) ([]byte, error) { + trKey := txscript.ComputeTaprootKeyNoScript(privateKey.PubKey()) + return txscript.PayToTaprootScript(trKey) +} + +// SignP2TR signs a message using the given private key using the P2TR address +// that corresponds to the given key. The message MUST be valid UTF-8. +func SignP2TR(message string, privateKey *btcec.PrivateKey) (string, error) { + if privateKey == nil { + return "", errPrivateKeyNil + } + + pkScript, err := payToTaprootScript(privateKey) + if err != nil { + return "", fmt.Errorf("error creating pkScript: %w", err) + } + + toSign, err := BuildToSignPacketSimple([]byte(message), pkScript) + if err != nil { + return "", fmt.Errorf("error creating toSign packet: %w", err) + } + + err = signInputTaprootKeySpend(toSign, 0, privateKey) + if err != nil { + return "", fmt.Errorf("error signing: %w", err) + } + + if err := psbt.MaybeFinalizeAll(toSign); err != nil { + return "", fmt.Errorf("error finalizing packet: %w", err) + } + + return SerializeSignature(toSign) +} + +// payToWitnessPubKeyHashScript creates a new script to pay to a version 0 +// pubkey hash witness program. The passed private key is expected to be valid. +func payToWitnessPubKeyHashScript( + privateKey *btcec.PrivateKey) ([]byte, error) { + + pubKeyHash := address.Hash160(privateKey.PubKey().SerializeCompressed()) + return txscript.NewScriptBuilder().AddOp(txscript.OP_0). + AddData(pubKeyHash).Script() +} + +// SignP2WPKH signs a message using the given private key using the P2WPKH +// address that corresponds to the given key. The message MUST be valid UTF-8. +func SignP2WPKH(message string, privateKey *btcec.PrivateKey) (string, error) { + if privateKey == nil { + return "", errPrivateKeyNil + } + + pkScript, err := payToWitnessPubKeyHashScript(privateKey) + if err != nil { + return "", fmt.Errorf("error creating pkScript: %w", err) + } + + toSign, err := BuildToSignPacketSimple([]byte(message), pkScript) + if err != nil { + return "", fmt.Errorf("error creating toSign packet: %w", err) + } + + err = signInputWitness(toSign, 0, pkScript, privateKey) + if err != nil { + return "", fmt.Errorf("error signing: %w", err) + } + + if err := psbt.MaybeFinalizeAll(toSign); err != nil { + return "", fmt.Errorf("error finalizing packet: %w", err) + } + + return SerializeSignature(toSign) +} + +// payToNestedWitnessPubKeyHashScript creates a new script to pay to a nested +// version 0 pubkey hash witness program. The passed private key is expected to +// be valid. +func payToNestedWitnessPubKeyHashScript( + privateKey *btcec.PrivateKey) ([]byte, []byte, error) { + + witnessProgram, err := payToWitnessPubKeyHashScript(privateKey) + if err != nil { + return nil, nil, err + } + + scriptHash := address.Hash160(witnessProgram) + pkScript, err := txscript.NewScriptBuilder().AddOp(txscript.OP_HASH160). + AddData(scriptHash).AddOp(txscript.OP_EQUAL).Script() + if err != nil { + return nil, nil, err + } + + return pkScript, witnessProgram, nil +} + +// SignNestedP2WPKH signs a message using the given private key using the +// NestedP2WKH (sometimes also called NP2WKH or P2SH-P2WPKH) address that +// corresponds to the given key. The message MUST be valid UTF-8. +func SignNestedP2WPKH(message string, privateKey *btcec.PrivateKey) (string, + error) { + + if privateKey == nil { + return "", errPrivateKeyNil + } + + pkScript, witnessScript, err := payToNestedWitnessPubKeyHashScript( + privateKey, + ) + if err != nil { + return "", fmt.Errorf("error creating pkScript: %w", err) + } + + toSign, err := BuildToSignPacketFull([]byte(message), pkScript, 0, 0, 0) + if err != nil { + return "", fmt.Errorf("error building packet: %w", err) + } + + toSign.Inputs[0].RedeemScript = witnessScript + err = signInputWitness(toSign, 0, witnessScript, privateKey) + if err != nil { + return "", fmt.Errorf("error signing: %w", err) + } + + if err := psbt.MaybeFinalizeAll(toSign); err != nil { + return "", fmt.Errorf("error finalizing packet: %w", err) + } + + return SerializeSignature(toSign) +} + +// payToPubKeyHashScript creates a new script to pay a transaction +// output to a 20-byte pubkey hash. It is expected that the input is a valid +// hash. +func payToPubKeyHashScript(privateKey *btcec.PrivateKey) ([]byte, error) { + pubKeyHash := address.Hash160(privateKey.PubKey().SerializeCompressed()) + return txscript.NewScriptBuilder().AddOp(txscript.OP_DUP). + AddOp(txscript.OP_HASH160).AddData(pubKeyHash). + AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG). + Script() +} + +// SignP2PKH signs a message using the given private key using the P2PKH +// address that corresponds to the given key. The message MUST be valid UTF-8. +func SignP2PKH(message string, privateKey *btcec.PrivateKey) (string, error) { + if privateKey == nil { + return "", errPrivateKeyNil + } + + pkScript, err := payToPubKeyHashScript(privateKey) + if err != nil { + return "", fmt.Errorf("error creating pkScript: %w", err) + } + + toSign, err := BuildToSignPacketFull([]byte(message), pkScript, 0, 0, 0) + if err != nil { + return "", fmt.Errorf("error building packet: %w", err) + } + + err = signInputLegacy(toSign, 0, pkScript, privateKey) + if err != nil { + return "", fmt.Errorf("error signing: %w", err) + } + + if err := psbt.MaybeFinalizeAll(toSign); err != nil { + return "", fmt.Errorf("error finalizing packet: %w", err) + } + + return SerializeSignature(toSign) +} From 3bdc352141c2279d6f1f068dd7456b12719033a5 Mon Sep 17 00:00:00 2001 From: Oli Date: Thu, 9 Apr 2026 14:08:33 +0200 Subject: [PATCH 6/7] bip322: add message verification This commit adds verification functions for the generic message signing protocol and also adds test cases for all common script types. --- bip322/bip322.go | 702 +++++++++ bip322/bip322_test.go | 1135 ++++++++++++-- bip322/go.mod | 2 +- bip322/signing_test.go | 108 ++ bip322/test_vectors_test.go | 1512 +++++++++++++++++++ bip322/testdata/basic-test-vectors.json | 196 +++ bip322/testdata/generated-test-vectors.json | 551 +++++++ bip322/validation.go | 195 +++ bip322/validation_test.go | 936 ++++++++++++ 9 files changed, 5250 insertions(+), 87 deletions(-) create mode 100644 bip322/signing_test.go create mode 100644 bip322/test_vectors_test.go create mode 100644 bip322/testdata/generated-test-vectors.json create mode 100644 bip322/validation_test.go diff --git a/bip322/bip322.go b/bip322/bip322.go index 5d3ff30cb6..e8c95412f1 100644 --- a/bip322/bip322.go +++ b/bip322/bip322.go @@ -7,9 +7,13 @@ import ( "encoding/base64" "errors" "fmt" + "io" + "maps" + "slices" "strings" "unicode/utf8" + "github.com/btcsuite/btcd/address/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/psbt/v2" "github.com/btcsuite/btcd/txscript/v2" @@ -44,6 +48,30 @@ const ( ) var ( + // ErrInvalidSignature is the error returned when everything is formally + // correct, but the actual signature verification fails. + ErrInvalidSignature = errors.New("invalid signature") + + // ErrInvalidSigHashFlag is returned when a signature in the witness or + // signature script uses a sighash flag other than the one required by + // BIP-322 (SIGHASH_ALL or SIGHASH_DEFAULT for P2TR). + ErrInvalidSigHashFlag = errors.New("invalid sighash flag") + + // ErrCodeSeparator is returned when an OP_CODESEPARATOR is found in any + // of the revealed/executed scripts of an input. BIP-322's required + // rules forbid the use of OP_CODESEPARATOR. The script engine only + // rejects it in non-segwit scripts (via ScriptVerifyConstScriptCode), + // so we inspect segwit witness scripts and taproot leaf scripts + // ourselves. + ErrCodeSeparator = errors.New("OP_CODESEPARATOR is forbidden") + + // ErrInconclusive is returned when verification cannot reach a + // definitive valid/invalid result because of a BIP-322 "upgradeable + // rules" violation (e.g., a tx version other than 0 or 2). Per BIP-322 + // the validator must stop and output the "inconclusive" state in this + // case. + ErrInconclusive = errors.New("inconclusive") + // ErrInvalidToSign is returned when the to_sign transaction structure // does not match the BIP-322 specification (wrong output count, wrong // output script, wrong first input outpoint, etc.). @@ -56,6 +84,20 @@ var ( "supported for the simple variant", ) + // errMoreDataAvailable is returned when more data is present after + // parsing an input. + errMoreDataAvailable = errors.New( + "more data present after parsing input", + ) + + // errNilAddress is returned when a nil address is passed to a + // verification function. + errNilAddress = errors.New("address must not be nil") + + // errSignatureTooLarge is returned when a raw signature payload + // exceeds maxWitnessItems. + errSignatureTooLarge = errors.New("signature payload too large") + // errInvalidMessage is returned if the message is not valid according // to the UTF-8 encoding rules. errInvalidMessage = errors.New("message must be valid UTF-8") @@ -74,6 +116,25 @@ func b64StrictDecode(s string) ([]byte, error) { return base64.StdEncoding.Strict().DecodeString(s) } +// TimeConstraints specifies constraints on the validation result of a message +// signature. If a result is Constrained = true, it means the witness stack +// validated correctly but has time lock properties that need to be evaluated +// separately. +type TimeConstraints struct { + // Constrained indicates whether the provided witness stack has time or + // age based restrictions (e.g., uses LockTime or Sequence check + // opcodes). + Constrained bool + + // ValidAtTime indicates the LockTime value that must be satisfied for + // the witness stack to be valid. + ValidAtTime uint32 + + // ValidAtAge indicates the Sequence value that must be satisfied for + // the witness stack to be valid. + ValidAtAge uint32 +} + // buildToSpendTx constructs a transaction to spend an output using the // specified message and output script. It computes the message hash, // constructs the scriptSig, and creates the to_spend transaction, according to @@ -357,3 +418,644 @@ func SerializeSignature(packet *psbt.Packet) (string, error) { return PrefixSimple + content, nil } } + +// ParseTxWitness parses the raw witness bytes into a wire.TxWitness. This +// function rejects trailing data that isn't part of the expected encoding. +func ParseTxWitness(rawWitness []byte) (wire.TxWitness, error) { + if len(rawWitness) > maxWitnessItems { + return nil, fmt.Errorf("%w [size %d, max %d]", + errSignatureTooLarge, len(rawWitness), maxWitnessItems) + } + + var ( + buf [8]byte + reader = bytes.NewReader(rawWitness) + ) + witCount, err := wire.ReadVarIntBuf(reader, 0, buf[:]) + if err != nil { + return nil, err + } + + // Prevent memory exhaustion via a huge witness item count. The upfront + // make([][]byte, witCount) allocates ~24 bytes per entry on 64-bit. + if witCount > maxWitnessItems { + return nil, fmt.Errorf("too many witness items "+ + "[count %d, max %d]", witCount, maxWitnessItems) + } + + // It is theoretically possible (and likely consensus valid in a + // non-taproot context before MAX_STACK size was introduced) to create + // a witness stack of maxWitnessItems/2 empty items. But to avoid a + // trivial memory allocation of maxWitnessItems elements with a single + // varInt claiming maxWitnessItems number of items without providing + // them, we pre-allocate with a much smaller size. This doesn't prevent + // a TX to actually contain maxWitnessItems empty items, but they + // actually need to be encoded fully. Which means the raw witness would + // be very long and noticeable by the user. So this simply prevents a + // ~96MB memory allocation (on a 64bit system) for a seemingly harmless + // and short encoded witness. + allocCount := witCount + if allocCount > txscript.MaxStackSize { + allocCount = txscript.MaxStackSize + } + + // Then for witCount number of stack items, each item has a varint + // length prefix, followed by the witness item itself. + witness := make([][]byte, 0, allocCount) + for j := uint64(0); j < witCount; j++ { + count, err := wire.ReadVarIntBuf(reader, 0, buf[:]) + if err != nil { + return nil, err + } + + // Enforce the per-item size cap. + if count > maxWitnessItems { + return nil, fmt.Errorf("witness item too large "+ + "[count %d, max %d]", count, maxWitnessItems) + } + + // Validate against the remaining input length BEFORE + // allocating. Without this, an attacker claiming a large item + // but providing no data still triggers a full-size allocation. + if int64(count) > int64(reader.Len()) { + return nil, fmt.Errorf("witness item length %d "+ + "exceeds remaining input %d", count, + reader.Len()) + } + + item := make([]byte, count) + if _, err := io.ReadFull(reader, item); err != nil { + return nil, err + } + + witness = append(witness, item) + } + + // If we didn't fully read all the data, this probably isn't a proper + // witness stack. + if reader.Len() > 0 { + return nil, errMoreDataAvailable + } + + return witness, nil +} + +// ParseTx parses the raw transaction bytes into a wire.MsgTx. The input is +// capped at maxWitnessItems to bound the worst-case memory usage during +// deserialization. This function rejects trailing data that isn't part of the +// expected encoding. +func ParseTx(rawTx []byte) (*wire.MsgTx, error) { + if len(rawTx) > maxWitnessItems { + return nil, fmt.Errorf("%w [size %d, max %d]", + errSignatureTooLarge, len(rawTx), maxWitnessItems) + } + + var ( + tx wire.MsgTx + reader = bytes.NewReader(rawTx) + ) + err := tx.Deserialize(reader) + if err != nil { + return nil, fmt.Errorf("error parsing transaction: %w", err) + } + + // If we didn't fully read all the data, this probably isn't a proper + // transaction, if there's trailing data. + if reader.Len() > 0 { + return nil, errMoreDataAvailable + } + + return &tx, nil +} + +// ParsePsbt parses the given bytes as a PSBT packet. This function rejects +// trailing data that isn't part of the expected encoding. +func ParsePsbt(rawPacket []byte) (*psbt.Packet, error) { + reader := bytes.NewReader(rawPacket) + signaturePacket, err := psbt.NewFromRawBytes(reader, false) + if err != nil { + return nil, fmt.Errorf("error parsing packet: %w", err) + } + + // NewFromRawBytes already checks that the reader is exhausted, so + // we don't need an additional check here. + return signaturePacket, nil +} + +// VerifyMessage verifies a message signed with the "simple", "full" or +// "Proof of Funds" variant of BIP-322 against the given address. The signature +// is expected to be base64-encoded. +// +// Return value semantics: +// - On success: returns (true, {Constrained, ValidAtTime, ValidAtAge}, nil). +// - On any form of verification failure (malformed signature, wrong message, +// wrong address, invalid script, etc.): returns (false, {}, non-nil error). +// - The boolean is never true when err != nil; callers may check either. +// +// Timelock semantics (full and proof of funds variant): the transaction +// version, locktime, and sequence are taken from the caller-supplied signature +// and used to rebuild the to_sign sighash. The signature commits to those +// values, so they can't be forged — but a signer may legitimately choose any +// values that satisfy CSV/CLTV opcodes inside the script. A successful +// verification therefore proves the signer can satisfy the script; it does NOT +// prove they could spend the associated on-chain output at the current chain +// tip. +func VerifyMessage(message string, address address.Address, + signature string) (bool, TimeConstraints, error) { + + empty := TimeConstraints{} + if address == nil { + return false, empty, errNilAddress + } + + challengeScript, err := txscript.PayToAddrScript(address) + if err != nil { + return false, empty, fmt.Errorf("error creating pkScript: %w", + err) + } + + return verifyMessageForChallenge( + []byte(message), challengeScript, signature, + ) +} + +// verifyMessageForChallenge verifies the given signature against the given +// message and challenge script. +func verifyMessageForChallenge(message, challengeScript []byte, + signature string) (bool, TimeConstraints, error) { + + // Even without a prefix, the signature needs to be at least 3 + // characters. + empty := TimeConstraints{} + if len(signature) < 3 { + return false, empty, errors.New("signature too short") + } + + switch { + // Full (ful) variant. + case strings.HasPrefix(signature, PrefixFull): + signatureBytes, err := b64StrictDecode( + signature[len(PrefixFull):], + ) + if err != nil { + return false, empty, fmt.Errorf("error base64 "+ + "decoding signature as full variant: %w", err) + } + + tx, err := ParseTx(signatureBytes) + if err != nil { + return false, empty, fmt.Errorf("error parsing "+ + "signature as full variant: %w", err) + } + + if len(tx.TxIn) == 0 { + return false, empty, errors.New("no inputs in " + + "transaction") + } + + // The Proof of Funds variant requires us to have the previous + // outputs of the additional inputs. So it needs to be encoded + // differently (the full finalized PSBT packet) and also have a + // different prefix. + if len(tx.TxIn) > 1 { + return false, empty, errors.New("proof of funds " + + "variant with incorrect prefix supplied") + } + + // BIP-322 verification process basic validation: confirm the + // supplied to_sign transaction has the structure required by + // the BIP. This is checked here (before reconstruction) so + // malformed to_sign transactions are rejected explicitly + // rather than slipping through unchecked because reconstruction + // ignores the parts that get rebuilt. + toSpendHash := buildToSpendTx(message, challengeScript).TxHash() + if err := validateToSignTx(tx, toSpendHash); err != nil { + return false, empty, err + } + + // BIP-322 upgradeable rules: tx version must be 0 or 2. + // A failure here returns ErrInconclusive per spec. + if err := validateUpgradeableRules(tx.Version); err != nil { + return false, empty, err + } + + firstIn := tx.TxIn[0] + return VerifyMessageFull( + message, challengeScript, firstIn.SignatureScript, + firstIn.Witness, tx.Version, tx.LockTime, + firstIn.Sequence, + ) + + // Proof of Funds (pof) variant. + case strings.HasPrefix(signature, PrefixProofOfFunds): + signatureBytes, err := b64StrictDecode( + signature[len(PrefixProofOfFunds):], + ) + if err != nil { + return false, empty, fmt.Errorf("error base64 "+ + "decoding signature as pof variant: %w", err) + } + + signaturePacket, err := ParsePsbt(signatureBytes) + if err != nil { + return false, empty, fmt.Errorf("error parsing "+ + "signature as pof variant: %w", err) + } + + return VerifyMessagePoF( + message, challengeScript, signaturePacket, + ) + + // If there is no prefix, we fall back to simple. + default: + sig := strings.TrimPrefix(signature, PrefixSimple) + signatureBytes, err := b64StrictDecode(sig) + if err != nil { + return false, empty, fmt.Errorf("error decoding "+ + "signature as base64: %w", err) + } + + witness, err := ParseTxWitness(signatureBytes) + if err != nil { + return false, empty, fmt.Errorf("error parsing "+ + "signature as witness: %w", err) + } + + return VerifyMessageSimple(message, challengeScript, witness) + } +} + +// VerifyMessageSimple verifies a message signed with the "simple" variant of +// BIP-322. The witness stack must be the complete stack, including signatures, +// script inputs, the script itself, the control block, and so on (depending on +// the script type). This variant can only be used for fully native SegWit +// outputs (P2WPKH, P2WSH, P2TR). Any other script type (legacy, nested, bare, +// or unknown future witness versions) must use the "full" variant. +func VerifyMessageSimple(message, pkScript []byte, + witness wire.TxWitness) (bool, TimeConstraints, error) { + + if !isNativeSegWitPkScript(pkScript) { + return false, TimeConstraints{}, errSimpleSegWitOnly + } + + return VerifyMessageFull(message, pkScript, nil, witness, 0, 0, 0) +} + +// VerifyMessageFull verifies a message signed with the "full" variant of +// BIP-322. The witness stack must be the complete stack, including signatures, +// script inputs, the script itself, the control block, and so on (depending on +// the script type). The sigScript must only be set for legacy (P2PKH, P2SH, +// NP2WPKH) address types. +func VerifyMessageFull(message, pkScript []byte, sigScript []byte, + witness wire.TxWitness, txVersion int32, lockTime, + sequence uint32) (bool, TimeConstraints, error) { + + // First, we create the full version of the to_sign tx. + empty := TimeConstraints{} + toSign, err := BuildToSignPacketFull( + message, pkScript, txVersion, lockTime, sequence, + ) + if err != nil { + return false, empty, err + } + + // We know this should never be the case, but let's just be safe. + if len(toSign.Inputs) != 1 { + return false, empty, errors.New("to_sign tx should have one " + + "input") + } + + witnessBytes, err := SerializeTxWitness(witness) + if err != nil { + return false, empty, fmt.Errorf("error serializing witness: %w", + err) + } + + // The finalized packet is just the toSign with the final script and + // witness sig applied. + toSign.Inputs[0].FinalScriptWitness = witnessBytes + toSign.Inputs[0].FinalScriptSig = sigScript + + return VerifyMessagePoF(message, pkScript, toSign) +} + +// VerifyMessagePoF verifies a message signed with the "Proof of Funds" variant +// of BIP-322. The sigPacket must be the complete, finalized PSBT packet of the +// to_sign transaction, including all witness stacks and UTXO information. +func VerifyMessagePoF(message, pkScript []byte, + sigPacket *psbt.Packet) (bool, TimeConstraints, error) { + + // Pure UX improvement, require valid UTF-8 message upfront with nice + // error message. Will be validated again by the PSBT library but then + // would produce a more cryptic error message. + empty := TimeConstraints{} + if !utf8.Valid(message) { + return false, empty, errInvalidMessage + } + + // The BIP-174 specifies that a finalized PSBT should have all but + // certain specific fields stripped from the inputs. But it doesn't + // explicitly mention what should happen to global fields. So we can't + // really define whether the PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE field + // should be stripped or may be left after signing (the purpose of the + // field is mainly for signing). So we only check that it matches the + // message. If it's missing, that's okay. + if sigPacket.GenericSignedMessage != nil && + *sigPacket.GenericSignedMessage != string(message) { + + return false, empty, fmt.Errorf("generic signed message " + + "field on signature packet doesn't match passed " + + "message string") + } + + // Do some basic validation on the signature packet. We can use the + // SerializeSignature function that checks the inputs and UTXO for the + // first input at least. + _, err := SerializeSignature(sigPacket) + if err != nil { + return false, empty, fmt.Errorf("invalid signature packet: %w", + err) + } + + // BIP-322 verification process basic validation, applied to + // the unsigned tx embedded in the PSBT. + toSpendHash := buildToSpendTx(message, pkScript).TxHash() + err = validateToSignTx(sigPacket.UnsignedTx, toSpendHash) + if err != nil { + return false, empty, err + } + + // BIP-322 upgradeable rules: tx version must be 0 or 2. + err = validateUpgradeableRules(sigPacket.UnsignedTx.Version) + if err != nil { + return false, empty, err + } + + // We now create the toSign packet as if we only had a single input. + toSign, err := BuildToSignPacketFull( + message, pkScript, sigPacket.UnsignedTx.Version, + sigPacket.UnsignedTx.LockTime, + sigPacket.UnsignedTx.TxIn[0].Sequence, + ) + if err != nil { + return false, empty, err + } + + // We then ONLY copy over the witness for the first input and the + // additional inputs from the supplied packet, everything else must come + // from the packet we just built for the verification to be meaningful. + // Otherwise, a user could just supply whatever. + toSign.Inputs[0].FinalScriptWitness = + sigPacket.Inputs[0].FinalScriptWitness + toSign.Inputs[0].FinalScriptSig = sigPacket.Inputs[0].FinalScriptSig + for idx := 1; idx < len(sigPacket.Inputs); idx++ { + toSign.UnsignedTx.TxIn = append( + toSign.UnsignedTx.TxIn, sigPacket.UnsignedTx.TxIn[idx], + ) + toSign.Inputs = append(toSign.Inputs, sigPacket.Inputs[idx]) + } + + // findUtxo is a helper for finding the UTXO information for an input at + // a given index. It implements the special BIP-0322 optimization where + // multiple non-witness inputs that spend from the same transaction can + // re-use the same NonWitnessUtxo field. + findUtxo := func(idx int) (*wire.TxOut, error) { + if idx < 0 || idx >= len(toSign.Inputs) { + return nil, fmt.Errorf("invalid input index %d", idx) + } + + in := toSign.Inputs[idx] + txIn := toSign.UnsignedTx.TxIn[idx] + switch { + // The non-witness UTXO is present, we need to look up the + // correct output. We prefer the NonWitnessUtxo over the + // WitnessUtxo because it allows signers to validate all + // amounts. That's why a lot of wallets set both fields. + case in.NonWitnessUtxo != nil: + prevTx := in.NonWitnessUtxo + prevOutIdx := txIn.PreviousOutPoint.Index + if prevOutIdx >= uint32(len(prevTx.TxOut)) { + return nil, fmt.Errorf("invalid non witness "+ + "utxo for input index %d", idx) + } + + if prevTx.TxHash() != txIn.PreviousOutPoint.Hash { + return nil, fmt.Errorf("non witness utxo " + + "does not match input prevout") + } + + return prevTx.TxOut[prevOutIdx], nil + + // Only the witness UTXO is present. This is sufficient only for + // segwit inputs, whose sighash commits to the input amount, so + // the amount in the witness UTXO is authenticated by the + // signature. For non-witness (legacy) inputs the amount is not + // committed to, so BIP-322 requires the full NonWitnessUtxo. + // + // The input type must be derived from the script, not from the + // presence of witness bytes: an empty witness stack still + // serializes to a non-empty FinalScriptWitness ({0x00}), so its + // length is not a reliable witness-input signal. + case in.WitnessUtxo != nil: + _, _, isSegWit := inputWitnessProgram( + in.WitnessUtxo.PkScript, in.FinalScriptSig, + ) + if !isSegWit { + return nil, fmt.Errorf("only witness utxo " + + "present for non-witness script type") + } + + return in.WitnessUtxo, nil + + // Neither is present. This normally wouldn't be valid, but the + // BIP-0322 defines a special exception to save space: If + // multiple non-witness inputs spend from the same transaction, + // only the first one in the list needs to specify the + // NonWitnessUtxo and the others can re-use it. So we do a + // lookup here. + default: + // If there is only a single input, then this special + // case doesn't apply and the packet is invalid. + numInputs := len(toSign.Inputs) + if numInputs == 1 { + return nil, fmt.Errorf("invalid signature "+ + "packet: no UTXO for input index %d", + idx) + } + + // The way a BIP-0322 packet is created, the very first + // input spends the to_spend transaction, which can't + // be re-used. So we start at index 1, which is the + // first additional input, and scan up to the current + // input index (the BIP mandates that only earlier + // NonWitnessUtxos can be re-used). + targetHash := txIn.PreviousOutPoint + for lookupIdx := 1; lookupIdx < idx; lookupIdx++ { + lookupInput := toSign.Inputs[lookupIdx] + prevTx := lookupInput.NonWitnessUtxo + if prevTx == nil { + continue + } + + if prevTx.TxHash() != targetHash.Hash { + continue + } + + prevOutIdx := txIn.PreviousOutPoint.Index + if prevOutIdx >= uint32(len(prevTx.TxOut)) { + return nil, fmt.Errorf("invalid non "+ + "witness utxo for input "+ + "index %d", idx) + } + + return prevTx.TxOut[prevOutIdx], nil + } + + // If we got here without finding a matching UTXO, + // the packet is invalid. + return nil, fmt.Errorf("invalid signature packet: "+ + "UTXO not found for input index %d", idx) + } + } + + utxos := make(map[wire.OutPoint]*wire.TxOut, len(toSign.Inputs)) + for idx := range toSign.Inputs { + txIn := toSign.UnsignedTx.TxIn[idx] + + // Duplicate inputs are not allowed. + _, exists := utxos[txIn.PreviousOutPoint] + if exists { + return false, empty, fmt.Errorf("duplicate inputs " + + "present") + } + + // The to_sign transaction also must have valid previous + // transaction hashes, which means a null hash is not allowed. + if txIn.PreviousOutPoint.Hash == (chainhash.Hash{}) { + return false, empty, fmt.Errorf("zero hash present " + + "in input") + } + + utxos[txIn.PreviousOutPoint], err = findUtxo(idx) + if err != nil { + return false, empty, fmt.Errorf("error finding UTXO: "+ + "%w", err) + } + } + + // Make sure the previous output amounts don't violate any consensus + // rules that can be checked without having the chain (mostly min/max + // values). + err = validateAmounts(slices.Collect(maps.Values(utxos))) + if err != nil { + return false, empty, err + } + + // Prepare the validation helpers and then extract the final transaction + // from the packet, now that we've parsed the UTXO information from it. + finalTx, err := psbt.Extract(toSign) + if err != nil { + return false, empty, fmt.Errorf("error extracting final "+ + "transaction: %w", err) + } + + prevOutFetcher := txscript.NewMultiPrevOutFetcher(utxos) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + // In case there are any time locks involved in the unlocking script, we + // need to inform the validator about that. The BIP explicitly only + // mentions the sequence of the first input (the input related to the + // challenge pkScript). + constraints := TimeConstraints{ + ValidAtTime: finalTx.LockTime, + ValidAtAge: toSign.UnsignedTx.TxIn[0].Sequence, + } + constraints.Constrained = constraints.ValidAtAge != 0 || + constraints.ValidAtTime != 0 + + // Verify the signature (full witness stack) of each input using the + // btcd script engine. This is what makes it possible to verify multisig + // or even custom scripts. + for idx := range toSign.Inputs { + txIn := toSign.UnsignedTx.TxIn[idx] + finalIn := finalTx.TxIn[idx] + utxo := utxos[txIn.PreviousOutPoint] + + // BIP-322 required rule: the use of OP_CODESEPARATOR is + // forbidden. The script engine (even with StandardVerifyFlags) + // only rejects it in non-segwit scripts, so we inspect the + // segwit witness scripts and taproot leaf scripts here. + err = validateNoCodeSeparator( + utxo.PkScript, finalIn.Witness, finalIn.SignatureScript, + ) + if err != nil { + return false, empty, err + } + + // BIP-322 required rule: all signatures must use SIGHASH_ALL + // (or SIGHASH_DEFAULT for BIP341 P2TR inputs). We enforce this + // with the ScriptVerifyRestrictSigHash flag, which makes the + // script engine reject any signature using a different sighash + // type. Unlike inspecting the witness up front, this constrains + // every signature the engine actually verifies, including those + // consumed by multisig and custom (e.g. tapscript) scripts. + vm, err := txscript.NewEngine( + utxo.PkScript, finalTx, idx, + txscript.StandardVerifyFlags| + txscript.ScriptVerifyRestrictSigHash, + nil, sigHashes, utxo.Value, prevOutFetcher, + ) + if err != nil { + return false, empty, fmt.Errorf("error creating "+ + "txscript engine: %w", err) + } + + err = vm.Execute() + if err != nil { + // The Error() function on the VM's Error struct doesn't + // return a nice error message. So we parse the error to + // get something more descriptive. If parsing fails, we + // at least make the error detectable as signature + // verification having failed. + var vmErr txscript.Error + if !errors.As(err, &vmErr) { + return false, empty, fmt.Errorf("%w: %w", + ErrInvalidSignature, err) + } + + // Re-map some of the VM internal errors to explicit + // BIP-322 errors. + switch vmErr.ErrorCode { + // Surface a disallowed sighash type as a dedicated + // BIP-322 error so callers can distinguish it from a + // generic signature failure. + case txscript.ErrDisallowedSigHashType: + return false, empty, ErrInvalidSigHashFlag + + // Map the "upgradeable rules" errors from the VM to the + // BIP-322 "inconclusive" error. + case txscript.ErrDiscourageUpgradableNOPs, + txscript.ErrDiscourageUpgradableWitnessProgram: + + return false, empty, ErrInconclusive + + // NOTE: ErrDiscourageUpgradeableTaprootVersion (unknown + // tapscript leaf version) and ErrDiscourageOpSuccess + // (tapscript OP_SUCCESSx opcode) are NOT remapped here. + // Both are arguably "upgradeable" in spirit + // (BIP-341/342 reserve them for future soft forks), but + // BIP-322 §4 does not list them, so we treat them as + // `invalid` - matching the literal spec text. + default: + return false, empty, fmt.Errorf("%w: "+ + "err_code=%s, err_desc=%s", + ErrInvalidSignature, + vmErr.ErrorCode.String(), + vmErr.Description) + } + } + } + + // Success, we have a valid signature. + return true, constraints, nil +} diff --git a/bip322/bip322_test.go b/bip322/bip322_test.go index edea2e6a66..1e1acb3598 100644 --- a/bip322/bip322_test.go +++ b/bip322/bip322_test.go @@ -1,124 +1,1087 @@ package bip322 import ( - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" + "bytes" + "strings" "testing" "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/psbt/v2" "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" "github.com/stretchr/testify/require" ) -var ( - hexEncode = hex.EncodeToString -) +// encodeWitnessHeader writes a (count, items...) witness stack into a buffer +// where each item is described by (length, data). Used for crafting edge-case +// payloads below. +func encodeWitnessHeader(t *testing.T, witCount uint64, + items ...[]byte) []byte { -// testVectors is the top-level structure of the test data file. -type testVectors struct { - TxHashes []txHashVector `json:"tx_hashes,omitempty"` - Simple []simpleSignatureVector `json:"simple"` -} - -// txHashVector contains expected transaction hashes for a given message. -type txHashVector struct { - Message string `json:"message"` - Address string `json:"address"` - MessageHash string `json:"message_hash"` - ToSpendTxHash string `json:"to_spend_tx_hash"` - ToSignTxHash string `json:"to_sign_tx_hash"` -} - -// simpleSignatureVector contains BIP-322 signature data for a given "simple" -// variant test case. -type simpleSignatureVector struct { - Message string `json:"message"` - PrivateKeys []string `json:"private_keys"` - Address string `json:"address"` - Type string `json:"type"` - WitnessScript string `json:"witness_script"` - Bip322Signatures []string `json:"bip322_signatures"` -} - -// fullSignatureVector contains BIP-322 signature data for a given "full" -// variant test case. -type fullSignatureVector struct { - Message string `json:"message"` - PrivateKeys []string `json:"private_keys"` - Address string `json:"address"` - Type string `json:"type"` - WitnessScript string `json:"witness_script"` - TxVersion int32 `json:"tx_version"` - LockTime uint32 `json:"lock_time"` - Sequence uint32 `json:"sequence"` - Bip322Signatures []string `json:"bip322_signatures"` -} - -// loadTestVectors reads and parses a test data file. -func loadTestVectors(t *testing.T, fileName string) *testVectors { t.Helper() - data, err := os.ReadFile(filepath.Join("testdata", fileName)) + var buf bytes.Buffer + require.NoError(t, wire.WriteVarInt(&buf, 0, witCount)) + for _, item := range items { + require.NoError(t, wire.WriteVarInt(&buf, 0, uint64(len(item)))) + _, err := buf.Write(item) + require.NoError(t, err) + } + return buf.Bytes() +} + +// TestParseTxWitnessTooManyItems asserts that ParseTxWitness rejects witness +// stacks whose declared item count exceeds maxWitnessItems, bounding the +// upfront slice-header allocation. +func TestParseTxWitnessTooManyItems(t *testing.T) { + // Just claim maxWitnessItems+1 items — no item data follows. If the + // parser allocated first, it would commit ~24 MB from a 5-byte input. + var buf bytes.Buffer + require.NoError(t, wire.WriteVarInt(&buf, 0, uint64(maxWitnessItems+1))) + + _, err := ParseTxWitness(buf.Bytes()) + require.Error(t, err) + require.Contains(t, err.Error(), "too many witness items") +} + +// TestParseTxWitnessItemTooLarge asserts that ParseTxWitness rejects a witness +// stack containing a single item whose declared length exceeds +// maxWitnessItems+1. +func TestParseTxWitnessItemTooLarge(t *testing.T) { + // 1 item, declared length = maxWitnessItems + 1, no body. + var buf bytes.Buffer + require.NoError(t, wire.WriteVarInt(&buf, 0, 1)) + require.NoError( + t, wire.WriteVarInt(&buf, 0, uint64(maxWitnessItems+1)), + ) + + _, err := ParseTxWitness(buf.Bytes()) + require.Error(t, err) + require.Contains(t, err.Error(), "witness item too large") +} + +// TestParseTxWitnessItemExceedsRemaining asserts that ParseTxWitness rejects +// a witness item whose declared length is larger than the remaining input, +// BEFORE committing a potentially large allocation. +func TestParseTxWitnessItemExceedsRemaining(t *testing.T) { + // 1 item, declared length = 100_000, but only 1 body byte available. + var buf bytes.Buffer + require.NoError(t, wire.WriteVarInt(&buf, 0, 1)) + require.NoError(t, wire.WriteVarInt(&buf, 0, 100_000)) + buf.WriteByte(0xaa) + + _, err := ParseTxWitness(buf.Bytes()) + require.Error(t, err) + require.Contains(t, err.Error(), "exceeds remaining input") +} + +// TestParseTxTrailingData asserts that ParseTx rejects input that contains +// trailing bytes after a fully-deserialized transaction. This mirrors the +// reader.Len() check that ParseTxWitness performs after parsing a witness +// stack: the transaction parses, but leftover data means the input isn't a +// clean, canonical encoding. +func TestParseTxTrailingData(t *testing.T) { + // Build a minimal but valid transaction and serialize it. + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Index: 0xffffffff}, + Sequence: 0xffffffff, + }) + tx.AddTxOut(&wire.TxOut{ + Value: 1000, + PkScript: []byte{txscript.OP_TRUE}, + }) + + var buf bytes.Buffer + require.NoError(t, tx.Serialize(&buf)) + raw := buf.Bytes() + + // The exact serialization (no trailing data) must parse cleanly back + // into the same transaction. + parsed, err := ParseTx(raw) require.NoError(t, err) + require.Equal(t, tx.TxHash(), parsed.TxHash()) + + // Appending any trailing bytes must be rejected: the transaction + // deserializes fully, but the reader still has data left over. We copy + // into a fresh slice first to avoid aliasing the serialization buffer. + withTrailing := append(append([]byte{}, raw...), 0xde, 0xad, 0xbe, 0xef) + _, err = ParseTx(withTrailing) + require.ErrorIs(t, err, errMoreDataAvailable) +} - var vectors testVectors - err = json.Unmarshal(data, &vectors) +// TestParsePsbtTrailingData asserts that ParsePsbt rejects input that contains +// trailing bytes after a fully-deserialized PSBT packet, mirroring the +// reader.Len() check ParseTx and ParseTxWitness perform: the packet parses, +// but leftover data means the input isn't a clean, canonical encoding. +func TestParsePsbtTrailingData(t *testing.T) { + // Build a minimal but valid unsigned PSBT packet (a BIP-322 to_sign + // template for a P2WPKH output) and serialize it to raw bytes. + pkScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_0). + AddData(bytes.Repeat([]byte{0x01}, 20)). + Script() require.NoError(t, err) - return &vectors + packet, err := BuildToSignPacketSimple([]byte("probe"), pkScript) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, packet.Serialize(&buf)) + raw := buf.Bytes() + + // The exact serialization (no trailing data) must parse cleanly back + // into the same packet. + parsed, err := ParsePsbt(raw) + require.NoError(t, err) + require.Equal(t, packet.UnsignedTx.TxHash(), parsed.UnsignedTx.TxHash()) + + // Appending any trailing bytes must be rejected: the packet + // deserializes fully, but the reader still has data left over. We copy + // into a fresh slice first to avoid aliasing the serialization buffer. + withTrailing := append(append([]byte{}, raw...), 0xde, 0xad, 0xbe, 0xef) + _, err = ParsePsbt(withTrailing) + require.ErrorIs(t, err, psbt.ErrInvalidPsbtFormat) } -// testName returns a human-readable sub-test name for a given message. -func testName(message string) string { - if message == "" { - return "msg=" - } +// TestParseTxWitnessRawTooLarge asserts that ParseTxWitness rejects raw +// inputs larger than maxWitnessItems up front. +func TestParseTxWitnessRawTooLarge(t *testing.T) { + raw := make([]byte, maxWitnessItems+1) + _, err := ParseTxWitness(raw) + require.ErrorIs(t, err, errSignatureTooLarge) +} + +// TestParseTxWitnessValidSmallStack asserts that ordinary, well-formed witness +// stacks inside the new bounds still parse. +func TestParseTxWitnessValidSmallStack(t *testing.T) { + sig := bytes.Repeat([]byte{0xaa}, 72) + pub := bytes.Repeat([]byte{0xbb}, 33) + + raw := encodeWitnessHeader(t, 2, sig, pub) - return fmt.Sprintf("msg=%s", message) + stack, err := ParseTxWitness(raw) + require.NoError(t, err) + require.Len(t, stack, 2) + require.Equal(t, sig, stack[0]) + require.Equal(t, pub, stack[1]) } -// TestTxHashes tests the tx_hashes test vectors as mentioned in BIP-322: -// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki -func TestTxHashes(t *testing.T) { - vectors := loadTestVectors(t, "basic-test-vectors.json") +// TestParseTxTooLarge asserts that ParseTx rejects raw inputs larger than +// maxWitnessItems. +func TestParseTxTooLarge(t *testing.T) { + raw := make([]byte, maxWitnessItems+1) + _, err := ParseTx(raw) + require.ErrorIs(t, err, errSignatureTooLarge) +} - for _, tc := range vectors.TxHashes { - t.Run(testName(tc.Message), func(t *testing.T) { - addr, err := address.DecodeAddress( - tc.Address, &chaincfg.MainNetParams, - ) - require.NoError(t, err) +// TestVerifyMessageNilAddress asserts that VerifyMessage returns a clean +// error (not a panic) when the caller passes a nil address. +func TestVerifyMessageNilAddress(t *testing.T) { + require.NotPanics(t, func() { + valid, _, err := VerifyMessage("msg", nil, "AA==") + require.False(t, valid) + require.ErrorIs(t, err, errNilAddress) + }) +} - scriptPubKey, err := txscript.PayToAddrScript(addr) - require.NoError(t, err) +// TestVerifyMessageSignatureTooLarge asserts that VerifyMessage rejects +// signatures whose base64-decoded size exceeds maxWitnessItems. +func TestVerifyMessageSignatureTooLarge(t *testing.T) { + addr, err := address.DecodeAddress( + "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + huge := make([]byte, maxWitnessItems+1) + sig := b64Encode(huge) + + valid, _, err := VerifyMessage("msg", addr, sig) + require.False(t, valid) + require.ErrorIs(t, err, errSignatureTooLarge) +} + +// bareMultisigScript builds OP_2 OP_2 OP_CHECKMULTISIG — a +// script that is neither P2PK, P2PKH, P2SH, nor any native SegWit type. +// Under the old exclusion-list gate this was silently accepted; under the +// inclusion-list gate it must be rejected. +func bareMultisigScript(t *testing.T) []byte { + t.Helper() + + // Two arbitrary 33-byte compressed pubkeys (bytes are not valid + // curve points, but txscript.PayToAddrScript isn't involved here — + // we just need the script shape). + pub1 := bytes.Repeat([]byte{0x02}, 33) + pub2 := bytes.Repeat([]byte{0x03}, 33) - toSpendTx := buildToSpendTx( - []byte(tc.Message), scriptPubKey, + b := txscript.NewScriptBuilder() + b.AddOp(txscript.OP_2) + b.AddData(pub1) + b.AddData(pub2) + b.AddOp(txscript.OP_2) + b.AddOp(txscript.OP_CHECKMULTISIG) + + script, err := b.Script() + require.NoError(t, err) + return script +} + +// TestBuildToSignPacketSimpleRejectsNonNativeSegWit asserts that +// BuildToSignPacketSimple rejects every non-native-SegWit script type, +// including ones that slipped through the previous exclusion-list gate. +func TestBuildToSignPacketSimpleRejectsNonNativeSegWit(t *testing.T) { + cases := []struct { + name string + pkScript []byte + }{ + { + name: "empty pkScript", + pkScript: nil, + }, + { + name: "OP_RETURN", + pkScript: []byte{txscript.OP_RETURN}, + }, + { + name: "bare multisig", + pkScript: bareMultisigScript(t), + }, + { + // A plausible-looking but non-existent future witness + // version. We want the gate to fail *closed* on + // unknown versions. + name: "future witness version (v2)", + pkScript: append( + []byte{txscript.OP_2, 0x20}, + bytes.Repeat([]byte{0xaa}, 32)..., + ), + }, + { + // Random garbage bytes. + name: "random bytes", + pkScript: bytes.Repeat([]byte{0x42}, 20), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := BuildToSignPacketSimple( + []byte("msg"), tc.pkScript, ) + require.ErrorIs(t, err, errSimpleSegWitOnly) + }) + } +} - // The message hash must be set as the OP_PUSH of the - // first input's scriptSig. - msgHash := toSpendTx.TxIn[0].SignatureScript[2:] - require.Equal(t, tc.MessageHash, hexEncode(msgHash)) +// TestVerifyMessageSimpleRejectsNonNativeSegWit mirrors the BuildToSignPacket +// test for the verification path. +func TestVerifyMessageSimpleRejectsNonNativeSegWit(t *testing.T) { + scripts := map[string][]byte{ + "OP_RETURN": {txscript.OP_RETURN}, + "bare multisig": bareMultisigScript(t), + "random bytes": bytes.Repeat([]byte{0x42}, 20), + } - require.Equal( - t, tc.ToSpendTxHash, - toSpendTx.TxHash().String(), + for name, pkScript := range scripts { + t.Run(name, func(t *testing.T) { + valid, _, err := VerifyMessageSimple( + []byte("msg"), pkScript, wire.TxWitness{}, ) + require.False(t, valid) + require.ErrorIs(t, err, errSimpleSegWitOnly) + }) + } +} + +// TestBuildToSignPacketSimpleAcceptsNativeSegWit asserts that the inclusion +// list still accepts all three native SegWit script types. +func TestBuildToSignPacketSimpleAcceptsNativeSegWit(t *testing.T) { + // Minimal but well-formed pkScripts for each native SegWit type. + p2wpkh := append( + []byte{txscript.OP_0, 0x14}, + bytes.Repeat([]byte{0xaa}, 20)..., + ) + p2wsh := append( + []byte{txscript.OP_0, 0x20}, + bytes.Repeat([]byte{0xbb}, 32)..., + ) + p2tr := append( + []byte{txscript.OP_1, 0x20}, + bytes.Repeat([]byte{0xcc}, 32)..., + ) - toSignTx, err := BuildToSignPacketSimple( - []byte(tc.Message), scriptPubKey, + cases := map[string][]byte{ + "p2wpkh": p2wpkh, + "p2wsh": p2wsh, + "p2tr": p2tr, + } + for name, pkScript := range cases { + t.Run(name, func(t *testing.T) { + packet, err := BuildToSignPacketSimple( + []byte("msg"), pkScript, ) require.NoError(t, err) + require.NotNil(t, packet) + }) + } +} + +// TestVerifyMessageDecodesBase64Error asserts the base64 decode path still +// returns a clean error (not a panic or bool=true). +func TestVerifyMessageDecodesBase64Error(t *testing.T) { + addr, err := address.DecodeAddress( + "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + valid, _, err := VerifyMessage("msg", addr, "not valid base64!!!") + require.False(t, valid) + require.Error(t, err) + require.True( + t, strings.Contains(err.Error(), "base64"), + "expected base64 error, got %q", err.Error(), + ) +} + +// makeUtxoTx returns a fresh transaction whose non-witness content (and thus +// its txid) is determined solely by tag: two calls with the same tag produce +// distinct *wire.MsgTx values that share the same TxHash, while different tags +// produce different TxHashes. When withWitness is set, witness data is attached +// to the input. Because a txid is computed over the non-witness serialization, +// the witness must NOT change the TxHash, so a witness-bearing tx still +// deduplicates against a witness-less tx with the same tag. +func makeUtxoTx(tag int64, withWitness bool) *wire.MsgTx { + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Index: 0xffffffff}, + Sequence: 0xffffffff, + }) + if withWitness { + tx.TxIn[0].Witness = wire.TxWitness{{0x01, 0x02, 0x03}} + } + tx.AddTxOut(&wire.TxOut{ + Value: tag, + PkScript: []byte{txscript.OP_TRUE}, + }) + + return tx +} + +// dedupInput describes a single PSBT input to construct for the +// deduplicateNonWitnessUtxos test. +type dedupInput struct { + // noUtxo indicates the input has no NonWitnessUtxo (it is left nil). + noUtxo bool + + // tag identifies the NonWitnessUtxo content; inputs with the same tag + // share a txid. Ignored when noUtxo is true. + tag int64 + + // withWitness attaches witness data to the NonWitnessUtxo transaction. + // Since a txid ignores witness data, such a transaction still + // deduplicates against a witness-less transaction with the same tag. + withWitness bool + + // withWitnessUtxo also sets a WitnessUtxo on the input. The + // deduplication must never touch the WitnessUtxo field. + withWitnessUtxo bool +} + +// TestDeduplicateNonWitnessUtxos exercises deduplicateNonWitnessUtxos across a +// range of inputs. The function must keep the first occurrence of each distinct +// NonWitnessUtxo transaction (identified by txid), set any later duplicates to +// nil, always preserve input 0, skip inputs that have no NonWitnessUtxo, and +// never modify the WitnessUtxo field. +func TestDeduplicateNonWitnessUtxos(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + inputs []dedupInput + + // want[i] is true if input i must retain its NonWitnessUtxo + // after deduplication, and false if it must be nil. + want []bool + }{ + { + name: "no inputs", + inputs: nil, + want: nil, + }, + { + name: "single input with utxo is kept", + inputs: []dedupInput{{tag: 1}}, + want: []bool{true}, + }, + { + name: "single input without utxo stays nil", + inputs: []dedupInput{{noUtxo: true}}, + want: []bool{false}, + }, + { + name: "two inputs same utxo dedups the second", + inputs: []dedupInput{{tag: 1}, {tag: 1}}, + want: []bool{true, false}, + }, + { + name: "two inputs distinct utxos both kept", + inputs: []dedupInput{{tag: 1}, {tag: 2}}, + want: []bool{true, true}, + }, + { + name: "three identical utxos keep only the first", + inputs: []dedupInput{ + {tag: 1}, {tag: 1}, {tag: 1}, + }, + want: []bool{true, false, false}, + }, + { + name: "duplicate of first input with unique middle", + inputs: []dedupInput{ + {tag: 1}, {tag: 2}, {tag: 1}, + }, + want: []bool{true, true, false}, + }, + { + name: "interleaved duplicates A B A B", + inputs: []dedupInput{ + {tag: 1}, {tag: 2}, {tag: 1}, {tag: 2}, + }, + want: []bool{true, true, false, false}, + }, + { + name: "first non-nil utxo after a nil input is kept", + inputs: []dedupInput{ + {noUtxo: true}, {tag: 1}, {tag: 1}, + }, + want: []bool{false, true, false}, + }, + { + name: "nil input between duplicates", + inputs: []dedupInput{ + {tag: 1}, {noUtxo: true}, {tag: 1}, + }, + want: []bool{true, false, false}, + }, + { + name: "all inputs without a utxo", + inputs: []dedupInput{ + {noUtxo: true}, {noUtxo: true}, {noUtxo: true}, + }, + want: []bool{false, false, false}, + }, + { + name: "first utxo duplicated repeatedly around a " + + "unique one", + inputs: []dedupInput{ + {tag: 1}, {tag: 1}, {tag: 2}, {tag: 1}, + }, + want: []bool{true, false, true, false}, + }, + { + name: "complex mix of nils and duplicates", + inputs: []dedupInput{ + {noUtxo: true}, {tag: 1}, {tag: 2}, + {tag: 1}, {noUtxo: true}, {tag: 2}, + }, + want: []bool{false, true, true, false, false, false}, + }, + { + name: "duplicate detected by txid ignores witness data", + inputs: []dedupInput{ + {tag: 1}, {tag: 1, withWitness: true}, + }, + want: []bool{true, false}, + }, + { + name: "witness-bearing utxo as the first occurrence", + inputs: []dedupInput{ + {tag: 1, withWitness: true}, {tag: 1}, + }, + want: []bool{true, false}, + }, + { + name: "witness utxo preserved on a deduplicated input", + inputs: []dedupInput{ + {tag: 1, withWitnessUtxo: true}, + {tag: 1, withWitnessUtxo: true}, + }, + want: []bool{true, false}, + }, + { + name: "input zero is always kept even if it has a " + + "later twin", + inputs: []dedupInput{ + {tag: 7}, {tag: 7}, + }, + want: []bool{true, false}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + require.Len(t, tc.want, len(tc.inputs), + "test setup: want must match inputs length") + + // Build the PSBT packet from the input specs. + packet := &psbt.Packet{} + for _, in := range tc.inputs { + pin := psbt.PInput{} + if !in.noUtxo { + pin.NonWitnessUtxo = makeUtxoTx( + in.tag, in.withWitness, + ) + } + if in.withWitnessUtxo { + pin.WitnessUtxo = &wire.TxOut{ + Value: 1000, + PkScript: []byte{ + txscript.OP_TRUE, + }, + } + } + packet.Inputs = append(packet.Inputs, pin) + } + + deduplicateNonWitnessUtxos(packet) + + // The deduplication must never add or remove inputs. + require.Len(t, packet.Inputs, len(tc.inputs), + "input count must not change") + + for i, in := range tc.inputs { + got := packet.Inputs[i].NonWitnessUtxo + + if tc.want[i] { + require.NotNilf(t, got, "input %d "+ + "should keep its "+ + "NonWitnessUtxo", i) + + // A retained utxo must be left + // unchanged. + want := makeUtxoTx( + in.tag, in.withWitness, + ) + require.Equalf( + t, want.TxHash(), got.TxHash(), + "input %d retained the wrong "+ + "transaction", i, + ) + } else { + require.Nilf(t, got, "input %d should "+ + "be deduplicated to nil", i) + } + + // The WitnessUtxo field must never be touched. + if in.withWitnessUtxo { + require.NotNilf( + t, packet.Inputs[i].WitnessUtxo, + "input %d WitnessUtxo must be "+ + "preserved", i, + ) + } + } + }) + } +} + +// TestVerifyMessagePoFDoesNotMutatePacket ensures verification is read-only. +func TestVerifyMessagePoFDoesNotMutatePacket(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevHash := prevTx.TxHash() + + for idx := uint32(0); idx < 2; idx++ { + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: idx, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + NonWitnessUtxo: prevTx, + FinalScriptWitness: witnessBytes, + }) + } + + require.NotNil(t, packet.Inputs[2].NonWitnessUtxo) + valid, _, err := VerifyMessagePoF(message, pkScript, packet) + require.NoError(t, err) + require.True(t, valid) + require.NotNil( + t, packet.Inputs[2].NonWitnessUtxo, + "VerifyMessagePoF must not mutate the caller-provided PSBT", + ) +} + +// TestVerifyMessagePoFRejectsLegacyBareWitnessUtxo is a regression test for a +// proof-of-funds forgery. A legacy (P2PKH) input's signature does not commit to +// the spent amount, so BIP-322 requires its full NonWitnessUtxo to authenticate +// that amount; a bare WitnessUtxo is only sufficient for segwit inputs, whose +// sighash does commit to the amount. +// +// An attacker can bypass a naive length-based witness-input check by encoding +// an empty witness stack as FinalScriptWitness = {0x00}: that has a non-zero +// byte length, yet extracts to a witness-less legacy spend that the script VM +// happily validates. The forger then supplies only a WitnessUtxo with an +// inflated, unauthenticated amount. Verification must reject the bare +// WitnessUtxo for the legacy input. +func TestVerifyMessagePoFRejectsLegacyBareWitnessUtxo(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + // Input 0 is the trivially valid OP_TRUE challenge input. + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes - require.Equal( - t, tc.ToSignTxHash, - toSignTx.UnsignedTx.TxHash().String(), + // Input 1 spends a legacy P2PKH output that the forger can validly sign + // but whose amount they want to lie about. + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + p2pkhScript, err := payToPubKeyHashScript(key) + require.NoError(t, err) + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: p2pkhScript}) + prevHash := prevTx.TxHash() + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0}, + }) + + // Produce a genuinely valid legacy scriptSig ( ) over the + // full to_sign transaction, so that (pre-fix) the input would pass the + // script VM and the forgery would be accepted. + sigScript, err := txscript.SignatureScript( + packet.UnsignedTx, 1, p2pkhScript, txscript.SigHashAll, key, + true, + ) + require.NoError(t, err) + + // The forgery: only a WitnessUtxo with an inflated, unauthenticated + // amount, no NonWitnessUtxo, and an empty-but-non-nil witness stack. + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: &wire.TxOut{ + Value: 100_000_000, + PkScript: p2pkhScript, + }, + FinalScriptSig: sigScript, + FinalScriptWitness: []byte{0x00}, + }) + + // The {0x00} FinalScriptWitness has a non-zero byte length, which a + // naive len() check would mistake for a witness input. + require.NotZero(t, len(packet.Inputs[1].FinalScriptWitness)) + + // The forgery is rejected: validateUtxoCorrectness catches the bare + // witness UTXO on a non-segwit input during the up-front serialization + // check, before the input's amount is ever trusted. + valid, _, err := VerifyMessagePoF(message, pkScript, packet) + require.Error(t, err) + require.ErrorContains( + t, err, "witness UTXO for spending a non-segwit script", + ) + require.False(t, valid) +} + +// TestProbeVerifyMessagePoFDoesNotMutateOutputDerivationOrder tests that +// VerifyMessagePoF doesn't mutate the order of derivation paths in the packet. +func TestProbeVerifyMessagePoFDoesNotMutateOutputDerivationOrder(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevHash := prevTx.TxHash() + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: prevTx.TxOut[0], + FinalScriptWitness: witnessBytes, + }) + + keyA, err := btcec.NewPrivateKey() + require.NoError(t, err) + keyB, err := btcec.NewPrivateKey() + require.NoError(t, err) + pubA := keyA.PubKey().SerializeCompressed() + pubB := keyB.PubKey().SerializeCompressed() + if string(pubA) > string(pubB) { + pubA, pubB = pubB, pubA + } + packet.Outputs[0].Bip32Derivation = []*psbt.Bip32Derivation{ + {PubKey: pubB}, + {PubKey: pubA}, + } + + require.Equal(t, pubB, packet.Outputs[0].Bip32Derivation[0].PubKey) + valid, _, err := VerifyMessagePoF(message, pkScript, packet) + require.NoError(t, err) + require.True(t, valid) + require.Equal( + t, pubB, packet.Outputs[0].Bip32Derivation[0].PubKey, + "VerifyMessagePoF must not mutate output metadata order", + ) +} + +// TestPoFSignatureRejectsEmbeddedBase64Newline checks strict PSBT base64 +// decoding. +func TestPoFSignatureRejectsEmbeddedBase64Newline(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevHash := prevTx.TxHash() + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: prevTx.TxOut[0], + FinalScriptWitness: witnessBytes, + }) + + sig, err := SerializeSignature(packet) + require.NoError(t, err) + sig = sig[:len(PrefixProofOfFunds)+4] + "\n" + + sig[len(PrefixProofOfFunds)+4:] + + valid, _, err := verifyMessageForChallenge(message, pkScript, sig) + require.Error(t, err) + require.False(t, valid) +} + +func makeBase64PaddingBitsNonZero(t *testing.T, s string) string { + t.Helper() + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy" + + "z0123456789+/" + if strings.HasSuffix(s, "==") { + i := len(s) - 3 + v := strings.IndexByte(alphabet, s[i]) + require.GreaterOrEqual(t, v, 0) + return s[:i] + string(alphabet[v|1]) + s[i+1:] + } + if strings.HasSuffix(s, "=") { + i := len(s) - 2 + v := strings.IndexByte(alphabet, s[i]) + require.GreaterOrEqual(t, v, 0) + return s[:i] + string(alphabet[v|1]) + s[i+1:] + } + t.Fatalf("signature has no padding: %q", s[len(s)-8:]) + return "" +} + +// TestProbePoFSignatureRejectsNonCanonicalBase64PaddingBits makes sure no +// non-canonical padding bits are used in the base64 signature encoding. +func TestProbePoFSignatureRejectsNonCanonicalBase64PaddingBits(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevHash := prevTx.TxHash() + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: prevTx.TxOut[0], + FinalScriptWitness: witnessBytes, + }) + + var sig string + for valueLen := 0; valueLen < 3; valueLen++ { + packet.Unknowns = []*psbt.Unknown{ + { + Key: []byte{0xaa}, + Value: make([]byte, valueLen), + }, + } + var err error + sig, err = SerializeSignature(packet) + require.NoError(t, err) + if strings.Contains(sig, "=") { + break + } + } + require.Contains(t, sig, "=") + sig = PrefixProofOfFunds + makeBase64PaddingBitsNonZero( + t, sig[len(PrefixProofOfFunds):], + ) + + valid, _, err := verifyMessageForChallenge(message, pkScript, sig) + require.Error(t, err) + require.False(t, valid) +} + +// TestProbePoFRejectsLaterNonWitnessUtxoReuse tests that re-used +// NonWitnessUtxos must come before the input that re-uses them. +func TestProbePoFRejectsLaterNonWitnessUtxoReuse(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + prevHash := prevTx.TxHash() + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + FinalScriptWitness: witnessBytes, + }) + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: 1, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + NonWitnessUtxo: prevTx, + FinalScriptWitness: witnessBytes, + }) + + valid, _, err := VerifyMessagePoF(message, pkScript, packet) + require.Error(t, err) + require.False(t, valid) +} + +// TestVerifyMessageNonUtf8 tests that a non-UTF-8 message returns a nice error. +func TestVerifyMessageNonUtf8(t *testing.T) { + msg := []byte{0xff, 0xfe, 0xfd} + _, _, err := VerifyMessagePoF(msg, nil, &psbt.Packet{}) + require.ErrorIs(t, err, errInvalidMessage) +} + +// TestVerifyMessagePoFRejectsMissingOrMismatchedGenericSignedMessage tests that +// a mismatched PSBT_GLOBAL_GENERIC_SIGNED_MESSAGE field on the PoF PSBT is +// rejected. +func TestVerifyMessagePoFRejectsMissingOrMismatchedGenericSignedMessage( + t *testing.T) { + + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + makeSig := func(t *testing.T, field *string) string { + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + packet.GenericSignedMessage = field + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevTx.TxHash(), + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: prevTx.TxOut[0], + FinalScriptWitness: witnessBytes, + }) + + sig, err := SerializeSignature(packet) + require.NoError(t, err) + + return sig + } + + different := "different message" + tests := []struct { + name string + field *string + wantError bool + }{ + { + name: "missing", + field: nil, + wantError: false, + }, + { + name: "mismatched", + field: &different, + wantError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sig := makeSig(t, test.field) + + valid, _, err := verifyMessageForChallenge( + message, pkScript, sig, ) + + if test.wantError { + require.Error(t, err) + require.False(t, valid) + } else { + require.NoError(t, err) + require.True(t, valid) + } + }) + } +} + +// TestSerializeSignatureWitnessUtxoCorrectness tests that attempting to +// serialize a PoF signature fails if the UTXO information isn't correct for +// the input type. +func TestSerializeSignatureWitnessUtxoCorrectness(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + legacyPkScript, err := payToPubKeyHashScript(key) + require.NoError(t, err) + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: legacyPkScript}) + prevTx.AddTxOut(&wire.TxOut{Value: 2, PkScript: legacyPkScript}) + prevHash := prevTx.TxHash() + + for outIdx := uint32(0); outIdx < 2; outIdx++ { + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevHash, + Index: outIdx, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + NonWitnessUtxo: prevTx, + WitnessUtxo: prevTx.TxOut[outIdx], }) } + + for inputIdx := 1; inputIdx <= 2; inputIdx++ { + scriptSig, err := txscript.SignatureScript( + packet.UnsignedTx, inputIdx, legacyPkScript, + txscript.SigHashAll, key, true, + ) + require.NoError(t, err) + + packet.Inputs[inputIdx].FinalScriptSig = scriptSig + } + + _, err = SerializeSignature(packet) + require.ErrorContains( + t, err, "witness UTXO for spending a non-segwit script", + ) +} + +// TestSerializeSignatureCheckOrder demonstrates that SerializeSignature runs +// validateUtxoCorrectness *before* IsComplete(). +func TestSerializeSignatureCheckOrder(t *testing.T) { + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Build a P2SH-P2WPKH pkScript. This puts the challenge input into + // the P2SH-nested-segwit path, where BuildToSignPacketFull nils + // WitnessUtxo on input 0 (so input 0 alone can't trigger the bug). + pkScript, _, err := payToNestedWitnessPubKeyHashScript(key) + require.NoError(t, err) + + packet, err := BuildToSignPacketFull([]byte("probe"), pkScript, 0, 0, 0) + require.NoError(t, err) + + // Add a second, P2SH-nested-segwit PoF input that a wallet has + // pre-populated with both UTXO fields but has NOT signed yet. This is + // the exact state a wallet's outgoing PSBT is in before being handed + // off for signing. + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1000, PkScript: pkScript}) + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevTx.TxHash(), + Index: 0, + }, + }) + + // FinalScriptSig deliberately left nil — packet is unfinalized. + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: prevTx.TxOut[0], + NonWitnessUtxo: prevTx, + }) + + // The packet is not finalized. Ask SerializeSignature what it thinks. + _, err = SerializeSignature(packet) + require.Error(t, err) + + // The honest error would be "packet must be finalized"; today the + // caller instead gets the misleading BIP-174-shaped error. + require.ErrorContains( + t, err, "packet must be finalized", + "expected the honest finalized-first error; got the "+ + "misleading UTXO-correctness error because "+ + "validateUtxoCorrectness runs before IsComplete "+ + "in SerializeSignature", + ) } diff --git a/bip322/go.mod b/bip322/go.mod index c81ebdf046..fc6bc83286 100644 --- a/bip322/go.mod +++ b/bip322/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/btcsuite/btcd/address/v2 v2.0.0 github.com/btcsuite/btcd/btcec/v2 v2.5.0 + github.com/btcsuite/btcd/btcutil/v2 v2.0.0 github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 github.com/btcsuite/btcd/chainhash/v2 v2.0.0 github.com/btcsuite/btcd/psbt/v2 v2.0.0 @@ -14,7 +15,6 @@ require ( ) require ( - github.com/btcsuite/btcd/btcutil/v2 v2.0.0 // indirect github.com/btcsuite/btclog v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect diff --git a/bip322/signing_test.go b/bip322/signing_test.go new file mode 100644 index 0000000000..467259c13b --- /dev/null +++ b/bip322/signing_test.go @@ -0,0 +1,108 @@ +package bip322 + +import ( + mrand "math/rand/v2" + "strings" + "testing" + "unicode/utf8" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +// randomUTF8String returns a string of n randomly-chosen valid UTF-8 runes. +func randomUTF8String(n int) string { + runes := make([]rune, n) + for i := range runes { + runes[i] = randomRune() + } + return string(runes) +} + +// randomRune returns a random valid Unicode scalar value. It rejects the +// surrogate range (U+D800–U+DFFF) and anything above U+10FFFF, both of which +// cannot be legally encoded as UTF-8. +func randomRune() rune { + for { + // Random value in [0, 0x10FFFF]. + r := rune(mrand.Int64N(utf8.MaxRune + 1)) + if utf8.ValidRune(r) { + return r + } + } +} + +// TestSign tests that a valid signature can be created for all supported +// single-key address types. +func TestSign(t *testing.T) { + for range 100 { + randString := randomUTF8String(256) + + randPrivateKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Sign for a P2TR address. + sigP2TR, err := SignP2TR(randString, randPrivateKey) + require.NoError(t, err) + require.Contains(t, sigP2TR, PrefixSimple) + require.True(t, strings.HasPrefix(sigP2TR, PrefixSimple)) + + scriptP2TR, err := payToTaprootScript(randPrivateKey) + require.NoError(t, err) + + valid, _, err := verifyMessageForChallenge( + []byte(randString), scriptP2TR, sigP2TR, + ) + require.NoError(t, err) + require.True(t, valid) + + // Sign for a P2WPKH address. + sigP2WPKH, err := SignP2WPKH(randString, randPrivateKey) + require.NoError(t, err) + require.Contains(t, sigP2WPKH, PrefixSimple) + require.True(t, strings.HasPrefix(sigP2WPKH, PrefixSimple)) + + scriptP2WPKH, err := payToWitnessPubKeyHashScript( + randPrivateKey, + ) + require.NoError(t, err) + + valid, _, err = verifyMessageForChallenge( + []byte(randString), scriptP2WPKH, sigP2WPKH, + ) + require.NoError(t, err) + require.True(t, valid) + + // Sign for a NP2WPKH address. + sigNP2WPKH, err := SignNestedP2WPKH(randString, randPrivateKey) + require.NoError(t, err) + require.Contains(t, sigNP2WPKH, PrefixFull) + require.True(t, strings.HasPrefix(sigNP2WPKH, PrefixFull)) + + scriptNP2WPKH, _, err := payToNestedWitnessPubKeyHashScript( + randPrivateKey, + ) + require.NoError(t, err) + + valid, _, err = verifyMessageForChallenge( + []byte(randString), scriptNP2WPKH, sigNP2WPKH, + ) + require.NoError(t, err) + require.True(t, valid) + + // Sign for a P2PKH address. + sigP2PKH, err := SignP2PKH(randString, randPrivateKey) + require.NoError(t, err) + require.Contains(t, sigP2PKH, PrefixFull) + require.True(t, strings.HasPrefix(sigP2PKH, PrefixFull)) + + scriptP2PKH, err := payToPubKeyHashScript(randPrivateKey) + require.NoError(t, err) + + valid, _, err = verifyMessageForChallenge( + []byte(randString), scriptP2PKH, sigP2PKH, + ) + require.NoError(t, err) + require.True(t, valid) + } +} diff --git a/bip322/test_vectors_test.go b/bip322/test_vectors_test.go new file mode 100644 index 0000000000..2bdd32706b --- /dev/null +++ b/bip322/test_vectors_test.go @@ -0,0 +1,1512 @@ +package bip322 + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +var ( + netParams = &chaincfg.MainNetParams + testValue int64 = 345678 + testCSVTimeout uint32 = 2016 + + hexDecode = hex.DecodeString + hexEncode = hex.EncodeToString + + testRevokePrivateKey, _ = hexDecode( + "318ea3020520a834e013c38ef4400d098fd56299c98dba89ece7a2466526" + + "984b", + ) + + timeLockTemplate = ` + OP_IF + {{ hex .RevokeKey }} + OP_ELSE + {{ .CsvTimeout }} OP_CHECKSEQUENCEVERIFY OP_DROP + {{ hex .SelfKey }} + OP_ENDIF + OP_CHECKSIG + ` + multiSig2of2Template = ` + OP_2 + {{ hex .FirstKey }} + {{ hex .SecondKey }} + OP_2 + OP_CHECKMULTISIG + ` + multiSig3of3Template = ` + OP_3 + {{ hex .FirstKey }} + {{ hex .SecondKey }} + {{ hex .ThirdKey }} + OP_3 + OP_CHECKMULTISIG + ` +) + +// testVectors is the top-level structure of the test data files. +type testVectors struct { + TxHashes []txHashVector `json:"tx_hashes,omitempty"` + Simple []simpleSignatureVector `json:"simple"` + Full []fullSignatureVector `json:"full"` + ProofOfFunds []pofSignatureVector `json:"proof_of_funds"` + Valid []validVector `json:"valid,omitempty"` + Error []errorVector `json:"error"` +} + +// txHashVector contains expected transaction hashes for a given message. +type txHashVector struct { + Message string `json:"message"` + Address string `json:"address"` + MessageHash string `json:"message_hash"` + ToSpendTxHash string `json:"to_spend_tx_hash"` + ToSignTxHash string `json:"to_sign_tx_hash"` +} + +// simpleSignatureVector contains BIP-322 signature data for a given "simple" +// variant test case. +type simpleSignatureVector struct { + Message string `json:"message"` + PrivateKeys []string `json:"private_keys"` + Address string `json:"address"` + Type string `json:"type"` + WitnessScript string `json:"witness_script"` + Bip322Signatures []string `json:"bip322_signatures"` +} + +// fullSignatureVector contains BIP-322 signature data for a given "full" +// variant test case. +type fullSignatureVector struct { + simpleSignatureVector + SigScript string `json:"sig_script"` + TxVersion int32 `json:"tx_version"` + LockTime uint32 `json:"lock_time"` + Sequence uint32 `json:"sequence"` +} + +// additionalInput defines an additional input that is part of a +// "Proof of Funds" test case. +type additionalInput struct { + PrivateKeyIndex int `json:"private_key_index"` + Type string `json:"type"` + Value int64 `json:"value"` + PkScript string `json:"pk_script"` +} + +// pofSignatureVector contains BIP-322 signature data for a given +// "Proof of Funds" variant test case. +type pofSignatureVector struct { + fullSignatureVector + + // AdditionalInputs is a two-dimensional slice that defines inputs we + // want to add to a Proof of Funds transaction, grouped by previous + // transaction. + AdditionalInputs [][]additionalInput `json:"additional_inputs"` +} + +// errorVector contains a test case that must fail verification. +type errorVector struct { + Description string `json:"description"` + Message string `json:"message"` + Address string `json:"address"` + Signature string `json:"signature"` + ErrorSubstr string `json:"error_substr"` +} + +// validVector contains a static signature that must verify successfully. Unlike +// the simple/full/proof_of_funds vectors, these are not re-signed from private +// keys; they hold a fixed signature that VerifyMessage must accept. This is used +// for spends that can't be expressed as a standard, re-signable input type +// (e.g. custom tapscript or witness scripts). +type validVector struct { + Description string `json:"description"` + Message string `json:"message"` + Address string `json:"address"` + Signature string `json:"signature"` +} + +// loadTestVectors reads and parses a test data file. +func loadTestVectors(t *testing.T, fileName string) *testVectors { + t.Helper() + + data, err := os.ReadFile(filepath.Join("testdata", fileName)) + require.NoError(t, err) + + var vectors testVectors + err = json.Unmarshal(data, &vectors) + require.NoError(t, err) + + return &vectors +} + +// storeTestVectors writes the test vectors to the test file. +func storeTestVectors(t *testing.T, vectors *testVectors, fileName string) { + t.Helper() + + data, err := json.MarshalIndent(vectors, "", " ") + require.NoError(t, err) + + err = os.WriteFile(filepath.Join("testdata", fileName), data, 0644) + require.NoError(t, err) +} + +// testName returns a human-readable sub-test name for a given message. +func testName(scriptType, message string) string { + if message == "" { + return fmt.Sprintf("type=%s,msg=", scriptType) + } + + return fmt.Sprintf("type=%s,msg=%s", scriptType, message) +} + +// parseWIFs parses a list of WIFs into a slice of *address.WIF. +func parseWIFs(t *testing.T, keys []string) []*btcutil.WIF { + wifs := make([]*btcutil.WIF, len(keys)) + for i, key := range keys { + var err error + wifs[i], err = btcutil.DecodeWIF(key) + require.NoError(t, err) + } + return wifs +} + +// formatWIFs formats a slice of *address.WIF into a slice of strings. +func formatWIFs(wifs []*btcutil.WIF) []string { + keys := make([]string, len(wifs)) + for i, wif := range wifs { + keys[i] = wif.String() + } + return keys +} + +// signAndExtract signs the given toSignTx with the given private keys and +// returns the resulting signed transaction. +func signAndExtract(t *testing.T, privateKeys []string, typ, witnessScriptHex, + sigScriptHex string, toSignTx *psbt.Packet) (string, *wire.MsgTx) { + + wifs := parseWIFs(t, privateKeys) + inputType, err := parseInputType(typ) + require.NoError(t, err) + + witnessScript, err := hexDecode(witnessScriptHex) + require.NoError(t, err) + + // Must be nil when empty for PSBT library. + if len(witnessScript) == 0 { + witnessScript = nil + } + + sigScript, err := hexDecode(sigScriptHex) + require.NoError(t, err) + + if len(sigScript) == 0 { + sigScript = nil + } + + inputType.decorateInput( + t, wifs[0].PrivKey, &toSignTx.Inputs[0], + witnessScript, sigScript, + ) + inputType.sign(t, 0, wifs, toSignTx) + inputType.finalize(t, toSignTx) + + signature, err := SerializeSignature(toSignTx) + require.NoError(t, err) + + signedTx, err := psbt.Extract(toSignTx) + require.NoError(t, err) + + return signature, signedTx +} + +// TestTxHashesVectors tests the tx_hashes test vectors as mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestTxHashesVectors(t *testing.T) { + vectors := loadTestVectors(t, "basic-test-vectors.json") + + for _, tc := range vectors.TxHashes { + t.Run(testName("n/a", tc.Message), func(t *testing.T) { + addr, err := address.DecodeAddress( + tc.Address, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + toSpendTx := buildToSpendTx( + []byte(tc.Message), scriptPubKey, + ) + + // The message hash must be set as the OP_PUSH of the + // first input's scriptSig. + msgHash := toSpendTx.TxIn[0].SignatureScript[2:] + require.Equal(t, tc.MessageHash, hexEncode(msgHash)) + + require.Equal( + t, tc.ToSpendTxHash, + toSpendTx.TxHash().String(), + ) + + toSignTx, err := BuildToSignPacketSimple( + []byte(tc.Message), scriptPubKey, + ) + require.NoError(t, err) + + require.Equal( + t, tc.ToSignTxHash, + toSignTx.UnsignedTx.TxHash().String(), + ) + }) + } +} + +// TestSimpleSignaturesVectors tests the simple variant test vectors as +// mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestSimpleSignaturesVectors(t *testing.T) { + validateSimpleVector := func(t *testing.T, tc simpleSignatureVector) { + addr, err := address.DecodeAddress( + tc.Address, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + toSignTx, err := BuildToSignPacketSimple( + []byte(tc.Message), scriptPubKey, + ) + require.NoError(t, err) + + signature, _ := signAndExtract( + t, tc.PrivateKeys, tc.Type, tc.WitnessScript, "", + toSignTx, + ) + require.NoError(t, err) + + // Special case for the fallback to no prefix, which is allowed + // for the "simple" variant. + expectedSigs := tc.Bip322Signatures + if len(expectedSigs) == 1 && + !strings.Contains(expectedSigs[0], PrefixSimple) { + + require.Contains( + t, expectedSigs, signature[len(PrefixSimple):], + ) + } else { + require.Contains(t, expectedSigs, signature) + } + + require.Contains(t, signature, PrefixSimple) + witnessBytes, err := b64StrictDecode(signature[len(PrefixSimple):]) + require.NoError(t, err) + witness, err := ParseTxWitness(witnessBytes) + require.NoError(t, err) + + // Verify the signature with the corresponding function. + valid, _, err := VerifyMessageSimple( + []byte(tc.Message), scriptPubKey, witness, + ) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + + // And with the generic function. + valid, _, err = VerifyMessage(tc.Message, addr, expectedSigs[0]) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + + // And make sure an incorrect message produces a negative + // result. + valid, _, err = VerifyMessage( + "incorrect message", addr, expectedSigs[0], + ) + require.ErrorContains(t, err, "invalid signature") + require.False(t, valid, "signature of fake msg must be invalid") + } + + vectors := loadTestVectors(t, "basic-test-vectors.json") + + for _, tc := range vectors.Simple { + t.Run(testName(tc.Type, tc.Message), func(t *testing.T) { + validateSimpleVector(t, tc) + }) + } +} + +// TestFullSignaturesVectors tests the full variant test vectors as mentioned in +// BIP-322: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestFullSignaturesVectors(t *testing.T) { + validateFullVector := func(t *testing.T, tc fullSignatureVector) { + addr, err := address.DecodeAddress( + tc.Address, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + toSignTx, err := BuildToSignPacketFull( + []byte(tc.Message), scriptPubKey, tc.TxVersion, + tc.LockTime, tc.Sequence, + ) + require.NoError(t, err) + + signature, signedTx := signAndExtract( + t, tc.PrivateKeys, tc.Type, tc.WitnessScript, + tc.SigScript, toSignTx, + ) + + require.Contains(t, tc.Bip322Signatures, signature) + + // Verify the signature with the corresponding function. + valid, _, err := VerifyMessageFull( + []byte(tc.Message), scriptPubKey, + signedTx.TxIn[0].SignatureScript, + signedTx.TxIn[0].Witness, tc.TxVersion, tc.LockTime, + tc.Sequence, + ) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + + // And with the generic function. + valid, _, err = VerifyMessage( + tc.Message, addr, tc.Bip322Signatures[0], + ) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + + // And make sure an incorrect message produces a negative + // result. The error should be that the wrong message is + // detected at the to_sign first input prev outpoint check. + valid, _, err = VerifyMessage( + "incorrect message", addr, tc.Bip322Signatures[0], + ) + require.Error(t, err) + require.ErrorIs(t, err, ErrInvalidToSign) + require.False(t, valid, "signature of fake msg must be invalid") + } + + vectors := loadTestVectors(t, "basic-test-vectors.json") + for _, tc := range vectors.Full { + t.Run(testName(tc.Type, tc.Message), func(t *testing.T) { + validateFullVector(t, tc) + }) + } + + vectors = loadTestVectors(t, "generated-test-vectors.json") + for _, tc := range vectors.Full { + t.Run(testName(tc.Type, tc.Message), func(t *testing.T) { + validateFullVector(t, tc) + }) + } +} + +// TestPoFSignaturesVectors tests the Proof of Funds variant test vectors as +// mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestPoFSignaturesVectors(t *testing.T) { + validatePoFVector := func(t *testing.T, tc pofSignatureVector) { + addr, err := address.DecodeAddress( + tc.Address, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + // And with the generic function. + valid, _, err := VerifyMessage( + tc.Message, addr, tc.Bip322Signatures[0], + ) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + + // And make sure an incorrect message produces a negative + // result. The error should be that the wrong message is + // detected at the to_sign first input prev outpoint check. + valid, _, err = VerifyMessage( + "incorrect message", addr, tc.Bip322Signatures[0], + ) + require.Error(t, err) + require.ErrorIs(t, err, ErrInvalidToSign) + require.False(t, valid, "signature of fake msg must be invalid") + } + + vectors := loadTestVectors(t, "generated-test-vectors.json") + for _, tc := range vectors.ProofOfFunds { + t.Run(testName(tc.Type, tc.Message), func(t *testing.T) { + validatePoFVector(t, tc) + }) + } +} + +// TestErrorVectors tests that invalid signatures, wrong messages, and wrong +// addresses are correctly rejected during BIP-322 verification. +func TestErrorVectors(t *testing.T) { + testErrorVectors := func(t *testing.T, vectors *testVectors) { + for _, tc := range vectors.Error { + t.Run(tc.Description, func(t *testing.T) { + addr, err := address.DecodeAddress( + tc.Address, netParams, + ) + require.NoError(t, err) + + valid, _, err := VerifyMessage( + tc.Message, addr, tc.Signature, + ) + require.False(t, valid) + require.Error(t, err) + require.Contains( + t, err.Error(), tc.ErrorSubstr, + ) + }) + } + } + + vectors := loadTestVectors(t, "basic-test-vectors.json") + testErrorVectors(t, vectors) + + vectors = loadTestVectors(t, "generated-test-vectors.json") + testErrorVectors(t, vectors) +} + +// TestValidVectors tests that the static "valid" signature vectors (spends that +// cannot be expressed as a standard, re-signable input type) verify +// successfully. +func TestValidVectors(t *testing.T) { + testValidVectors := func(t *testing.T, vectors *testVectors) { + for _, tc := range vectors.Valid { + t.Run(tc.Description, func(t *testing.T) { + addr, err := address.DecodeAddress( + tc.Address, netParams, + ) + require.NoError(t, err) + + valid, _, err := VerifyMessage( + tc.Message, addr, tc.Signature, + ) + require.NoError(t, err) + require.True(t, valid, "signature must be valid") + }) + } + } + + vectors := loadTestVectors(t, "basic-test-vectors.json") + testValidVectors(t, vectors) + + vectors = loadTestVectors(t, "generated-test-vectors.json") + testValidVectors(t, vectors) +} + +// testInputType is a type that represents different types of inputs that are +// used to test the BIP-322 message signing and verification. +type testInputType uint8 + +const ( + plainP2PKH testInputType = iota + plainP2WPKH + plainP2TR + timeLockP2TR + nestedP2WPKH + timeLockP2WSH + multisig2of2P2WSH + multisig3of3P2WSH + nestedMultisig2of2P2WSH + legacyMultisig2of2P2SH +) + +// pofTestCase is a single test case for a Proof of Funds variant. +type pofTestCase struct { + name string + challengeInputType testInputType + pofInputTypes [][]testInputType +} + +var ( + // allTypes is the list of all defined testInputType constants. This is + // used to test the "full" variant. + allTypes = []testInputType{ + plainP2PKH, + plainP2WPKH, + plainP2TR, + timeLockP2TR, + nestedP2WPKH, + timeLockP2WSH, + multisig2of2P2WSH, + multisig3of3P2WSH, + nestedMultisig2of2P2WSH, + legacyMultisig2of2P2SH, + } + + // nativeSegWitTypes is the list of testInputType constants that use + // native segwit scripts. This is used to test the "simple" variant. + nativeSegWitTypes = []testInputType{ + plainP2WPKH, + plainP2TR, + multisig2of2P2WSH, + multisig3of3P2WSH, + } + + // legacyTypes is the list of testInputType constants that use legacy + // scripts. This is used to test that the "simple" variant errors out + // for legacy scripts. + legacyTypes = []testInputType{ + plainP2PKH, + nestedP2WPKH, + nestedMultisig2of2P2WSH, + legacyMultisig2of2P2SH, + } + + // pofTestCases is the list of all Proof of Funds test cases. + pofTestCases = []pofTestCase{ + { + name: "p2pkh", + challengeInputType: plainP2PKH, + pofInputTypes: [][]testInputType{ + {plainP2PKH}, + {nestedP2WPKH}, + }, + }, + { + name: "p2pkh-with-utxo-re-use", + challengeInputType: plainP2PKH, + pofInputTypes: [][]testInputType{ + {plainP2PKH, plainP2PKH, plainP2PKH}, + {nestedP2WPKH}, + }, + }, + { + name: "p2tr", + challengeInputType: plainP2TR, + pofInputTypes: [][]testInputType{ + {plainP2TR}, + {plainP2TR, plainP2TR}, + }, + }, + } +) + +func parseInputType(s string) (testInputType, error) { + for _, t := range allTypes { + if t.String() == s { + return t, nil + } + } + return 0, fmt.Errorf("unknown input type: %s", s) +} + +func (i testInputType) String() string { + switch i { + case plainP2PKH: + return "p2pkh" + case plainP2WPKH: + return "p2wpkh" + case plainP2TR: + return "p2tr" + case timeLockP2TR: + return "p2tr-time-lock" + case nestedP2WPKH: + return "p2sh-p2wpkh" + case timeLockP2WSH: + return "p2wsh-time-lock" + case multisig2of2P2WSH: + return "p2wsh-multisig-2of2" + case multisig3of3P2WSH: + return "p2wsh-multisig-3of3" + case nestedMultisig2of2P2WSH: + return "p2sh-p2wsh-multisig-2of2" + case legacyMultisig2of2P2SH: + return "p2sh-multisig-2of2" + default: + return "unknown" + } +} + +func (i testInputType) output(t *testing.T, + privKeys []*btcec.PrivateKey) (*wire.TxOut, string, []*btcutil.WIF, + []byte, []byte) { + + require.GreaterOrEqual(t, len(privKeys), 1) + privKey := privKeys[0] + pubKey := privKey.PubKey() + _, revokeKey := btcec.PrivKeyFromBytes(testRevokePrivateKey) + + // We're simulating a delay-to-self script which we're going to spend + // through the time lock path. + timeLockScript, err := txscript.ScriptTemplate( + timeLockTemplate, txscript.WithScriptTemplateParams( + map[string]interface{}{ + "RevokeKey": revokeKey.SerializeCompressed(), + "CsvTimeout": int64(testCSVTimeout), + "SelfKey": pubKey.SerializeCompressed(), + }, + ), + ) + require.NoError(t, err) + + taprootTimeLockScript, err := txscript.ScriptTemplate( + timeLockTemplate, txscript.WithScriptTemplateParams( + map[string]interface{}{ + "RevokeKey": schnorr.SerializePubKey( + revokeKey, + ), + "CsvTimeout": int64(testCSVTimeout), + "SelfKey": schnorr.SerializePubKey( + pubKey, + ), + }, + ), + ) + require.NoError(t, err) + + privKeyWif, err := btcutil.NewWIF(privKey, netParams, true) + require.NoError(t, err) + + var ( + addr address.Address + witnessScript []byte + sigScript []byte + signingKeys = []*btcutil.WIF{privKeyWif} + ) + switch i { + case plainP2PKH: + h := address.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = address.NewAddressPubKeyHash(h, netParams) + require.NoError(t, err) + + case plainP2WPKH: + h := address.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = address.NewAddressWitnessPubKeyHash(h, netParams) + require.NoError(t, err) + + case plainP2TR: + trKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey()) + addr, err = address.NewAddressTaproot( + schnorr.SerializePubKey(trKey), netParams, + ) + require.NoError(t, err) + + case timeLockP2TR: + witnessScript = taprootTimeLockScript + leaf := txscript.NewBaseTapLeaf(witnessScript) + tree := txscript.AssembleTaprootScriptTree(leaf) + rootHash := tree.RootNode.TapHash() + trKey := txscript.ComputeTaprootOutputKey( + privKey.PubKey(), rootHash[:], + ) + addr, err = address.NewAddressTaproot( + schnorr.SerializePubKey(trKey), netParams, + ) + require.NoError(t, err) + + case nestedP2WPKH: + witnessProgram, err := payToWitnessPubKeyHashScript(privKey) + require.NoError(t, err) + + addr, err = address.NewAddressScriptHash( + witnessProgram, netParams, + ) + require.NoError(t, err) + sigScript = witnessProgram + + case timeLockP2WSH: + witnessScript = timeLockScript + + h := sha256.Sum256(witnessScript) + addr, err = address.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + case multisig2of2P2WSH: + require.GreaterOrEqual(t, len(privKeys), 2) + + // We're simulating a 2-of-2 multisig. + key2 := privKeys[1] + params := map[string]interface{}{ + "FirstKey": pubKey.SerializeCompressed(), + "SecondKey": key2.PubKey().SerializeCompressed(), + } + witnessScript, err = txscript.ScriptTemplate( + multiSig2of2Template, txscript.WithScriptTemplateParams( + params, + ), + ) + require.NoError(t, err) + + key2Wif, err := btcutil.NewWIF(key2, netParams, true) + require.NoError(t, err) + signingKeys = []*btcutil.WIF{privKeyWif, key2Wif} + + h := sha256.Sum256(witnessScript) + addr, err = address.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + case multisig3of3P2WSH: + require.Len(t, privKeys, 3) + + // We're simulating a 3-of-3 multisig. + key2 := privKeys[1] + key3 := privKeys[2] + params := map[string]interface{}{ + "FirstKey": pubKey.SerializeCompressed(), + "SecondKey": key2.PubKey().SerializeCompressed(), + "ThirdKey": key3.PubKey().SerializeCompressed(), + } + witnessScript, err = txscript.ScriptTemplate( + multiSig3of3Template, txscript.WithScriptTemplateParams( + params, + ), + ) + require.NoError(t, err) + + key2Wif, err := btcutil.NewWIF(key2, netParams, true) + require.NoError(t, err) + key3Wif, err := btcutil.NewWIF(key3, netParams, true) + require.NoError(t, err) + signingKeys = []*btcutil.WIF{ + privKeyWif, key2Wif, key3Wif, + } + + h := sha256.Sum256(witnessScript) + addr, err = address.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + case nestedMultisig2of2P2WSH: + require.GreaterOrEqual(t, len(privKeys), 2) + + // We're simulating a 2-of-2 multisig wrapped in P2SH-P2WSH. + key2 := privKeys[1] + params := map[string]interface{}{ + "FirstKey": pubKey.SerializeCompressed(), + "SecondKey": key2.PubKey().SerializeCompressed(), + } + witnessScript, err = txscript.ScriptTemplate( + multiSig2of2Template, txscript.WithScriptTemplateParams( + params, + ), + ) + require.NoError(t, err) + + key2Wif, err := btcutil.NewWIF(key2, netParams, true) + require.NoError(t, err) + signingKeys = []*btcutil.WIF{privKeyWif, key2Wif} + + // Create the P2WSH address, then wrap it in P2SH. + h := sha256.Sum256(witnessScript) + wshAddr, err := address.NewAddressWitnessScriptHash( + h[:], netParams, + ) + require.NoError(t, err) + + wshScript, err := txscript.PayToAddrScript(wshAddr) + require.NoError(t, err) + + addr, err = address.NewAddressScriptHash(wshScript, netParams) + require.NoError(t, err) + sigScript = wshScript + + case legacyMultisig2of2P2SH: + require.GreaterOrEqual(t, len(privKeys), 2) + + // We're simulating a pure legacy P2SH 2-of-2 multisig. + key2 := privKeys[1] + params := map[string]interface{}{ + "FirstKey": pubKey.SerializeCompressed(), + "SecondKey": key2.PubKey().SerializeCompressed(), + } + sigScript, err = txscript.ScriptTemplate( + multiSig2of2Template, txscript.WithScriptTemplateParams( + params, + ), + ) + require.NoError(t, err) + + key2Wif, err := btcutil.NewWIF(key2, netParams, true) + require.NoError(t, err) + signingKeys = []*btcutil.WIF{privKeyWif, key2Wif} + + addr, err = address.NewAddressScriptHash(sigScript, netParams) + require.NoError(t, err) + + default: + t.Fatalf("invalid input type") + } + + pkScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + txOut := &wire.TxOut{ + Value: testValue, + PkScript: pkScript, + } + + return txOut, addr.String(), signingKeys, witnessScript, sigScript +} + +func (i testInputType) decorateInput(t *testing.T, privKey *btcec.PrivateKey, + in *psbt.PInput, witnessScript, sigScript []byte) { + + in.WitnessScript = witnessScript + in.RedeemScript = sigScript + + switch i { + case timeLockP2TR: + leaf := txscript.NewBaseTapLeaf(witnessScript) + tree := txscript.AssembleTaprootScriptTree(leaf) + controlBlock := tree.LeafMerkleProofs[0].ToControlBlock( + privKey.PubKey(), + ) + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + in.TaprootLeafScript = []*psbt.TaprootTapLeafScript{ + { + ControlBlock: controlBlockBytes, + Script: witnessScript, + LeafVersion: leaf.LeafVersion, + }, + } + + default: + // Nothing to do for the other script types. + } +} + +func (i testInputType) sign(t *testing.T, idx int, signingKeys []*btcutil.WIF, + packet *psbt.Packet) { + + in := &packet.Inputs[idx] + txIn := packet.UnsignedTx.TxIn[idx] + tx := packet.UnsignedTx + utxo := in.WitnessUtxo + if utxo == nil { + targetOP := txIn.PreviousOutPoint + if in.NonWitnessUtxo != nil { + utxo = in.NonWitnessUtxo.TxOut[targetOP.Index] + } else { + numInputs := len(packet.UnsignedTx.TxIn) + for lookupIdx := 1; lookupIdx < numInputs; lookupIdx++ { + lookupInput := packet.Inputs[lookupIdx] + prevTx := lookupInput.NonWitnessUtxo + if prevTx == nil { + continue + } + + if prevTx.TxHash() != targetOP.Hash { + continue + } + + utxo = prevTx.TxOut[targetOP.Index] + } + } + } + + switch i { + case plainP2PKH: + require.NoError(t, signInputLegacy( + packet, idx, utxo.PkScript, signingKeys[0].PrivKey, + )) + + case plainP2WPKH, timeLockP2WSH, nestedP2WPKH: + script := in.WitnessScript + if i == plainP2WPKH { + script = utxo.PkScript + } + if i == nestedP2WPKH { + script = in.RedeemScript + } + + require.NoError(t, signInputWitness( + packet, idx, script, signingKeys[0].PrivKey, + )) + + case plainP2TR: + require.NoError(t, signInputTaprootKeySpend( + packet, idx, signingKeys[0].PrivKey, + )) + + case timeLockP2TR: + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + + leaf := txscript.NewBaseTapLeaf(in.WitnessScript) + leafHash := leaf.TapHash() + sig, err := txscript.RawTxInTapscriptSignature( + tx, sigHashes, idx, utxo.Value, utxo.PkScript, + leaf, txscript.SigHashDefault, signingKeys[0].PrivKey, + ) + require.NoError(t, err) + in.TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{ + { + XOnlyPubKey: schnorr.SerializePubKey( + signingKeys[0].PrivKey.PubKey(), + ), + LeafHash: leafHash[:], + Signature: sig, + SigHash: txscript.SigHashDefault, + }, + } + + case multisig2of2P2WSH, multisig3of3P2WSH, nestedMultisig2of2P2WSH: + for _, key := range signingKeys { + require.NoError(t, signInputWitness( + packet, idx, in.WitnessScript, key.PrivKey, + )) + } + + case legacyMultisig2of2P2SH: + for _, key := range signingKeys { + require.NoError(t, signInputLegacy( + packet, idx, in.RedeemScript, key.PrivKey, + )) + } + } +} + +func (i testInputType) finalize(t *testing.T, packet *psbt.Packet) { + in := &packet.Inputs[0] + + var witnessStack wire.TxWitness + switch i { + case timeLockP2TR: + script := in.TaprootLeafScript[0] + scriptSpend := in.TaprootScriptSpendSig[0] + + witnessStack = make([][]byte, 4) + witnessStack[0] = scriptSpend.Signature + witnessStack[1] = nil + witnessStack[2] = in.WitnessScript + witnessStack[3] = script.ControlBlock + + case timeLockP2WSH: + sigBytes := in.PartialSigs[0].Signature + + witnessStack = make([][]byte, 3) + witnessStack[0] = sigBytes + witnessStack[1] = nil + witnessStack[2] = in.WitnessScript + + case legacyMultisig2of2P2SH: + // Pure legacy P2SH multisig requires manual finalization. + // scriptSig: OP_0 + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + for _, partialSig := range in.PartialSigs { + builder.AddData(partialSig.Signature) + } + builder.AddData(in.RedeemScript) + + var err error + in.FinalScriptSig, err = builder.Script() + require.NoError(t, err) + + return + + default: + // The PSBT finalizer knows what to do if we're not using a + // custom script. + err := psbt.MaybeFinalizeAll(packet) + require.NoError(t, err) + + return + } + + var err error + in.FinalScriptWitness, err = SerializeTxWitness(witnessStack) + require.NoError(t, err) +} + +// TestSignAndVerifySimpleScriptTypes tests the full BIP-322 signing and +// verification flow end-to-end for the script types compatible with the +// "simple" variant. +func TestSignAndVerifySimpleScriptTypes(t *testing.T) { + for _, inputType := range nativeSegWitTypes { + t.Run(inputType.String(), func(t *testing.T) { + _ = runTestCaseSimple(t, inputType) + }) + } +} + +// TestSignAndVerifyFullScriptTypes tests the full BIP-322 signing and +// verification flow end-to-end for the "full" variant. +func TestSignAndVerifyFullScriptTypes(t *testing.T) { + for _, inputType := range allTypes { + t.Run(inputType.String(), func(t *testing.T) { + _ = runTestCaseFull(t, inputType) + }) + } +} + +// TestSignAndVerifyPoFScriptTypes tests the BIP-322 signing and verification +// flow end-to-end for the "Proof of Funds" variant. +func TestSignAndVerifyPoFScriptTypes(t *testing.T) { + for _, tc := range pofTestCases { + t.Run(tc.name, func(t *testing.T) { + _ = runTestCasePoF( + t, tc.challengeInputType, tc.pofInputTypes, + ) + }) + } +} + +// TestIncompatibleSimpleScriptTypes tests that the "simple" variant cannot be +// used with legacy script types. +func TestIncompatibleSimpleScriptTypes(t *testing.T) { + for _, inputType := range legacyTypes { + t.Run(inputType.String(), func(t *testing.T) { + message := []byte(rand.Text()) + + // This is the private key we're going to sign with. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Extra keys for multisig types. + privKey2, err := btcec.NewPrivateKey() + require.NoError(t, err) + + txOut, _, _, _, _ := inputType.output( + t, []*btcec.PrivateKey{privKey, privKey2}, + ) + _, err = BuildToSignPacketSimple( + message, txOut.PkScript, + ) + require.ErrorIs(t, err, errSimpleSegWitOnly) + }) + } +} + +// runTestCaseSimple runs a test case for the "simple" variant of BIP-322. +func runTestCaseSimple(t *testing.T, + inputType testInputType) simpleSignatureVector { + + message := []byte(rand.Text()) + + // This is the private key we're going to sign with. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // And two more random keys for the multisig test cases. + privKey2, err := btcec.NewPrivateKey() + require.NoError(t, err) + privKey3, err := btcec.NewPrivateKey() + require.NoError(t, err) + + txOut, addr, wifs, witnessScript, sigScript := inputType.output( + t, []*btcec.PrivateKey{privKey, privKey2, privKey3}, + ) + toSign, err := BuildToSignPacketSimple(message, txOut.PkScript) + require.NoError(t, err) + + // Add the necessary input values. + inputType.decorateInput( + t, privKey, &toSign.Inputs[0], witnessScript, sigScript, + ) + + // Now sign the input. + inputType.sign(t, 0, wifs, toSign) + + // If the witness stack needs to be assembled, give the caller + // the option to do that now. + inputType.finalize(t, toSign) + + finalTx, err := psbt.Extract(toSign) + require.NoError(t, err) + + valid, _, err := VerifyMessageSimple( + message, txOut.PkScript, finalTx.TxIn[0].Witness, + ) + require.NoError(t, err) + require.True(t, valid) + + signature, err := SerializeSignature(toSign) + require.NoError(t, err) + + // Make sure the signature is valid. + parsedAddr, err := address.DecodeAddress(addr, netParams) + require.NoError(t, err) + valid, constraints, err := VerifyMessage( + string(message), parsedAddr, signature, + ) + require.NoError(t, err) + require.True(t, valid) + require.Equal(t, TimeConstraints{}, constraints) + + return simpleSignatureVector{ + Message: string(message), + PrivateKeys: formatWIFs(wifs), + Address: addr, + Type: inputType.String(), + WitnessScript: hexEncode(witnessScript), + Bip322Signatures: []string{ + signature, + }, + } +} + +// runTestCaseFull runs a test case for the "full" variant of BIP-322. +func runTestCaseFull(t *testing.T, + inputType testInputType) fullSignatureVector { + + message := []byte(rand.Text()) + + // This is the private key we're going to sign with. + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // And two more random keys for the multisig test cases. + privKey2, err := btcec.NewPrivateKey() + require.NoError(t, err) + privKey3, err := btcec.NewPrivateKey() + require.NoError(t, err) + + txOut, addr, wifs, witnessScript, sigScript := inputType.output( + t, []*btcec.PrivateKey{privKey, privKey2, privKey3}, + ) + + // The transaction types that don't have any custom script won't + // care about the lock time and sequence, so we just always set + // it. + toSign, err := BuildToSignPacketFull( + message, txOut.PkScript, 2, testCSVTimeout, + testCSVTimeout, + ) + require.NoError(t, err) + + // Add the necessary input values. + inputType.decorateInput( + t, privKey, &toSign.Inputs[0], witnessScript, sigScript, + ) + + // Now sign the input. + inputType.sign(t, 0, wifs, toSign) + + // If the witness stack needs to be assembled, give the caller + // the option to do that now. + inputType.finalize(t, toSign) + + finalTx, err := psbt.Extract(toSign) + require.NoError(t, err) + + valid, _, err := VerifyMessageFull( + message, txOut.PkScript, + finalTx.TxIn[0].SignatureScript, + finalTx.TxIn[0].Witness, 2, testCSVTimeout, + testCSVTimeout, + ) + require.NoError(t, err) + require.True(t, valid) + + signature, err := SerializeSignature(toSign) + require.NoError(t, err) + + // Make sure the signature is valid. + parsedAddr, err := address.DecodeAddress(addr, netParams) + require.NoError(t, err) + valid, constraints, err := VerifyMessage( + string(message), parsedAddr, signature, + ) + require.NoError(t, err) + require.True(t, valid) + require.Equal( + t, TimeConstraints{true, testCSVTimeout, testCSVTimeout}, + constraints, + ) + + return fullSignatureVector{ + simpleSignatureVector: simpleSignatureVector{ + Message: string(message), + PrivateKeys: formatWIFs(wifs), + Address: addr, + Type: inputType.String(), + WitnessScript: hexEncode(witnessScript), + Bip322Signatures: []string{ + signature, + }, + }, + SigScript: hexEncode(sigScript), + TxVersion: 2, + LockTime: testCSVTimeout, + Sequence: testCSVTimeout, + } +} + +// runTestCasePoF runs a test case for the "proof of funds" variant of BIP-322. +func runTestCasePoF(t *testing.T, challengeInputType testInputType, + pofInputTypes [][]testInputType) pofSignatureVector { + + message := []byte(rand.Text()) + + challengePrivateKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + challengeWif, err := btcutil.NewWIF( + challengePrivateKey, netParams, true, + ) + require.NoError(t, err) + + allWifs := []*btcutil.WIF{challengeWif} + + type pofInput struct { + typ testInputType + prevTx *wire.MsgTx + prevTxIndex int + txOut *wire.TxOut + pInput psbt.PInput + privateKey *btcec.PrivateKey + } + + // We start by creating enough private keys. We need a separate key for + // the challenge input and then one each per POF input. + keyIndex := 1 + additionalInputs := make([][]additionalInput, len(pofInputTypes)) + var pofInputs []pofInput + for idx, inputTypes := range pofInputTypes { + additionalInputs[idx] = make([]additionalInput, len(inputTypes)) + + prevTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + Witness: wire.TxWitness{}, + }}, + TxOut: make([]*wire.TxOut, len(inputTypes)), + } + for j, inputType := range inputTypes { + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + txOut, _, wifs, witnessScript, sigScript := + inputType.output( + t, []*btcec.PrivateKey{privKey}, + ) + + prevTx.TxOut[j] = txOut + additionalInputs[idx][j] = additionalInput{ + PrivateKeyIndex: keyIndex, + Type: inputType.String(), + Value: txOut.Value, + PkScript: hexEncode(txOut.PkScript), + } + allWifs = append(allWifs, wifs...) + + pInput := psbt.PInput{ + RedeemScript: sigScript, + WitnessScript: witnessScript, + } + + if isNativeSegWitPkScript(txOut.PkScript) || + inputType == nestedP2WPKH { + + pInput.WitnessUtxo = txOut + } else { + pInput.NonWitnessUtxo = prevTx + } + + pofInputs = append(pofInputs, pofInput{ + typ: inputType, + prevTx: prevTx, + prevTxIndex: j, + txOut: txOut, + pInput: pInput, + privateKey: privKey, + }) + + keyIndex++ + } + } + + // We assume that all input types are single-sig types for this test + // case. We start by assembling the base to_sign transaction. + txOut, addr, _, witnessScript, sigScript := challengeInputType.output( + t, []*btcec.PrivateKey{challengePrivateKey}, + ) + toSign, err := BuildToSignPacketFull( + message, txOut.PkScript, 2, 123, 456, + ) + require.NoError(t, err) + + // Now we can add the additional inputs to the transaction. + for _, pofInput := range pofInputs { + toSign.UnsignedTx.TxIn = append( + toSign.UnsignedTx.TxIn, &wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: pofInput.prevTx.TxHash(), + Index: uint32(pofInput.prevTxIndex), + }, + }, + ) + toSign.Inputs = append(toSign.Inputs, pofInput.pInput) + } + + // Now that we've assembled the full transaction, we can start to sign. + challengeInputType.sign(t, 0, []*btcutil.WIF{challengeWif}, toSign) + for idx, pofInput := range pofInputs { + wif, err := btcutil.NewWIF(pofInput.privateKey, netParams, true) + require.NoError(t, err) + pofInput.typ.sign(t, idx+1, []*btcutil.WIF{wif}, toSign) + } + + err = psbt.MaybeFinalizeAll(toSign) + require.NoError(t, err) + + // For our test vectors, we want to show the special case for + // deduplicating NonWitnessUTXOs. + deduplicateNonWitnessUtxos(toSign) + + // We also strip the generic signed message field, as that's not needed + // inside the "signature" part, since it will always be transported to + // the verifier as part of the "message, address, signature" triplet. + toSign.GenericSignedMessage = nil + + signature, err := SerializeSignature(toSign) + require.NoError(t, err) + + // Make sure the signature is valid. + parsedAddr, err := address.DecodeAddress(addr, netParams) + require.NoError(t, err) + valid, constraints, err := VerifyMessage( + string(message), parsedAddr, signature, + ) + require.NoError(t, err) + require.True(t, valid) + require.Equal(t, TimeConstraints{true, 123, 456}, constraints) + + return pofSignatureVector{ + fullSignatureVector: fullSignatureVector{ + simpleSignatureVector: simpleSignatureVector{ + Message: string(message), + PrivateKeys: formatWIFs(allWifs), + Address: addr, + Type: challengeInputType.String(), + WitnessScript: hexEncode(witnessScript), + Bip322Signatures: []string{ + signature, + }, + }, + SigScript: hexEncode(sigScript), + TxVersion: 2, + LockTime: 123, + Sequence: 456, + }, + AdditionalInputs: additionalInputs, + } +} + +// randomAddress generates a random address for the given input type. +func randomAddress(t *testing.T, inputType testInputType) string { + t.Helper() + + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + privKey2, err := btcec.NewPrivateKey() + require.NoError(t, err) + privKey3, err := btcec.NewPrivateKey() + require.NoError(t, err) + + _, addr, _, _, _ := inputType.output( + t, []*btcec.PrivateKey{privKey, privKey2, privKey3}, + ) + return addr +} + +// TestUpdateTestVectors updates the test vectors in the testdata folder. +func TestUpdateTestVectors(t *testing.T) { + if os.Getenv("BIP322_REGEN") == "" { + t.Skip("set BIP322_REGEN=1 to regenerate test vectors") + } + + var vectors testVectors + for _, inputType := range nativeSegWitTypes { + vectors.Simple = append( + vectors.Simple, runTestCaseSimple(t, inputType), + ) + } + for _, inputType := range allTypes { + vectors.Full = append( + vectors.Full, runTestCaseFull(t, inputType), + ) + } + for _, tc := range pofTestCases { + vectors.ProofOfFunds = append( + vectors.ProofOfFunds, runTestCasePoF( + t, tc.challengeInputType, tc.pofInputTypes, + ), + ) + } + + // Generate error vectors from the simple variants. + for i, tc := range vectors.Simple { + inputType := nativeSegWitTypes[i] + + // Wrong message: valid signature verified against a + // different message. + vectors.Error = append(vectors.Error, errorVector{ + Description: fmt.Sprintf( + "wrong message for %s simple signature", + inputType, + ), + Message: rand.Text(), + Address: tc.Address, + Signature: tc.Bip322Signatures[0], + ErrorSubstr: "invalid signature", + }) + + // Wrong signer: valid signature verified against a + // different address of the same type. + vectors.Error = append(vectors.Error, errorVector{ + Description: fmt.Sprintf( + "wrong signer for %s simple signature", + inputType, + ), + Message: tc.Message, + Address: randomAddress(t, inputType), + Signature: tc.Bip322Signatures[0], + ErrorSubstr: "invalid signature", + }) + } + + // Generate error vectors from the full variants. + for i, tc := range vectors.Full { + inputType := allTypes[i] + + // Wrong message: valid signature verified against a + // different message. + vectors.Error = append(vectors.Error, errorVector{ + Description: fmt.Sprintf( + "wrong message for %s full signature", + inputType, + ), + Message: rand.Text(), + Address: tc.Address, + Signature: tc.Bip322Signatures[0], + ErrorSubstr: "invalid to_sign transaction", + }) + + // Wrong signer: valid signature verified against a + // different address of the same type. + vectors.Error = append(vectors.Error, errorVector{ + Description: fmt.Sprintf( + "wrong signer for %s full signature", + inputType, + ), + Message: tc.Message, + Address: randomAddress(t, inputType), + Signature: tc.Bip322Signatures[0], + ErrorSubstr: "invalid to_sign transaction", + }) + } + + storeTestVectors(t, &vectors, "generated-test-vectors.json") +} diff --git a/bip322/testdata/basic-test-vectors.json b/bip322/testdata/basic-test-vectors.json index 6f835176ca..23e5554987 100644 --- a/bip322/testdata/basic-test-vectors.json +++ b/bip322/testdata/basic-test-vectors.json @@ -62,6 +62,202 @@ "bip322_signatures": [ "smpBQBHMEQCIFX9aaqPJWq2Ff2kpen5bFDTid+ehgUOpHV0LfjncXy4AiA3GNicF7aKPzdpa9PCpmaYQs3pHd+qbvvhXdxOCKCAMAFIMEUCIQD/ELXg6CNYyUQijCg96JtgvgjZb9dsl1Ctof4QAeyTcQIgVM/1AAblFl/DCt6A1gJg+T/i2qU5SQD09+chFJzolRwBSDBFAiEAlqRfSFyWNVQhvaCnmeV5tyneiCWMTcFbuujoD/pFa3wCIGnZjfQb8NolSYq9asV+ZeBSkCGHJcqnaV4JYS5MYPEGAWlTIQJ1aLEfEi/4p7wcV+XHZCBVvGGJZ7L3v+jhH+mZA8lN0yECCovfec+kIdllXpKCgA8RX/HZ2x5yHOtCSKP8/sf6pnwhAwxSng6kCgCXXSAmJOOZFdr3vdK3HzGqCFloOHgc5fM6U64=" ] + }, + { + "message": "No prefix fallback", + "private_keys": [ + "KyrSGCFPhqZMjCe5fNTYddiLMp4tMj4gLKuJ26TsB2rvr1VJGPbt" + ], + "address": "bc1pss0zhytly75awhm6x2hhvd5lnzv3vssgrf9axfheq8ldyzn88ges79fler", + "type": "p2tr", + "witness_script": "", + "bip322_signatures": [ + "AUCJYOwOjxYAvatTAGYaVlNXBVyFuc4MwNQkOuK2tl8xhfKDONd0NjfYyNSYcRqeCp8hsAnCEPHAVEkO9h6vbQ/R" + ] + } + ], + "valid": [ + { + "description": "valid P2TR tapscript (HTLC) spend whose preimage resembles a 65-byte Schnorr signature", + "message": "Hello World", + "address": "bc1prcjws24grzqyatm0h24xpr0t7h57rhprty4ewzgn9zh8r85qgwes0vjpr5", + "signature": "smpBUDFyv0qSSuNwHwxyMs064kILVtcCgnx4wXoYoRONmDYwjiS2d2hncDXPBIbh2jqNzeuzMPKYs1pGwJtJzmkXhwZQQECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0ACAQFvY6ggc9cCfr9GRSq4DoYv9ZUX2DDVRcctvXDM/4YAmN55fUyIIO7HJF1rfSzLMDgL++KjZIzXqUJlP1qjQO3OofKDaGYZrGcC9AGxdSAkZT6sQ0SIACzAa7+38Q/hiZHjX5/kMC2+ptI1PcCrHKxoIcB/MevFRiwf3OG3N+z/UtN9dd6kPOEcdNJaopcWX6ogBw==" + }, + { + "description": "valid P2WSH script (HTLC) spend whose preimage resembles a DER-encoded ECDSA signature", + "message": "Hello World", + "address": "bc1q9zgd3xcxanse37y7365zty493qf725rhw5mjpqm36f62vltvcwssrp6vul", + "signature": "smpBEgwRQIhAIdOUJUBtWvNlc6fAJ5RNU6NN9XbByNFLc3Cf4yGQXnJAiAX0Mj6w6Ur4am4LHZzhCQPntXvBKcB+NJqA/hXrI3TTgEgMB0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBAXFjqCAgx8lbUGza5gB08FceetY5oFOgaOzdSA3MhtO/vUMalYghA7r3aJwKNVj7YEWJA2qNHktoXZCfbg4sYBihQEmuZOwmrGcC9AGxdSECnB9YWqgHYvn0RY9oBpeLe++0bHtor4LTmWMHk/8aK1CsaA==" + } + ], + "error": [ + { + "description": "invalid base64 encoding", + "message": "", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "not-valid-base64!!!", + "error_substr": "base64" + }, + { + "description": "empty signature", + "message": "", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "", + "error_substr": "signature too short" + }, + { + "description": "wrong message for valid simple p2wpkh signature (empty message was signed)", + "message": "Wrong message that was not signed", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + "error_substr": "invalid signature" + }, + { + "description": "wrong address for valid simple p2wpkh signature (signed for different address)", + "message": "", + "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9", + "signature": "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + "error_substr": "invalid signature" + }, + { + "description": "empty witness stack (single zero byte)", + "message": "", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "smpAA==", + "error_substr": "invalid signature" + }, + { + "description": "wrong message for valid simple p2wsh 3-of-3 multisig signature", + "message": "This is not the message that was signed", + "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9", + "signature": "smpBQBHMEQCIFX9aaqPJWq2Ff2kpen5bFDTid+ehgUOpHV0LfjncXy4AiA3GNicF7aKPzdpa9PCpmaYQs3pHd+qbvvhXdxOCKCAMAFIMEUCIQD/ELXg6CNYyUQijCg96JtgvgjZb9dsl1Ctof4QAeyTcQIgVM/1AAblFl/DCt6A1gJg+T/i2qU5SQD09+chFJzolRwBSDBFAiEAlqRfSFyWNVQhvaCnmeV5tyneiCWMTcFbuujoD/pFa3wCIGnZjfQb8NolSYq9asV+ZeBSkCGHJcqnaV4JYS5MYPEGAWlTIQJ1aLEfEi/4p7wcV+XHZCBVvGGJZ7L3v+jhH+mZA8lN0yECCovfec+kIdllXpKCgA8RX/HZ2x5yHOtCSKP8/sf6pnwhAwxSng6kCgCXXSAmJOOZFdr3vdK3HzGqCFloOHgc5fM6U64=", + "error_substr": "invalid signature" + }, + { + "description": "invalid signature prefix", + "message": "", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fooAA==", + "error_substr": "error decoding signature as base64" + }, + { + "description": "incorrect prefix type", + "message": "incorrect prefix", + "address": "bc1pyrgrm6cu6n54jrvkdjd9rvyd3xfyu84s2623awu2srn6mxhscwpsm5644w", + "signature": "fulAUDZwFXUp+adN+/UZj5dVrGAbB3zKs1Vcalz5fCF9srxS63eSWNGvH1NYbrBkPt1BJDUyWUz9zgUxfc63/QheT6M", + "error_substr": "error parsing signature as full variant" + }, + { + "description": "wrong first input prev outpoint (not to_spend.txid)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABAefgOV4T7eH9omMAYEh6usEbN3XsHUAEWZO6hYxpaBffAAAAAAAAAAAAAQAAAAAAAAAAAWoCRzBEAiAgE8y4jECU7FwFLPCvrQoE8TZrrfUt51hP55swxsaCvQIgRNrC4iRRYjw3C7FMt625ui9R8fpmQirSUmhr/uQP57sBIQLH8SADGWRClD2FiOAa7oQEI8xU/BUhUmo7hcKwy9WIcgAAAAA=", + "error_substr": "does not match to_spend txid" + }, + { + "description": "to_sign has more than one output", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAgAAAAAAAAAAAWoAAAAAAAAAAAFqAkgwRQIhAJU6h3iXWbvNYmcUkBvuMlNw2QuSElQaXuHoKZDG/01iAiBSvxO21J1OY7+SA0XpepI6wdPyeuWUBj9RQZozDUlc1gEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1YhyAAAAAA==", + "error_substr": "to_sign must have exactly one output" + }, + { + "description": "to_sign output scriptPubKey is not OP_RETURN", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAXYCSDBFAiEA+C6IExKqNl4xTKrT3RE5yoUD8fuCIAh3MmWgp/LphTkCIEopcLx0+uZAzttqhOT/hr+x1oyKlTIybR1qEKabHQ7FASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHIAAAAA", + "error_substr": "to_sign output scriptPubKey must be a single OP_RETURN byte" + }, + { + "description": "to_sign output value is non-zero", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQEAAAAAAAAAAWoCSDBFAiEAvB0R/Tuyyn19FfZEhfOqDz7dt39ez130ltFwIoihpMsCIBGGwEct2Di7NKTdkXWtMv8uBKAI7fz4WNTjE8cgsKeVASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHIAAAAA", + "error_substr": "to_sign output value must be 0" + }, + { + "description": "to_sign has tx version 1 (must be 0 or 2)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAQAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAWoCSDBFAiEAt3+tQT2ohV4qdGYyYUtvjb0huVHDYYapZMtBLvMtPQMCIFPE0tz79jasG9W0ptM0V5uLaI85jhNyU0UD3xE8fGjaASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHIAAAAA", + "error_substr": "inconclusive: to_sign tx version must be 0 or 2" + }, + { + "description": "to_sign has tx version 3 (must be 0 or 2)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAwAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAWoCSDBFAiEA1+uw/ICOtLbY+d3UDnoFpbCUW+B2W7ZbzXYG20PAsoACIEDVw194aGEZum0DtWw0FzCy2IGFFkxRRrn0cI9EyaynASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHIAAAAA", + "error_substr": "inconclusive: to_sign tx version must be 0 or 2" + }, + { + "description": "ECDSA signature with sighash flag SIGHASH_NONE (must be SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAWoCRzBEAiAO6q+NrtgkK9FKOwkfNd7BLr4QVqqc9wtPhTIl5P/H3gIgbieR7UiA+SuQM6bdn4dF9J+AAr0shhlcanEiJkF495YCIQLH8SADGWRClD2FiOAa7oQEI8xU/BUhUmo7hcKwy9WIcgAAAAA=", + "error_substr": "invalid sighash flag" + }, + { + "description": "ECDSA signature with sighash flag SIGHASH_SINGLE (must be SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAWoCSDBFAiEApP1p14ZOAWklkPF1atoOCCFmBp/dtjhMQU4P43VZGEICIEMG4eprl9nS9AXVT/P/O9fGVU8DJys0juITXvMb6DKhAyECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHIAAAAA", + "error_substr": "invalid sighash flag" + }, + { + "description": "ECDSA signature with sighash flag SIGHASH_ALL|ANYONECANPAY (must be SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + "signature": "fulAAAAAAABASs1A9aiYU3q8XFsIzJcU+BRS0r8mBAcdxdSrUBnGZ23AAAAAAAAAAAAAQAAAAAAAAAAAWoCRzBEAiAinWb6HWWl1jBphd4FBc1Bisy2azxbhdN0/r8yt1M7uwIgRePkKdg+kAlUmOVSq+CkT22B1FoFzw1bkYG7kzlRWxGBIQLH8SADGWRClD2FiOAa7oQEI8xU/BUhUmo7hcKwy9WIcgAAAAA=", + "error_substr": "invalid sighash flag" + }, + { + "description": "P2TR Schnorr signature with incorrect explicit sighash byte (must be SIGHASH_DEFAULT or SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + "signature": "fulAAAAAAABAQZ52yMWanylo3mYung2wzGYu6l1UmV7Eo2xCNKfbmYhAAAAAAAAAAAAAQAAAAAAAAAAAWoBQeWGTijlm2zsFP/scDgJ1Z9oJGXI04hbwDN0aSYV6dduty9fJukSljKih3NhPLm+rR8nrHfymhOThSP/NTC+NgsCAAAAAA==", + "error_substr": "invalid sighash flag" + }, + { + "description": "OP_CODESEPARATOR in a P2WSH witness script is forbidden", + "message": "Hello World", + "address": "bc1q00yx5wpnln4c2ya097u0cu3ae4auxcglk4rwkyvs58ayv2lnsvwqj7h0zr", + "signature": "smpAQKrUQ==", + "error_substr": "OP_CODESEPARATOR is forbidden" + }, + { + "description": "OP_CODESEPARATOR in a P2TR tapscript (script-path spend) is forbidden", + "message": "Hello World", + "address": "bc1psncxkzalrhfwlssw6enn6n6nje9gm2cvujqdkg8zm0jrzr4nfuyqsvu5mh", + "signature": "smpAgKrUSHBTzVb3LfMCvco7zzOuWFdkGhLtbLKX4WasPC3BAdYcao=", + "error_substr": "OP_CODESEPARATOR is forbidden" + }, + { + "description": "P2TR tapscript signature with disallowed sighash flag SIGHASH_SINGLE (must be SIGHASH_DEFAULT or SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1pfqwdte9y4k4su0pnngk3uz5qku5gpq8gtrcuxc0n6fd05w8dzkpqr45pea", + "signature": "smpBUF71zyYilHdnJwf9jjUEQtK/He6SNyyzEbP4r4tlSzP+dmQvcfKICfjuDLbPTdf8Ld5eXvWebl3t6OG3ZvjnYo+AyABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fIAEBb2OoIK4hbC71JHo3gsE176J5o+TNxhCUJw9dK+WMYgS3phLJiCCNdQDdTBJoXR9Wi0wrUEjoU0uHMxnzqNqmErRpEy7H96xnAvQBsXUgRm1/yuVj5csJoNGHC7WANEgEYXh5oUlJzyIoXxuuPyesaCHA4R9Ar2tB9JS/vCfEeheM5XLouMpofMZ+EphRSGGsXkg=", + "error_substr": "invalid sighash flag" + }, + { + "description": "P2WSH multisig signature with disallowed sighash flag SIGHASH_SINGLE (must be SIGHASH_ALL)", + "message": "Hello World", + "address": "bc1qrhfzums6ve0ftkdr8fltwwu0udv5hcyx9m94j5kl9md95y0447kq58rxyu", + "signature": "smpBABHMEQCIH4Vxgnaif9dJoJqXOcrREwo68vOrueuniF9FMMrQsjTAiAklN3WWQCTYyWFs2Kg3OlYl+SaNxGUkqmD4lyG36nfrgFHMEQCIAM/gld3ksCnpFqOF84RhvnPWGbeO4N16eGkC7ZMIHusAiBwbwuQc9O+OKkM6TgGPrQyd2/DZ5LPKh2jGNVFcn6BKgNHUiEDaTD0bdCxbYZtWdEFSqYymLNXSZzRhi7xbz9V8cr864IhApCZnbv0MDS/+x3VPqwetMM6TqHE9Iulhc/eODCEDwVVUq4=", + "error_substr": "invalid sighash flag" + }, + { + "description": "reserved upgradeable NOP (OP_NOP5) in witness script is inconclusive", + "message": "Hello World", + "address": "bc1qqv2jj3l8f73fkcqj3dwgduc2dfur30u3mgk5n4zm8dupl8jq5lhqmjjpa3", + "signature": "smpAQK0UQ==", + "error_substr": "inconclusive" + }, + { + "description": "tx version 1 is inconclusive", + "message": "probe", + "address": "bc1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsq2gp3gp", + "signature": "fulAQAAAAABAWYb1fFuDTtm8ZysRgBkdesyTwY3KY18Cy3BNkDnQv0NAAAAAAAAAAAAAQAAAAAAAAAAAWoBAVEAAAAA", + "error_substr": "inconclusive" } ] } \ No newline at end of file diff --git a/bip322/testdata/generated-test-vectors.json b/bip322/testdata/generated-test-vectors.json new file mode 100644 index 0000000000..81984cb8e3 --- /dev/null +++ b/bip322/testdata/generated-test-vectors.json @@ -0,0 +1,551 @@ +{ + "simple": [ + { + "message": "DWKEDKK2C5SCLJB46FD4N5XVIH", + "private_keys": [ + "KzfkAdtcYCWB6fDAEMjGf75Y6tKyHbLfo1cvE9zSfxJtCmqnwVGR" + ], + "address": "bc1q090mjp3fangcwx3pcpdvnx2etdp99skt9p4vk9", + "type": "p2wpkh", + "witness_script": "", + "bip322_signatures": [ + "smpAkgwRQIhALqdF6nJrsRYgUY4UeMZ/IlfFAj0K43HM9DSKBwYrCZGAiB7oEbn6MQ8zRrTZOkv0k2dnBofPCsM9hvpXiHMTsaf3AEhAqywYj2W44HMCxv+rcxoryJjUNnfYA5AmwlKzjcgoig6" + ] + }, + { + "message": "GWX7UEKBRIBFSYORDOFQQNP7BH", + "private_keys": [ + "KzaLPeT6D4muD7V6Zudb9UhEd4CvQhTj9a44Tcw7FUw3GuF5ksGY" + ], + "address": "bc1pud70rjk9wrcm62s465s70ynjz5mpyrvfyr2v7yrklsf8xq4z04csd4kp97", + "type": "p2tr", + "witness_script": "", + "bip322_signatures": [ + "smpAUDp1jDqJyx/+FBRLy0GWSo8InmrDxcz2tZ1eeYR/QM+OPsXG841HOnFCi5Z9jgFiyThNKUHRC2b0LQQTNNLESX2" + ] + }, + { + "message": "EOYMRLSL6QMSGALT4LDZDACRQX", + "private_keys": [ + "L59HJynJ2QBLKRq2cvoXguMtMX8kBvyBV8nXT56SrWbFN8kVG2kM", + "L55ivV1431huuHkWdTv66D4RGmMFKpzwm56qqKdRdxWjqdh71beb" + ], + "address": "bc1q0aduk9j2v8kn4fd5ux4uzejje8vekdfkpdl6fmdtus0h43wc0tmqvhklds", + "type": "p2wsh-multisig-2of2", + "witness_script": "522103b81680e17f3a14260b5717b5fc4b2ad492a4e4c5ffc8a127e5e89a9483b9b8d621032d13fb4f8b6f60e2a5ad75b7b46794e86865b5b089b7f19aa859c311f5c3e90b52ae", + "bip322_signatures": [ + "smpBABIMEUCIQDNbOUISwseZi7zTTWon52IZA/uWLKA5dO0nC/qlJNKsQIgKt9DRBcPgR3+mkgY9btMd0X6VZOUE9cKEjFvCPYXESUBSDBFAiEAjYatzWASwnYVdPuW4J3CNuAjdi3QkVn6lk7wi87O0YsCIET0T43N25WhsHGpFtPImYvnQ4mgbpQ+6HiZ65ezrSHKAUdSIQO4FoDhfzoUJgtXF7X8SyrUkqTkxf/IoSfl6JqUg7m41iEDLRP7T4tvYOKlrXW3tGeU6GhltbCJt/GaqFnDEfXD6QtSrg==" + ] + }, + { + "message": "UF6EAYJVM4INFEFNUOQOV5MFLY", + "private_keys": [ + "L44xmqcbUEBZ2Em7rLXj5TyTutn4Udf8zDFrP4NuVTRjjSmgZUqi", + "L2YeVEzU33RW3YfCxBu8X4PEjdkpjfKsTZuNnNRCaMWHzkj2XqmH", + "L5AgpcQXndS9WpGYzCazpRsLczybmSHxtz6Jn1ykMjvHe8TG33sf" + ], + "address": "bc1qj2wph7l089xksx3yjafd5r9p7d0nfwpzul28dvwgac7fy26myzcq9fx428", + "type": "p2wsh-multisig-3of3", + "witness_script": "532103a9cda0487b0cc349328fcf4d4dce11f9a41b09a57bacc30caa2b871cba92aca821022b22373ec7c1322a5965e36f5c211ad6c1262a2093da4c8e7830942d268e3fd121038e76913effdf6705a8bfc5de2b4d05165369f68fae22d9f002478fb4b9db353d53ae", + "bip322_signatures": [ + "smpBQBHMEQCIAsmBmCU8lABOVvvU8XCLt4Dza/0UukLStNMtH3CsOS+AiBH0y5mKRORqpVsDjewYTLyDaQ7ZxoMUxSc7ZeK6zx/9gFHMEQCIH7uqO5c2jWeFqymma/KX8kEabjjjqlxxbK5W1nsqgHwAiAjZcwfww+PNTP7TgY/QNcC4FGCiy8N7tSKhW+wzwMAdwFHMEQCIGMLlgYW01vsyDGkAhOP9kcvZrAllcV4agCmYeOUttVfAiAcHAHVPUPD9F+GKno+N6ei4f8qQZzHQrvC9cPLEiSPpQFpUyEDqc2gSHsMw0kyj89NTc4R+aQbCaV7rMMMqiuHHLqSrKghAisiNz7HwTIqWWXjb1whGtbBJiogk9pMjngwlC0mjj/RIQOOdpE+/99nBai/xd4rTQUWU2n2j64i2fACR4+0uds1PVOu" + ] + } + ], + "full": [ + { + "message": "MBC2L6KNBD7UQABVPAHQ3E7JLJ", + "private_keys": [ + "KyrwqUywq4dJxPoyESEmtrSECqpw1N2eCotfiB3AheRQMqDxUe15" + ], + "address": "1MZPxUBqc5DS3RTC8WTh1VYxJp65AHG5X", + "type": "p2pkh", + "witness_script": "", + "bip322_signatures": [ + "fulAgAAAAE4Q2BTvhQOFbtSdO5Vb3uYV3uo5EGqEo8ORW8gd8mRRQAAAABrSDBFAiEAgp8LwYzt8GrCj+wlIbFsTi7fJCRSFrPWeh/X1AyC5+YCIG1Ea+fmGcnC/h5rqhcVeRQBfdbg5gkFmScXr7jlppIYASEDI7cMtsm1/aN6H5bLakkfn8M5xB4RQLmKxlEEfXm8Ew3gBwAAAQAAAAAAAAAAAWrgBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "IFCSDWSHR5YUM6ATSTZN4A37UB", + "private_keys": [ + "L2BSYwyJxexgg73HSomiKbi2oJuUuqpDASYBJMQPspPUEZ79LmYb" + ], + "address": "bc1qyct9q2dt33fwp36ndctl9qjxp2wyp4lkxyrrke", + "type": "p2wpkh", + "witness_script": "", + "bip322_signatures": [ + "fulAgAAAAABARhcUa+co2TTcV3YFt0xVCeiy0tPngbuOXz2TidvIxgjAAAAAADgBwAAAQAAAAAAAAAAAWoCSDBFAiEA5qoIVPxw4+ZL3AtJZnxb0gkMrxnqPrV+Cbw3GlYy570CIAjjCRmP9/mJG0iPMCq9ZCzHdl3tQZ6CL8qpCn/hirKqASEDP5mOTnq5psmfip3kCAI6oCpgvKGeom3Sd9MJrSyU3orgBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "7CTUGAM2KLQPVZJ3IJXOFC7WMZ", + "private_keys": [ + "KyjJXhG7DGU2qcprDtHw5PS9zDbfKAuLyf3jig9VyYAu26hssddu" + ], + "address": "bc1pwge6ts0a5dpnzaz6cedk88scrga0q4udaz6ad0u0402atng3y0pq8r7qtw", + "type": "p2tr", + "witness_script": "", + "bip322_signatures": [ + "fulAgAAAAABAb17jzVmxvgSiVm+dn6Ob1/rXWESvf7WHX7eOcbxJrgWAAAAAADgBwAAAQAAAAAAAAAAAWoBQMKitvm0qbmmios5ZoVUzv3C4vErEwScValAySAgGQ/N5coNRFVmVdbfyEF85DUN2EyynJBEAM1JQsCjEZsIOU7gBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "B5UFDLDC6XUEUNIKIUCUFS6A5P", + "private_keys": [ + "KwVS4AD5j73i9mS2THqgP3GRfB39RuDErC6Qo65RfB3mSdQ4PdNt" + ], + "address": "bc1p8pa69clqmu5ue8gfskha0txgdmm644y8aj5ark7x42gpftky9r6q5c54sz", + "type": "p2tr-time-lock", + "witness_script": "6320ad87d784e921d02bf0b89a41f8eded6a5d8409f3b4bfb935fc0e0f4e519c42206702e007b275208b3649dd61e3d77befa5f0b4cb50f721b6e965f93f36d5b38f2f41af3fa0180e68ac", + "bip322_signatures": [ + "fulAgAAAAABAWHsykrvU5UhgczJ0I1FO2J0PTEr9CGU7CioMPQn05qgAAAAAADgBwAAAQAAAAAAAAAAAWoEQPCB3xQj+6rNL6Ui5d8K8NO1/5Xu31KKip94G2JOxZPOiWh7RAp1kH1V9miQ0dgLMAfQvr2FT0QLqm1UlaYHUgkAS2MgrYfXhOkh0CvwuJpB+O3tal2ECfO0v7k1/A4PTlGcQiBnAuAHsnUgizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA5orCHAizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA7gBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "SYSIUK3F5ECJCVY7STPPSY7BZG", + "private_keys": [ + "L1g1kjhUeqLghX1VGhwrLX2DKN9fundWgcPsXQXu3xJUwMkRA32B" + ], + "address": "34Rpaf7qKjEFtSa5cgjFV65Gesn1MG6RRj", + "type": "p2sh-p2wpkh", + "witness_script": "", + "bip322_signatures": [ + "fulAgAAAAABAQMtwtqA+ZStiSLrfJX3ec/a4pLdlUfuTSt9abma5PL/AAAAABcWABS9SnOwd4tfU/UhvE0+TWxou2gO/+AHAAABAAAAAAAAAAABagJHMEQCICWcT45/nNjC/SPQBfk+Mh4nWutH1AIGaeOdGadrd2M3AiB8odieK9o74A+BANgOX8XUkZw+tGRu2DBLfheVoDzaQAEhA05byjCvMoykHQ4XgePcWqeVNUMH+fpK8dPKl1ct//EJ4AcAAA==" + ], + "sig_script": "0014bd4a73b0778b5f53f521bc4d3e4d6c68bb680eff", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "EGA4VPOGMJ3RGA6MVU6CQ6OHES", + "private_keys": [ + "L13fzbNkEB6VLpz2yvZMDhw39DHbzqtAFxrcNCX6itypBdkrt3LZ" + ], + "address": "bc1qqjvf4qtmq28jpchxedh0c0lphhxmcv3yf5j9tn5a9ywara4qa9xqes7wc3", + "type": "p2wsh-time-lock", + "witness_script": "632103ad87d784e921d02bf0b89a41f8eded6a5d8409f3b4bfb935fc0e0f4e519c42206702e007b27521023100c29f212daa62773ca8da62ab9a170704b6a082b00d78d28b93b86f1da03d68ac", + "bip322_signatures": [ + "fulAgAAAAABAV2F55emQmzAKIV2VdMFYK9L60v+6btGTxpnKndpnJMnAAAAAADgBwAAAQAAAAAAAAAAAWoDRzBEAiAU8bId2stu5NhW8pokxs5q1nmU5vxI627ez1qAW+/SkwIgAVFOEtEGhAuUvFtRYbT2AhR4XTY8p7vOhynUj3Y5M7cBAE1jIQOth9eE6SHQK/C4mkH47e1qXYQJ87S/uTX8Dg9OUZxCIGcC4AeydSECMQDCnyEtqmJ3PKjaYquaFwcEtqCCsA140ouTuG8doD1orOAHAAA=" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "WKIFF76HOU7UP5PNACPRDNKJOE", + "private_keys": [ + "KzapuQjwGLtUwtRzPoAsCDE2XVsiDhKKbL5oCbWRzP6VtJeaEvaT", + "L51wtFYD8RC1LA467e3MivQ3PbDr9EkGPzFqZihHoLjDJbLkDrRK" + ], + "address": "bc1qx4yfhk64vsvf78hng6yn3eyudy80mk4fyugjd7y3q7pszkqh9q7s76mwej", + "type": "p2wsh-multisig-2of2", + "witness_script": "522102a1ce88b53d580a4e2f25bc3420c8fdbfa0a7c08a499049727d32a70d05646b1c2103aac48701c1af8d0563f45426cbcd1977c9785dfa48aeaa6cd7c6852a81e082d952ae", + "bip322_signatures": [ + "fulAgAAAAABAeAlTevE8YGUlsjgMRJGD0IMswMYWmAhQfS+CouqnzwBAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAO/+3iXpm3JTGZEHX1+MLk0u+LlHxZoyEB5c1sPrpauOAiBLUUFtBazZc9WT0mCFhcpiebA2mgkcYm51YhXfCmdXgAFHMEQCICwhUWvNFWoe8/3l6+bYY2ykw/uP4nXPqhj/ezhf05VwAiAgki9SUuDNhUW8S8Hpdp/wpe15QblU6Dc9u57l/AvqKgFHUiECoc6ItT1YCk4vJbw0IMj9v6CnwIpJkElyfTKnDQVkaxwhA6rEhwHBr40FY/RUJsvNGXfJeF36SK6qbNfGhSqB4ILZUq7gBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "A7YVK3GGY4MMUA6C4F2L3ASEDW", + "private_keys": [ + "L1GzeBthJxp3JE6SLm69RohKxgRjkdESmWuPBDwPFfFr8ew1b2ox", + "L152Rf1hW2aduPUcm3S3zhHF2oUJdu75S9USutzi48c7czqt8JiE", + "L4wR9CK2vacfwutTqc4QAJk1BTbLZpmDbTyZYTtirZeYUCHKpXpJ" + ], + "address": "bc1qfspfhvzzrqektdx76uj0kzds6h3lu8wdh8vk3z2g28m4zem7xl7s47l3ng", + "type": "p2wsh-multisig-3of3", + "witness_script": "532102bb251bbcc206a07d8338fd483b385123c474877c87e31042618f66269de81e8821036373bbca4e43d039f3832b04a0c306376c18b3b6696f336a52f2cdbbea6123f32102ec14965a4e1071f437c8cc4184708f116e7a3757b678613925924dd5715a536953ae", + "bip322_signatures": [ + "fulAgAAAAABAVgs1IXe15rh8SqO2jbTmgmtdP1gsS6/T/RxdRinI8xMAAAAAADgBwAAAQAAAAAAAAAAAWoFAEcwRAIgAyXERMyt400pIZIz3RGO4F2p4n6rsBrnARunlirgXW8CIAHMthgjnkYtDIsP4japYFfDafSPOCTgnD2BBzlqXfWXAUcwRAIgRZPUCX9TMbhAKYZWXk8S7o5K2zzG9eAG/1wZVv9rtZMCIBSmylJ6Y6f9tQnRsYaXmYiB9EuMFE+1laNLGRbvrd+XAUcwRAIgeE6VBIf47QmMNLr9rrD1YrUJt7wyetg+oHYJ/avg5QoCIHxh4zBqYBQxOoHbYmQglu6o3Ap3i5h4RzQylDfyzUASAWlTIQK7JRu8wgagfYM4/Ug7OFEjxHSHfIfjEEJhj2YmnegeiCEDY3O7yk5D0DnzgysEoMMGN2wYs7ZpbzNqUvLNu+phI/MhAuwUllpOEHH0N8jMQYRwjxFuejdXtnhhOSWSTdVxWlNpU67gBwAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "4FMVP445Q7UDQA5PHEJAJPEBXT", + "private_keys": [ + "L5UF2rhF8NvGM2kphoB5mMaLj2mxRQTmdPDu1VHujqFLbw21bJRC", + "L1FcXBhHVzZRDRXh1HbfMHqnxHTMRdzXwvBroLvE9zdefyXsyTmm" + ], + "address": "3QN3gGr1WL5i4Z7njGLJDdosTf9VqukC9m", + "type": "p2sh-p2wsh-multisig-2of2", + "witness_script": "522102acd65215b3d69765fb1722d5a51f867fbfb466dd1a775182e693c6003dd978a021026483849c6f580c4f1e57600d77cf61739499052bb96e510d0a5871275845d79f52ae", + "bip322_signatures": [ + "fulAgAAAAABAehf4I+wavWpjzx3T1sLa5ERLVWGKnKTl+j4U5mqHCzLAAAAACMiACC36LM1vvTTMwlLFLFTidJzSIX+OH19Zkx4aYdQ/D6W6uAHAAABAAAAAAAAAAABagQARzBEAiA1/E8KVNhFgVwXFFtvbQWHm7uttryotBvNZLnraDauOgIgFGQS7pBg4g7QRj4ju/s2IlTx4MvVIololRRBKUGXypYBSDBFAiEA7yxOM8miz0NSaezUc3gmcHVGRuVXcmP85aUxQt51bPECIAzUXn6gV9REeRpyt2fLFdh38LR/JqIlDFGMNCknEtiMAUdSIQKs1lIVs9aXZfsXItWlH4Z/v7Rm3Rp3UYLmk8YAPdl4oCECZIOEnG9YDE8eV2ANd89hc5SZBSu5blENClhxJ1hF159SruAHAAA=" + ], + "sig_script": "0020b7e8b335bef4d333094b14b15389d2734885fe387d7d664c78698750fc3e96ea", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + }, + { + "message": "2NXJGXTLGKREF37LY4MYDAPAAJ", + "private_keys": [ + "KzFv15eWoFf3Ea4HZv37DBjt8U9ADjWRZo69jvALSuETupT1mrB5", + "L2xacKNjQX82mktsadGwvjoq1YwjT1zX3mVbWGu2VFHCB51TnJG4" + ], + "address": "3EnD6QQdPwVNLzw7MYdkJN6m9refVLvuYN", + "type": "p2sh-multisig-2of2", + "witness_script": "", + "bip322_signatures": [ + "fulAgAAAAENwuZZhE9piECSsE0ZpR8VUpJwdppH6aVrtpJoKEIfcAAAAADZAEcwRAIgGI69RNIL2JR0dw0raRB2t8mFJPwgU+7bVp/amPMo9t0CIH0/rzAfdfEiUXzhr/3nXdMJPLRGy3uc8k4/9ROVHGM5AUcwRAIgMDqFXDbvCRK9EOxw0vQ9thRDvXpMz/kBL0jnHsz58e0CID4OPnrsKRN5z80oCBKv9THGwjWzI5sScn25cMPrcAtlAUdSIQNmoQbyT39W+zRP0Rr6nsO2sh99TqTZ3XL2Gxglm+PnoCECmjX7zXfdntCWYKfhiC6xxyZzPZF/Nune1w8ewSi9PMBSruAHAAABAAAAAAAAAAABauAHAAA=" + ], + "sig_script": "52210366a106f24f7f56fb344fd11afa9ec3b6b21f7d4ea4d9dd72f61b18259be3e7a021029a35fbcd77dd9ed09660a7e1882eb1c726733d917f36e9ded70f1ec128bd3cc052ae", + "tx_version": 2, + "lock_time": 2016, + "sequence": 2016 + } + ], + "proof_of_funds": [ + { + "message": "DVVJPIKSJBEZIKKH75YMIOWY5U", + "private_keys": [ + "L1pPTFcv695A3wRhr3BfxwQ6YbbASTXXRTnrqGKhycPFKTFs5Nej", + "KwZyUGnv1F63qDmAh5MSJ9ZFxSUKyvJ7rCbYEnqJosNiEXJ3H3hV", + "L1RHjuekTuEZtizFzLYC4mJF1K5Sm9yq7cajsyp9s5TViMaNt1eM" + ], + "address": "1LBG4ok3P4QmhU1jzBQLkqtgcBak3m6P8U", + "type": "p2pkh", + "witness_script": "", + "bip322_signatures": [ + "pofcHNidP8BAI8CAAAAAzhakorxa1lQzERqfi97npI+DJlVGuMYSikFL+plY3+XAAAAAADIAQAAmLvvZ6yn5m56ZcXCm1qLScRWBiGKDfmmiwgTkPsfnskAAAAAAAAAAAA9kDdc1fDNQ39XxIoLyGEUeuUOTMl6Tp91HOtae/Dc6gAAAAAAAAAAAAEAAAAAAAAAAAFqewAAAAABAHcAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////yIAIMoDOUmC2lXzCG/6HvBljs3sr0VI/sOXkuqPw8ErCeE3AAAAAAEAAAAAAAAAABl2qRTSWyAq7XAwW6scW8wKe0JAF3bAz4isAAAAAAEHakcwRAIgLblNPIyOcYqlWzD1M1P0xRo0f+TLX74j4jXmYBWyDngCIF8cuefb+6oo8FzQfXbV3LNzWj3rvf+w/Bzchko2vi3NASECmXctUKrrhiVdtEyugLljylqDGupLddFXAQrHto4oqZMAAQBVAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFORgUAAAAAABl2qRRj2bA9GLPghwyYJrexCMX4A/uuq4isAAAAAAEHa0gwRQIhAMskmH7nxrPVHQG4fS2x5SW8qwn9reHmf8+siXXH1SQKAiBJl6Pw/yxZOGARGecqMEepCWAFfWPcJrpW2lrO+faH5AEhA3VTGOTrsTuXV/ht+J1ggPYjRGk3Vrzl382thVCR/cPlAAEBIE5GBQAAAAAAF6kUwWEalCaf2qWusyDaO0QvnG5i/fWHAQcXFgAUSrBr53awnJ+3nPi0yy7TzvY+6b4BCGsCRzBEAiAU7pXVKAPZ5OrREjRkXGySWsN6p6ouS4m7jT24aXgUVQIgVNfeiQAfZXQuAujtgp4X+K2LKwRtCJA9wcUF7T1SWZMBIQJo/g2GH5jZrWyxTkYYl+jpYnQJ2hzQjzt34TnuFiR7sAAA" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 123, + "sequence": 456, + "additional_inputs": [ + [ + { + "private_key_index": 1, + "type": "p2pkh", + "value": 345678, + "pk_script": "76a91463d9b03d18b3e0870c9826b7b108c5f803fbaeab88ac" + } + ], + [ + { + "private_key_index": 2, + "type": "p2sh-p2wpkh", + "value": 345678, + "pk_script": "a914c1611a94269fdaa5aeb320da3b442f9c6e62fdf587" + } + ] + ] + }, + { + "message": "B6KTPSLHSE3GO2PXJVGBNOUKWE", + "private_keys": [ + "L5dP14Z5BYNjiSAvqEMdPe54BRWfj9xDmFEWFUtyqaMy35E8cyJo", + "KySeEpMLMNfgypd6P8ActLaQtYLCZgLHJ7ambsQ32WHmLFYfSqNd", + "L3URw3bCwhVXZCSsK5GdGK41MC5Jnp7R8AcHuLKC3M8AzXZKYuYa", + "L2UU2AC3LCJKDGA31khAYAWbwfPMdHx9GH96MSrQCPBiPRfDm3HH", + "L5dJoJ2tBRKVLPUcQPb2sbHpFYWvHK9Hi5s8Uy7sgGPunQLdbxPs" + ], + "address": "1FFV3MRZZrzmxDo9ujtZ6foL6jYqR286tR", + "type": "p2pkh", + "witness_script": "", + "bip322_signatures": [ + "pofcHNidP8BAOECAAAABX924shXghkk7lZFlxQTsqnzhuRZEBIbJjGIv33rhBjgAAAAAADIAQAA+Nf0xvIZBOcDxbzpBF8e2lxZrA2b33cg7zk+vVBo7E0AAAAAAAAAAAD41/TG8hkE5wPFvOkEXx7aXFmsDZvfdyDvOT69UGjsTQEAAAAAAAAAAPjX9MbyGQTnA8W86QRfHtpcWawNm993IO85Pr1QaOxNAgAAAAAAAAAA3WfwpvHV7RuJVgCah2cXmBsy+xTmioRZf/aDrRTOeO0AAAAAAAAAAAABAAAAAAAAAAABansAAAAAAQB3AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8iACBXzO/CT8IkNar6mlFAZs+yBcIWhh27982a++5nJJlT/wAAAAABAAAAAAAAAAAZdqkUnE7909XOub71oVvcQ3+TpiW0ZJ6IrAAAAAABB2pHMEQCIBqmqWOj2VMbgDrvRRIuNXgwhe70hNQs6KLAqcoyZdq5AiBIuxFbH7Qwqx2K8ezeVnLplmzb/GYI2K8UXyQ/12pxrgEhApGMrjqJplDlUQz+hlbCwjG9GbqP1FZGydW4GNNVnBQ2AAEAmQAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADTkYFAAAAAAAZdqkUIqdLLotzDVo9koPrjbliK8TEOkeIrE5GBQAAAAAAGXapFBuK3D1hKyCNcC5bjgql39nvxUP7iKxORgUAAAAAABl2qRSQ1tdRvtf3NLQmUCPh/TftLMbSY4isAAAAAAEHakcwRAIgHUwP7f5J9YYQ/BJOnnXkWe8RIHlgccuD+4ZB/D3Wr1cCIEyYt49slk2jY/ijpTCS6FvSAhaGbfVOOYEHIl2kLAORASECBpjIAEQvke2LxLJnFgjGkPo7zK/dXY2MzqQySY5yWPkAAQdqRzBEAiBRae6+A3ow8wXCB9hE/6duwxSrFk7T0+PbeURb+yr6iAIgK0nlMm8dbY6ZUGJ4dc0xIv/CqYdej7OHhJZPiAFRl+gBIQOLBf4vHc5mV2/ecJIWSbXs5W+MJDTdUthepiZFj0kE5QABB2tIMEUCIQD7ewo4ByWfo0lOYx2D4A61pQAKKxXeBG88xkbCW+fSYwIgBG2HE6EeZXon5sJIKEQCbwhbGPwE3z+byVm+/dbWHXsBIQLw5qs9lqCyBYN8qyyxUbIXxB2GMc5rQtVk+14WDIYMagABASBORgUAAAAAABepFAaTSkDIgJd87RpmCAwHrrLmgH/6hwEHFxYAFGoWpRuZCF/8O1ktUDx+9M9FQJhyAQhrAkcwRAIgajWlV8Jo3hVDlgVmaKWYTfS2JcWCjsPI2fxg7QVzM+MCIH89X6MS0F6+x6zZ+hyaliEwoP96TgLFklgF2EQC794YASEDUzmt7UyzNuqkbySie/6CA5XLhlY4wn5BdjaBiG+8yBcAAA==" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 123, + "sequence": 456, + "additional_inputs": [ + [ + { + "private_key_index": 1, + "type": "p2pkh", + "value": 345678, + "pk_script": "76a91422a74b2e8b730d5a3d9283eb8db9622bc4c43a4788ac" + }, + { + "private_key_index": 2, + "type": "p2pkh", + "value": 345678, + "pk_script": "76a9141b8adc3d612b208d702e5b8e0aa5dfd9efc543fb88ac" + }, + { + "private_key_index": 3, + "type": "p2pkh", + "value": 345678, + "pk_script": "76a91490d6d751bed7f734b4265023e1fd37ed2cc6d26388ac" + } + ], + [ + { + "private_key_index": 4, + "type": "p2sh-p2wpkh", + "value": 345678, + "pk_script": "a91406934a40c880977ced1a66080c07aeb2e6807ffa87" + } + ] + ] + }, + { + "message": "KOWVNEVAHBYFKJJLE4WCQ37DMI", + "private_keys": [ + "KywwN6EfHwnNQbeWhWJkVbEqqY2RvoMzUrvUgPmZCnCMm5wbEgcs", + "KxGJVASeftrdCN5Rctc4fgB5cgixNyBg4EUGkar9443vX2MErP58", + "L3U8QJsP9xZpyNKN6HShRFcta6yBMS9FNTfkfYbiiXt2TwftubmS", + "KyGx6ui5g3MyuCUjZPi7GLADNXdZ1zJcJAF1QpcuwjoxBxLXjTeC" + ], + "address": "bc1p6jw2axp9yf25ftj2sytumrjspyrw7v5gy4zg2nm8n9ndfd4eyamqxwql4p", + "type": "p2tr", + "witness_script": "", + "bip322_signatures": [ + "pofcHNidP8BALgCAAAABIGlmfCetQ0TJuRe8FqQIKWyzno553wxHY0/eEJb167IAAAAAADIAQAAsdlHtqOb1NKybMj9TIX0EUcmqPjW4JDwSeS9H6kXLeUAAAAAAAAAAABWUW9kUuUP6CoCUqeMqjLI5VdjAjJ045moMuNK6kukmQAAAAAAAAAAAFZRb2RS5Q/oKgJSp4yqMsjlV2MCMnTjmagy40rqS6SZAQAAAAAAAAAAAQAAAAAAAAAAAWp7AAAAAAEBKwAAAAAAAAAAIlEg1JyumCUiVUSuSoEXzY5QCQbvMoglRIVPZ5lm1La5J3YBCEIBQLyyK2u7G6/sK9q9AqGBrj45zZQ29MnCY6GOY6iOrsIbq+5KSzDiXgwu9viOfgjkQutJFAyJeDQLTx3M4Vsq4wIAAQErTkYFAAAAAAAiUSDhM/8zTiHCEx6s0l/kSBAuavGCOS6AyumKK4CIMRKi6QEIQgFAClyEXlMj3OP3vupZ7MZo4mZ/VbMQjQPuhSv/hGKw/vNLGpilGX0fqddKTci5Oj6z1jPFWDfcixsaeTZHm3ZvkAABAStORgUAAAAAACJRII2TnpxBOcxC+lYs5+c2tYlq0HV1Q7sLpzO9ddKkJVlpAQhCAUCMK3h/p4wxIsTQ/Opg+KLFFaoz3i+BIj+7krRERqNsuHUGUQFCw8py4CB4pcc+Gc0ShQpnQXT24odDu1oISayWAAEBK05GBQAAAAAAIlEgWmtej7ItXqv38Ewp41JKtVX4r7Svr3PBYIhsO7aSvbMBCEIBQFNRtcGU1iGuUCaG2CsUUJpoyucKWIGsJSywQElIhtKpUqTbr5Z3sWTSB3xMPEkU0BDkWISnPwVNOATB7zcEW1kAAA==" + ], + "sig_script": "", + "tx_version": 2, + "lock_time": 123, + "sequence": 456, + "additional_inputs": [ + [ + { + "private_key_index": 1, + "type": "p2tr", + "value": 345678, + "pk_script": "5120e133ff334e21c2131eacd25fe448102e6af182392e80cae98a2b80883112a2e9" + } + ], + [ + { + "private_key_index": 2, + "type": "p2tr", + "value": 345678, + "pk_script": "51208d939e9c4139cc42fa562ce7e736b5896ad0757543bb0ba733bd75d2a4255969" + }, + { + "private_key_index": 3, + "type": "p2tr", + "value": 345678, + "pk_script": "51205a6b5e8fb22d5eabf7f04c29e3524ab555f8afb4afaf73c160886c3bb692bdb3" + } + ] + ] + } + ], + "error": [ + { + "description": "wrong message for p2wpkh simple signature", + "message": "76RNV6SPRBKPPKGJ726LD3GNXN", + "address": "bc1q090mjp3fangcwx3pcpdvnx2etdp99skt9p4vk9", + "signature": "smpAkgwRQIhALqdF6nJrsRYgUY4UeMZ/IlfFAj0K43HM9DSKBwYrCZGAiB7oEbn6MQ8zRrTZOkv0k2dnBofPCsM9hvpXiHMTsaf3AEhAqywYj2W44HMCxv+rcxoryJjUNnfYA5AmwlKzjcgoig6", + "error_substr": "invalid signature" + }, + { + "description": "wrong signer for p2wpkh simple signature", + "message": "DWKEDKK2C5SCLJB46FD4N5XVIH", + "address": "bc1qzt86fc7yqhfscd45hlukra6mdphtfpjrksap4l", + "signature": "smpAkgwRQIhALqdF6nJrsRYgUY4UeMZ/IlfFAj0K43HM9DSKBwYrCZGAiB7oEbn6MQ8zRrTZOkv0k2dnBofPCsM9hvpXiHMTsaf3AEhAqywYj2W44HMCxv+rcxoryJjUNnfYA5AmwlKzjcgoig6", + "error_substr": "invalid signature" + }, + { + "description": "wrong message for p2tr simple signature", + "message": "3CNVWDW2C3X7H27FDFHMIW6G7W", + "address": "bc1pud70rjk9wrcm62s465s70ynjz5mpyrvfyr2v7yrklsf8xq4z04csd4kp97", + "signature": "smpAUDp1jDqJyx/+FBRLy0GWSo8InmrDxcz2tZ1eeYR/QM+OPsXG841HOnFCi5Z9jgFiyThNKUHRC2b0LQQTNNLESX2", + "error_substr": "invalid signature" + }, + { + "description": "wrong signer for p2tr simple signature", + "message": "GWX7UEKBRIBFSYORDOFQQNP7BH", + "address": "bc1pg7huhanj3xw8z5kd65cwgvuz792pw39fdqav6qzmav7znarcsrvsvyv8yf", + "signature": "smpAUDp1jDqJyx/+FBRLy0GWSo8InmrDxcz2tZ1eeYR/QM+OPsXG841HOnFCi5Z9jgFiyThNKUHRC2b0LQQTNNLESX2", + "error_substr": "invalid signature" + }, + { + "description": "wrong message for p2wsh-multisig-2of2 simple signature", + "message": "55MTFP5OGWQGQ4ARXEIZSRPHL6", + "address": "bc1q0aduk9j2v8kn4fd5ux4uzejje8vekdfkpdl6fmdtus0h43wc0tmqvhklds", + "signature": "smpBABIMEUCIQDNbOUISwseZi7zTTWon52IZA/uWLKA5dO0nC/qlJNKsQIgKt9DRBcPgR3+mkgY9btMd0X6VZOUE9cKEjFvCPYXESUBSDBFAiEAjYatzWASwnYVdPuW4J3CNuAjdi3QkVn6lk7wi87O0YsCIET0T43N25WhsHGpFtPImYvnQ4mgbpQ+6HiZ65ezrSHKAUdSIQO4FoDhfzoUJgtXF7X8SyrUkqTkxf/IoSfl6JqUg7m41iEDLRP7T4tvYOKlrXW3tGeU6GhltbCJt/GaqFnDEfXD6QtSrg==", + "error_substr": "invalid signature" + }, + { + "description": "wrong signer for p2wsh-multisig-2of2 simple signature", + "message": "EOYMRLSL6QMSGALT4LDZDACRQX", + "address": "bc1qt7m6jxtvvpjrn79qjv0w653arpuxljdhl55fztje8awsrje5uagsn6s42p", + "signature": "smpBABIMEUCIQDNbOUISwseZi7zTTWon52IZA/uWLKA5dO0nC/qlJNKsQIgKt9DRBcPgR3+mkgY9btMd0X6VZOUE9cKEjFvCPYXESUBSDBFAiEAjYatzWASwnYVdPuW4J3CNuAjdi3QkVn6lk7wi87O0YsCIET0T43N25WhsHGpFtPImYvnQ4mgbpQ+6HiZ65ezrSHKAUdSIQO4FoDhfzoUJgtXF7X8SyrUkqTkxf/IoSfl6JqUg7m41iEDLRP7T4tvYOKlrXW3tGeU6GhltbCJt/GaqFnDEfXD6QtSrg==", + "error_substr": "invalid signature" + }, + { + "description": "wrong message for p2wsh-multisig-3of3 simple signature", + "message": "JFVRG6ASL4DGOYD55A5YJIRDTY", + "address": "bc1qj2wph7l089xksx3yjafd5r9p7d0nfwpzul28dvwgac7fy26myzcq9fx428", + "signature": "smpBQBHMEQCIAsmBmCU8lABOVvvU8XCLt4Dza/0UukLStNMtH3CsOS+AiBH0y5mKRORqpVsDjewYTLyDaQ7ZxoMUxSc7ZeK6zx/9gFHMEQCIH7uqO5c2jWeFqymma/KX8kEabjjjqlxxbK5W1nsqgHwAiAjZcwfww+PNTP7TgY/QNcC4FGCiy8N7tSKhW+wzwMAdwFHMEQCIGMLlgYW01vsyDGkAhOP9kcvZrAllcV4agCmYeOUttVfAiAcHAHVPUPD9F+GKno+N6ei4f8qQZzHQrvC9cPLEiSPpQFpUyEDqc2gSHsMw0kyj89NTc4R+aQbCaV7rMMMqiuHHLqSrKghAisiNz7HwTIqWWXjb1whGtbBJiogk9pMjngwlC0mjj/RIQOOdpE+/99nBai/xd4rTQUWU2n2j64i2fACR4+0uds1PVOu", + "error_substr": "invalid signature" + }, + { + "description": "wrong signer for p2wsh-multisig-3of3 simple signature", + "message": "UF6EAYJVM4INFEFNUOQOV5MFLY", + "address": "bc1q47shwzau5dkpcgs2cssfx5ctz374l6s4tu54gfjkql7yg2smhnhspryylf", + "signature": "smpBQBHMEQCIAsmBmCU8lABOVvvU8XCLt4Dza/0UukLStNMtH3CsOS+AiBH0y5mKRORqpVsDjewYTLyDaQ7ZxoMUxSc7ZeK6zx/9gFHMEQCIH7uqO5c2jWeFqymma/KX8kEabjjjqlxxbK5W1nsqgHwAiAjZcwfww+PNTP7TgY/QNcC4FGCiy8N7tSKhW+wzwMAdwFHMEQCIGMLlgYW01vsyDGkAhOP9kcvZrAllcV4agCmYeOUttVfAiAcHAHVPUPD9F+GKno+N6ei4f8qQZzHQrvC9cPLEiSPpQFpUyEDqc2gSHsMw0kyj89NTc4R+aQbCaV7rMMMqiuHHLqSrKghAisiNz7HwTIqWWXjb1whGtbBJiogk9pMjngwlC0mjj/RIQOOdpE+/99nBai/xd4rTQUWU2n2j64i2fACR4+0uds1PVOu", + "error_substr": "invalid signature" + }, + { + "description": "wrong message for p2pkh full signature", + "message": "LB7WQRWTJQQCPUITEOXJHONHQU", + "address": "1MZPxUBqc5DS3RTC8WTh1VYxJp65AHG5X", + "signature": "fulAgAAAAE4Q2BTvhQOFbtSdO5Vb3uYV3uo5EGqEo8ORW8gd8mRRQAAAABrSDBFAiEAgp8LwYzt8GrCj+wlIbFsTi7fJCRSFrPWeh/X1AyC5+YCIG1Ea+fmGcnC/h5rqhcVeRQBfdbg5gkFmScXr7jlppIYASEDI7cMtsm1/aN6H5bLakkfn8M5xB4RQLmKxlEEfXm8Ew3gBwAAAQAAAAAAAAAAAWrgBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2pkh full signature", + "message": "MBC2L6KNBD7UQABVPAHQ3E7JLJ", + "address": "16yXVaKnprZg13caydx8szthVVKyE6n5cK", + "signature": "fulAgAAAAE4Q2BTvhQOFbtSdO5Vb3uYV3uo5EGqEo8ORW8gd8mRRQAAAABrSDBFAiEAgp8LwYzt8GrCj+wlIbFsTi7fJCRSFrPWeh/X1AyC5+YCIG1Ea+fmGcnC/h5rqhcVeRQBfdbg5gkFmScXr7jlppIYASEDI7cMtsm1/aN6H5bLakkfn8M5xB4RQLmKxlEEfXm8Ew3gBwAAAQAAAAAAAAAAAWrgBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2wpkh full signature", + "message": "TGQMAWZBVV63LIZU3GDDQKNIN5", + "address": "bc1qyct9q2dt33fwp36ndctl9qjxp2wyp4lkxyrrke", + "signature": "fulAgAAAAABARhcUa+co2TTcV3YFt0xVCeiy0tPngbuOXz2TidvIxgjAAAAAADgBwAAAQAAAAAAAAAAAWoCSDBFAiEA5qoIVPxw4+ZL3AtJZnxb0gkMrxnqPrV+Cbw3GlYy570CIAjjCRmP9/mJG0iPMCq9ZCzHdl3tQZ6CL8qpCn/hirKqASEDP5mOTnq5psmfip3kCAI6oCpgvKGeom3Sd9MJrSyU3orgBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2wpkh full signature", + "message": "IFCSDWSHR5YUM6ATSTZN4A37UB", + "address": "bc1qsewr4t8ryhlnpfadj656ghk3e57axgnj5j3sxr", + "signature": "fulAgAAAAABARhcUa+co2TTcV3YFt0xVCeiy0tPngbuOXz2TidvIxgjAAAAAADgBwAAAQAAAAAAAAAAAWoCSDBFAiEA5qoIVPxw4+ZL3AtJZnxb0gkMrxnqPrV+Cbw3GlYy570CIAjjCRmP9/mJG0iPMCq9ZCzHdl3tQZ6CL8qpCn/hirKqASEDP5mOTnq5psmfip3kCAI6oCpgvKGeom3Sd9MJrSyU3orgBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2tr full signature", + "message": "UFY4FCWJHDAIBHN6ZNCGPODBCB", + "address": "bc1pwge6ts0a5dpnzaz6cedk88scrga0q4udaz6ad0u0402atng3y0pq8r7qtw", + "signature": "fulAgAAAAABAb17jzVmxvgSiVm+dn6Ob1/rXWESvf7WHX7eOcbxJrgWAAAAAADgBwAAAQAAAAAAAAAAAWoBQMKitvm0qbmmios5ZoVUzv3C4vErEwScValAySAgGQ/N5coNRFVmVdbfyEF85DUN2EyynJBEAM1JQsCjEZsIOU7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2tr full signature", + "message": "7CTUGAM2KLQPVZJ3IJXOFC7WMZ", + "address": "bc1pal8elc6s45yt6c5s2v8rg2y8qh20naw5mylxml25huqej7lcp8qsp8rsg6", + "signature": "fulAgAAAAABAb17jzVmxvgSiVm+dn6Ob1/rXWESvf7WHX7eOcbxJrgWAAAAAADgBwAAAQAAAAAAAAAAAWoBQMKitvm0qbmmios5ZoVUzv3C4vErEwScValAySAgGQ/N5coNRFVmVdbfyEF85DUN2EyynJBEAM1JQsCjEZsIOU7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2tr-time-lock full signature", + "message": "X3XWTU4CBKN4A7BN5FDHOI4G7N", + "address": "bc1p8pa69clqmu5ue8gfskha0txgdmm644y8aj5ark7x42gpftky9r6q5c54sz", + "signature": "fulAgAAAAABAWHsykrvU5UhgczJ0I1FO2J0PTEr9CGU7CioMPQn05qgAAAAAADgBwAAAQAAAAAAAAAAAWoEQPCB3xQj+6rNL6Ui5d8K8NO1/5Xu31KKip94G2JOxZPOiWh7RAp1kH1V9miQ0dgLMAfQvr2FT0QLqm1UlaYHUgkAS2MgrYfXhOkh0CvwuJpB+O3tal2ECfO0v7k1/A4PTlGcQiBnAuAHsnUgizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA5orCHAizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2tr-time-lock full signature", + "message": "B5UFDLDC6XUEUNIKIUCUFS6A5P", + "address": "bc1pz3dceqkq3wtf6f7gn6y504vju3k9gu5qalcdkfy2zrpj4v9sdl0qu8x2el", + "signature": "fulAgAAAAABAWHsykrvU5UhgczJ0I1FO2J0PTEr9CGU7CioMPQn05qgAAAAAADgBwAAAQAAAAAAAAAAAWoEQPCB3xQj+6rNL6Ui5d8K8NO1/5Xu31KKip94G2JOxZPOiWh7RAp1kH1V9miQ0dgLMAfQvr2FT0QLqm1UlaYHUgkAS2MgrYfXhOkh0CvwuJpB+O3tal2ECfO0v7k1/A4PTlGcQiBnAuAHsnUgizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA5orCHAizZJ3WHj13vvpfC0y1D3IbbpZfk/NtWzjy9Brz+gGA7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2sh-p2wpkh full signature", + "message": "3FKWOZRGSSIRKEOE3QQO4Q35IC", + "address": "34Rpaf7qKjEFtSa5cgjFV65Gesn1MG6RRj", + "signature": "fulAgAAAAABAQMtwtqA+ZStiSLrfJX3ec/a4pLdlUfuTSt9abma5PL/AAAAABcWABS9SnOwd4tfU/UhvE0+TWxou2gO/+AHAAABAAAAAAAAAAABagJHMEQCICWcT45/nNjC/SPQBfk+Mh4nWutH1AIGaeOdGadrd2M3AiB8odieK9o74A+BANgOX8XUkZw+tGRu2DBLfheVoDzaQAEhA05byjCvMoykHQ4XgePcWqeVNUMH+fpK8dPKl1ct//EJ4AcAAA==", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2sh-p2wpkh full signature", + "message": "SYSIUK3F5ECJCVY7STPPSY7BZG", + "address": "3LE14Y6iMA9fjzSzwk8TjZgj2vDm4TvKef", + "signature": "fulAgAAAAABAQMtwtqA+ZStiSLrfJX3ec/a4pLdlUfuTSt9abma5PL/AAAAABcWABS9SnOwd4tfU/UhvE0+TWxou2gO/+AHAAABAAAAAAAAAAABagJHMEQCICWcT45/nNjC/SPQBfk+Mh4nWutH1AIGaeOdGadrd2M3AiB8odieK9o74A+BANgOX8XUkZw+tGRu2DBLfheVoDzaQAEhA05byjCvMoykHQ4XgePcWqeVNUMH+fpK8dPKl1ct//EJ4AcAAA==", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2wsh-time-lock full signature", + "message": "6VHHGSAVS4ZEWI6ASOPR3B42EG", + "address": "bc1qqjvf4qtmq28jpchxedh0c0lphhxmcv3yf5j9tn5a9ywara4qa9xqes7wc3", + "signature": "fulAgAAAAABAV2F55emQmzAKIV2VdMFYK9L60v+6btGTxpnKndpnJMnAAAAAADgBwAAAQAAAAAAAAAAAWoDRzBEAiAU8bId2stu5NhW8pokxs5q1nmU5vxI627ez1qAW+/SkwIgAVFOEtEGhAuUvFtRYbT2AhR4XTY8p7vOhynUj3Y5M7cBAE1jIQOth9eE6SHQK/C4mkH47e1qXYQJ87S/uTX8Dg9OUZxCIGcC4AeydSECMQDCnyEtqmJ3PKjaYquaFwcEtqCCsA140ouTuG8doD1orOAHAAA=", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2wsh-time-lock full signature", + "message": "EGA4VPOGMJ3RGA6MVU6CQ6OHES", + "address": "bc1qvwg9wd32g8xjh95h9g0tcru4cmyvjmslxk3njazqklnwrldvvruqt54gye", + "signature": "fulAgAAAAABAV2F55emQmzAKIV2VdMFYK9L60v+6btGTxpnKndpnJMnAAAAAADgBwAAAQAAAAAAAAAAAWoDRzBEAiAU8bId2stu5NhW8pokxs5q1nmU5vxI627ez1qAW+/SkwIgAVFOEtEGhAuUvFtRYbT2AhR4XTY8p7vOhynUj3Y5M7cBAE1jIQOth9eE6SHQK/C4mkH47e1qXYQJ87S/uTX8Dg9OUZxCIGcC4AeydSECMQDCnyEtqmJ3PKjaYquaFwcEtqCCsA140ouTuG8doD1orOAHAAA=", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2wsh-multisig-2of2 full signature", + "message": "QCMPUTHI5XRA6AVZBWKUALEUU6", + "address": "bc1qx4yfhk64vsvf78hng6yn3eyudy80mk4fyugjd7y3q7pszkqh9q7s76mwej", + "signature": "fulAgAAAAABAeAlTevE8YGUlsjgMRJGD0IMswMYWmAhQfS+CouqnzwBAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAO/+3iXpm3JTGZEHX1+MLk0u+LlHxZoyEB5c1sPrpauOAiBLUUFtBazZc9WT0mCFhcpiebA2mgkcYm51YhXfCmdXgAFHMEQCICwhUWvNFWoe8/3l6+bYY2ykw/uP4nXPqhj/ezhf05VwAiAgki9SUuDNhUW8S8Hpdp/wpe15QblU6Dc9u57l/AvqKgFHUiECoc6ItT1YCk4vJbw0IMj9v6CnwIpJkElyfTKnDQVkaxwhA6rEhwHBr40FY/RUJsvNGXfJeF36SK6qbNfGhSqB4ILZUq7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2wsh-multisig-2of2 full signature", + "message": "WKIFF76HOU7UP5PNACPRDNKJOE", + "address": "bc1qnzslrleu7q6xt4575j9sjln6flu2fuqmjqdg8wd3jvjyk6meuknsuvequa", + "signature": "fulAgAAAAABAeAlTevE8YGUlsjgMRJGD0IMswMYWmAhQfS+CouqnzwBAAAAAADgBwAAAQAAAAAAAAAAAWoEAEgwRQIhAO/+3iXpm3JTGZEHX1+MLk0u+LlHxZoyEB5c1sPrpauOAiBLUUFtBazZc9WT0mCFhcpiebA2mgkcYm51YhXfCmdXgAFHMEQCICwhUWvNFWoe8/3l6+bYY2ykw/uP4nXPqhj/ezhf05VwAiAgki9SUuDNhUW8S8Hpdp/wpe15QblU6Dc9u57l/AvqKgFHUiECoc6ItT1YCk4vJbw0IMj9v6CnwIpJkElyfTKnDQVkaxwhA6rEhwHBr40FY/RUJsvNGXfJeF36SK6qbNfGhSqB4ILZUq7gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2wsh-multisig-3of3 full signature", + "message": "STI2ISAZ6RFHASY3BPIU2XAQAP", + "address": "bc1qfspfhvzzrqektdx76uj0kzds6h3lu8wdh8vk3z2g28m4zem7xl7s47l3ng", + "signature": "fulAgAAAAABAVgs1IXe15rh8SqO2jbTmgmtdP1gsS6/T/RxdRinI8xMAAAAAADgBwAAAQAAAAAAAAAAAWoFAEcwRAIgAyXERMyt400pIZIz3RGO4F2p4n6rsBrnARunlirgXW8CIAHMthgjnkYtDIsP4japYFfDafSPOCTgnD2BBzlqXfWXAUcwRAIgRZPUCX9TMbhAKYZWXk8S7o5K2zzG9eAG/1wZVv9rtZMCIBSmylJ6Y6f9tQnRsYaXmYiB9EuMFE+1laNLGRbvrd+XAUcwRAIgeE6VBIf47QmMNLr9rrD1YrUJt7wyetg+oHYJ/avg5QoCIHxh4zBqYBQxOoHbYmQglu6o3Ap3i5h4RzQylDfyzUASAWlTIQK7JRu8wgagfYM4/Ug7OFEjxHSHfIfjEEJhj2YmnegeiCEDY3O7yk5D0DnzgysEoMMGN2wYs7ZpbzNqUvLNu+phI/MhAuwUllpOEHH0N8jMQYRwjxFuejdXtnhhOSWSTdVxWlNpU67gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2wsh-multisig-3of3 full signature", + "message": "A7YVK3GGY4MMUA6C4F2L3ASEDW", + "address": "bc1qpcdtk4v5mxpmkjnckjz3aw74ntpg3km842ee0u2lpk4uaumzm0lsrm2n5x", + "signature": "fulAgAAAAABAVgs1IXe15rh8SqO2jbTmgmtdP1gsS6/T/RxdRinI8xMAAAAAADgBwAAAQAAAAAAAAAAAWoFAEcwRAIgAyXERMyt400pIZIz3RGO4F2p4n6rsBrnARunlirgXW8CIAHMthgjnkYtDIsP4japYFfDafSPOCTgnD2BBzlqXfWXAUcwRAIgRZPUCX9TMbhAKYZWXk8S7o5K2zzG9eAG/1wZVv9rtZMCIBSmylJ6Y6f9tQnRsYaXmYiB9EuMFE+1laNLGRbvrd+XAUcwRAIgeE6VBIf47QmMNLr9rrD1YrUJt7wyetg+oHYJ/avg5QoCIHxh4zBqYBQxOoHbYmQglu6o3Ap3i5h4RzQylDfyzUASAWlTIQK7JRu8wgagfYM4/Ug7OFEjxHSHfIfjEEJhj2YmnegeiCEDY3O7yk5D0DnzgysEoMMGN2wYs7ZpbzNqUvLNu+phI/MhAuwUllpOEHH0N8jMQYRwjxFuejdXtnhhOSWSTdVxWlNpU67gBwAA", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2sh-p2wsh-multisig-2of2 full signature", + "message": "35X5NMZTDTM56MJQAXDSE644M4", + "address": "3QN3gGr1WL5i4Z7njGLJDdosTf9VqukC9m", + "signature": "fulAgAAAAABAehf4I+wavWpjzx3T1sLa5ERLVWGKnKTl+j4U5mqHCzLAAAAACMiACC36LM1vvTTMwlLFLFTidJzSIX+OH19Zkx4aYdQ/D6W6uAHAAABAAAAAAAAAAABagQARzBEAiA1/E8KVNhFgVwXFFtvbQWHm7uttryotBvNZLnraDauOgIgFGQS7pBg4g7QRj4ju/s2IlTx4MvVIololRRBKUGXypYBSDBFAiEA7yxOM8miz0NSaezUc3gmcHVGRuVXcmP85aUxQt51bPECIAzUXn6gV9REeRpyt2fLFdh38LR/JqIlDFGMNCknEtiMAUdSIQKs1lIVs9aXZfsXItWlH4Z/v7Rm3Rp3UYLmk8YAPdl4oCECZIOEnG9YDE8eV2ANd89hc5SZBSu5blENClhxJ1hF159SruAHAAA=", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2sh-p2wsh-multisig-2of2 full signature", + "message": "4FMVP445Q7UDQA5PHEJAJPEBXT", + "address": "37TBNw9iW1MpXuPDsmt1xX2JZnJ8y1cQ3h", + "signature": "fulAgAAAAABAehf4I+wavWpjzx3T1sLa5ERLVWGKnKTl+j4U5mqHCzLAAAAACMiACC36LM1vvTTMwlLFLFTidJzSIX+OH19Zkx4aYdQ/D6W6uAHAAABAAAAAAAAAAABagQARzBEAiA1/E8KVNhFgVwXFFtvbQWHm7uttryotBvNZLnraDauOgIgFGQS7pBg4g7QRj4ju/s2IlTx4MvVIololRRBKUGXypYBSDBFAiEA7yxOM8miz0NSaezUc3gmcHVGRuVXcmP85aUxQt51bPECIAzUXn6gV9REeRpyt2fLFdh38LR/JqIlDFGMNCknEtiMAUdSIQKs1lIVs9aXZfsXItWlH4Z/v7Rm3Rp3UYLmk8YAPdl4oCECZIOEnG9YDE8eV2ANd89hc5SZBSu5blENClhxJ1hF159SruAHAAA=", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong message for p2sh-multisig-2of2 full signature", + "message": "Y7HFVADKFXUNXCZYPEETLZKG2T", + "address": "3EnD6QQdPwVNLzw7MYdkJN6m9refVLvuYN", + "signature": "fulAgAAAAENwuZZhE9piECSsE0ZpR8VUpJwdppH6aVrtpJoKEIfcAAAAADZAEcwRAIgGI69RNIL2JR0dw0raRB2t8mFJPwgU+7bVp/amPMo9t0CIH0/rzAfdfEiUXzhr/3nXdMJPLRGy3uc8k4/9ROVHGM5AUcwRAIgMDqFXDbvCRK9EOxw0vQ9thRDvXpMz/kBL0jnHsz58e0CID4OPnrsKRN5z80oCBKv9THGwjWzI5sScn25cMPrcAtlAUdSIQNmoQbyT39W+zRP0Rr6nsO2sh99TqTZ3XL2Gxglm+PnoCECmjX7zXfdntCWYKfhiC6xxyZzPZF/Nune1w8ewSi9PMBSruAHAAABAAAAAAAAAAABauAHAAA=", + "error_substr": "invalid to_sign transaction" + }, + { + "description": "wrong signer for p2sh-multisig-2of2 full signature", + "message": "2NXJGXTLGKREF37LY4MYDAPAAJ", + "address": "39uNuU8jJCkNizR3FNkSw21skqPkNCW4SQ", + "signature": "fulAgAAAAENwuZZhE9piECSsE0ZpR8VUpJwdppH6aVrtpJoKEIfcAAAAADZAEcwRAIgGI69RNIL2JR0dw0raRB2t8mFJPwgU+7bVp/amPMo9t0CIH0/rzAfdfEiUXzhr/3nXdMJPLRGy3uc8k4/9ROVHGM5AUcwRAIgMDqFXDbvCRK9EOxw0vQ9thRDvXpMz/kBL0jnHsz58e0CID4OPnrsKRN5z80oCBKv9THGwjWzI5sScn25cMPrcAtlAUdSIQNmoQbyT39W+zRP0Rr6nsO2sh99TqTZ3XL2Gxglm+PnoCECmjX7zXfdntCWYKfhiC6xxyZzPZF/Nune1w8ewSi9PMBSruAHAAABAAAAAAAAAAABauAHAAA=", + "error_substr": "invalid to_sign transaction" + } + ] +} \ No newline at end of file diff --git a/bip322/validation.go b/bip322/validation.go index c0601d0604..2aa205e374 100644 --- a/bip322/validation.go +++ b/bip322/validation.go @@ -3,10 +3,77 @@ package bip322 import ( "fmt" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/psbt/v2" "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" ) +// validateToSignTx checks that the to_sign transaction structure conforms to +// the BIP-322 verification process basic validation requirements: +// - to_sign has at least one input +// - to_sign's first input spends the output of to_spend (at index 0) +// - to_sign has exactly one output with value 0 and an OP_RETURN scriptPubKey +// +// The toSpendTxHash argument is the txid of the to_spend transaction +// reconstructed from the message and challenge script. +func validateToSignTx(tx *wire.MsgTx, toSpendTxHash chainhash.Hash) error { + if len(tx.TxIn) == 0 { + return fmt.Errorf("%w: to_sign must have at least one input", + ErrInvalidToSign) + } + + // The first input must spend the to_spend transaction's only output + // (at index 0). + firstPrev := tx.TxIn[0].PreviousOutPoint + if firstPrev.Hash != toSpendTxHash { + return fmt.Errorf("%w: to_sign first input prev hash %s does "+ + "not match to_spend txid %s", ErrInvalidToSign, + firstPrev.Hash, toSpendTxHash) + } + if firstPrev.Index != 0 { + return fmt.Errorf("%w: to_sign first input prev index must be "+ + "0, got %d", ErrInvalidToSign, firstPrev.Index) + } + + // to_sign must have exactly one output: value 0, scriptPubKey + // OP_RETURN. + if len(tx.TxOut) != 1 { + return fmt.Errorf("%w: to_sign must have exactly one output, "+ + "got %d", ErrInvalidToSign, len(tx.TxOut)) + } + out := tx.TxOut[0] + if out.Value != 0 { + return fmt.Errorf("%w: to_sign output value must be 0, got %d", + ErrInvalidToSign, out.Value) + } + if len(out.PkScript) != 1 || out.PkScript[0] != txscript.OP_RETURN { + return fmt.Errorf("%w: to_sign output scriptPubKey must be a "+ + "single OP_RETURN byte, got %x", ErrInvalidToSign, + out.PkScript) + } + + return nil +} + +// validateUpgradeableRules implements the BIP-322 "upgradeable rules" check: +// the to_sign tx version must be 0 or 2. Other versions cause an inconclusive +// result. +// +// The remaining upgradeable rules (no upgrade-reserved NOPs, no Segwit versions +// > 1) are enforced by the script engine via StandardVerifyFlags +// (ScriptDiscourageUpgradableNops, +// ScriptVerifyDiscourageUpgradeableWitnessProgram). +func validateUpgradeableRules(version int32) error { + if version != 0 && version != 2 { + return fmt.Errorf("%w: to_sign tx version must be 0 or 2, "+ + "got %d", ErrInconclusive, version) + } + + return nil +} + // validateUtxoCorrectness enforces the BIP-174 rule that an input spending a // non-segwit (legacy) output must not have a witness UTXO field // (PSBT_IN_WITNESS_UTXO) present. @@ -40,6 +107,79 @@ func validateUtxoCorrectness(packet *psbt.Packet) error { return nil } +// validateNoCodeSeparator enforces the BIP-322 required rule that forbids the +// use of OP_CODESEPARATOR. +// +// The script engine, even with StandardVerifyFlags, only rejects +// OP_CODESEPARATOR in non-segwit scripts (via ScriptVerifyConstScriptCode). It +// does NOT reject it inside a segwit witness script (P2WSH) or a taproot leaf +// script (P2TR script-path spend), so those scripts are inspected here. The +// non-segwit cases (legacy scriptSig, P2SH redeem script) are already handled +// by the script engine and don't need to be re-checked. +func validateNoCodeSeparator(pkScript []byte, witness wire.TxWitness, + sigScript []byte) error { + + scripts := revealedWitnessScripts(pkScript, sigScript, witness) + for _, script := range scripts { + hasSep, err := scriptHasCodeSeparator(script) + if err != nil { + // A malformed script will be rejected by the script + // engine during execution, so we don't fail here. + continue + } + if hasSep { + return ErrCodeSeparator + } + } + + return nil +} + +// revealedWitnessScripts returns the scripts that a segwit witness reveals and +// that the script engine does not scan for OP_CODESEPARATOR under +// StandardVerifyFlags: the witness script of a P2WSH spend and the leaf script +// of a P2TR script-path spend. Nested (P2SH-wrapped) segwit spends are resolved +// to their underlying witness program first. +func revealedWitnessScripts(pkScript, sigScript []byte, + witness wire.TxWitness) [][]byte { + + version, program, ok := inputWitnessProgram(pkScript, sigScript) + if !ok { + return nil + } + + items := stripAnnex(witness) + switch { + // P2WSH: a v0 program of 32 bytes. The witness script is the last + // witness item. + case version == 0 && len(program) == 32 && len(items) >= 1: + return [][]byte{items[len(items)-1]} + + // P2TR script-path spend: a v1 program of 32 bytes with at least two + // items (after annex removal). The leaf script is the second-to-last + // item (the last item being the control block). + case version == 1 && len(program) == 32 && len(items) >= 2: + return [][]byte{items[len(items)-2]} + } + + return nil +} + +// scriptHasCodeSeparator reports whether the passed script contains an +// OP_CODESEPARATOR opcode. It walks the whole script (including unexecuted +// branches), matching the script engine's behavior for non-segwit scripts. +func scriptHasCodeSeparator(script []byte) (bool, error) { + const scriptVersion = 0 + tokenizer := txscript.MakeScriptTokenizer(scriptVersion, script) + for tokenizer.Next() { + if tokenizer.Opcode() == txscript.OP_CODESEPARATOR { + return true, nil + } + } + + return false, tokenizer.Err() +} + // inputWitnessProgram returns the witness version and program that govern an // input's execution, resolving a nested P2SH redeem script if present. ok is // false when the input is not a (possibly nested) segwit spend. @@ -77,3 +217,58 @@ func redeemScript(sigScript []byte) ([]byte, error) { return pushes[len(pushes)-1], nil } + +// stripAnnex removes the taproot annex from a witness stack if present. Per +// BIP-341, if there are at least two items and the last one starts with the +// annex tag (0x50), it is the annex. +func stripAnnex(witness wire.TxWitness) wire.TxWitness { + if len(witness) >= 2 { + last := witness[len(witness)-1] + if len(last) > 0 && last[0] == txscript.TaprootAnnexTag { + return witness[:len(witness)-1] + } + } + + return witness +} + +// validateAmounts validates the previous output amount ranges against consensus +// rules. Each transaction output must not be negative or more than the max +// allowed per transaction. Also, the total of all outputs must abide by the +// same restrictions. This is a partial copy of +// blockchain.CheckTransactionSanity, which can't be used directly without +// depending on the whole btcd project. +func validateAmounts(outputs []*wire.TxOut) error { + var totalSatoshi int64 + for _, txOut := range outputs { + satoshi := txOut.Value + if satoshi < 0 { + return fmt.Errorf("transaction output has negative "+ + "value of %v", satoshi) + } + if satoshi > btcutil.MaxSatoshi { + return fmt.Errorf("transaction output value is "+ + "higher than max allowed value: %v > %v ", + satoshi, btcutil.MaxSatoshi) + } + + // Two's complement int64 overflow guarantees that any overflow + // is detected and reported. This is impossible for Bitcoin, + // but perhaps possible if an alt increases the total money + // supply. + totalSatoshi += satoshi + if totalSatoshi < 0 { + return fmt.Errorf("total value of all transaction "+ + "outputs exceeds max allowed value of %v", + btcutil.MaxSatoshi) + } + if totalSatoshi > btcutil.MaxSatoshi { + return fmt.Errorf("total value of all transaction "+ + "outputs is %v which is higher than max "+ + "allowed value of %v", totalSatoshi, + btcutil.MaxSatoshi) + } + } + + return nil +} diff --git a/bip322/validation_test.go b/bip322/validation_test.go new file mode 100644 index 0000000000..49d3a12018 --- /dev/null +++ b/bip322/validation_test.go @@ -0,0 +1,936 @@ +package bip322 + +import ( + "crypto/sha256" + "errors" + "testing" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/stretchr/testify/require" +) + +// codeSeparatorScript builds the minimal `OP_CODESEPARATOR OP_TRUE` script used +// by the OP_CODESEPARATOR rejection probes. +func codeSeparatorScript(t *testing.T) []byte { + t.Helper() + + script, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_CODESEPARATOR). + AddOp(txscript.OP_TRUE). + Script() + require.NoError(t, err) + + return script +} + +// witnessScriptChallenge return the pkScript for a p2wsh challenge. +func witnessScriptChallenge(t *testing.T, witnessScript []byte, + versionOP byte) []byte { + + scriptHash := sha256.Sum256(witnessScript) + pkScript, err := txscript.NewScriptBuilder(). + AddOp(versionOP). + AddData(scriptHash[:]). + Script() + require.NoError(t, err) + + return pkScript +} + +// opTrueChallenge creates an OP_TRUE pkScript challenge and witness stack for +// spending it. +func opTrueChallenge(t *testing.T) ([]byte, wire.TxWitness, []byte) { + t.Helper() + + witnessScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_TRUE). + Script() + require.NoError(t, err) + + pkScript := witnessScriptChallenge(t, witnessScript, txscript.OP_0) + witness := wire.TxWitness{witnessScript} + witnessBytes, err := SerializeTxWitness(witness) + require.NoError(t, err) + + return pkScript, witness, witnessBytes +} + +func makeHtlcScript(t *testing.T, paymentHash [32]byte, taproot bool) ([]byte, + *btcec.PrivateKey) { + + receiverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + refundKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + receiverKeyBytes := receiverKey.PubKey().SerializeCompressed() + refundKeyBytes := refundKey.PubKey().SerializeCompressed() + if taproot { + receiverKeyBytes = schnorr.SerializePubKey(receiverKey.PubKey()) + refundKeyBytes = schnorr.SerializePubKey(refundKey.PubKey()) + } + + htlcScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_IF). + AddOp(txscript.OP_SHA256). + AddData(paymentHash[:]). + AddOp(txscript.OP_EQUALVERIFY). + AddData(receiverKeyBytes). + AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_ELSE). + AddInt64(500). + AddOp(txscript.OP_CHECKLOCKTIMEVERIFY). + AddOp(txscript.OP_DROP). + AddData(refundKeyBytes). + AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_ENDIF). + Script() + require.NoError(t, err) + + return htlcScript, receiverKey +} + +func taprootWitness(t *testing.T, script []byte) ([]byte, []byte) { + internalKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + leaf := txscript.NewBaseTapLeaf(script) + tree := txscript.AssembleTaprootScriptTree(leaf) + rootHash := tree.RootNode.TapHash() + outputKey := txscript.ComputeTaprootOutputKey( + internalKey.PubKey(), rootHash[:], + ) + pkScript, err := txscript.PayToTaprootScript(outputKey) + require.NoError(t, err) + + controlBlock := tree.LeafMerkleProofs[0].ToControlBlock( + internalKey.PubKey(), + ) + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + return pkScript, controlBlockBytes +} + +func taprootHtlcWitness(t *testing.T, packet *psbt.Packet, htlcScript, preimage, + controlBlockBytes []byte, sigHashType txscript.SigHashType, + receiverKey *btcec.PrivateKey) wire.TxWitness { + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + receiverSig, err := txscript.RawTxInTapscriptSignature( + finalTx, sigHashes, 0, utxo.Value, utxo.PkScript, + txscript.NewBaseTapLeaf(htlcScript), sigHashType, receiverKey, + ) + require.NoError(t, err) + + return wire.TxWitness{ + receiverSig, + preimage, + []byte{1}, // Select the preimage (hashlock) branch. + htlcScript, + controlBlockBytes, + } +} + +// TestVerifyMessageSimpleRejectsCodeSeparator asserts that the BIP-322 required +// rule forbidding OP_CODESEPARATOR is enforced for segwit witness scripts and +// taproot leaf scripts. The script engine, even with StandardVerifyFlags, only +// rejects OP_CODESEPARATOR in non-segwit scripts, so without the dedicated +// inspection these probes would (incorrectly) verify as valid. +func TestVerifyMessageSimpleRejectsCodeSeparator(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + // P2WSH witness script: OP_CODESEPARATOR OP_TRUE. + t.Run("p2wsh", func(t *testing.T) { + script := codeSeparatorScript(t) + + pkScript := witnessScriptChallenge(t, script, txscript.OP_0) + witness := wire.TxWitness{script} + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrCodeSeparator) + require.False(t, valid) + }) + + // P2TR tapscript: OP_CODESEPARATOR OP_TRUE, revealed via a script-path + // spend. + t.Run("p2tr", func(t *testing.T) { + leafScript := codeSeparatorScript(t) + pkScript, controlBlockBytes := taprootWitness(t, leafScript) + + witness := wire.TxWitness{leafScript, controlBlockBytes} + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrCodeSeparator) + require.False(t, valid) + }) +} + +// TestVerifyMessageSimpleAcceptsTaprootScriptPathData asserts that a valid P2TR +// script-path spend is not rejected just because one of its (non-signature) +// stack inputs happens to look like a Schnorr signature with an explicit +// sighash byte. +// +// The probe uses an HTLC-style tapscript whose hashlock branch consumes a +// 65-byte preimage whose final byte is SigHashNone, so it structurally +// resembles a 65-byte Schnorr signature carrying a disallowed sighash flag. +// Because the restricted-sighash policy is enforced by the script engine, only +// signatures actually consumed by a signature opcode are checked; the preimage +// is consumed by OP_SHA256, not OP_CHECKSIG, so it is never subject to the +// rule. The real signature in the witness is a valid 64-byte SIGHASH_DEFAULT +// Schnorr signature, so BIP-322 gives no reason to reject the message. +func TestVerifyMessageSimpleAcceptsTaprootScriptPathData(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + // Build a 65-byte preimage whose trailing byte is a (disallowed) + // explicit sighash flag, so it structurally resembles a 65-byte Schnorr + // signature. + preimage := make([]byte, 65) + for i := 0; i < len(preimage)-1; i++ { + preimage[i] = byte(i + 1) + } + preimage[len(preimage)-1] = byte(txscript.SigHashNone) + paymentHash := sha256.Sum256(preimage) + + htlcScript, receiverKey := makeHtlcScript(t, paymentHash, true) + pkScript, controlBlockBytes := taprootWitness(t, htlcScript) + + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + witness := taprootHtlcWitness( + t, packet, htlcScript, preimage, controlBlockBytes, + txscript.SigHashDefault, receiverKey, + ) + finalTx.TxIn[0].Witness = witness + + // Sanity check: the witness is valid under raw consensus rules. + sigHashes = txscript.NewTxSigHashes(finalTx, prevOutFetcher) + vm, err := txscript.NewEngine( + utxo.PkScript, finalTx, 0, txscript.StandardVerifyFlags, nil, + sigHashes, utxo.Value, prevOutFetcher, + ) + require.NoError(t, err) + require.NoError(t, vm.Execute()) + + // BIP-322 verification must reach the same conclusion: valid. + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.NoError(t, err) + require.True(t, valid) +} + +// TestVerifyTaprootKeyPathRejectsBadSigHash asserts that the restricted-sighash +// policy is enforced for a taproot key-spend: a P2TR key-path spend whose lone +// witness item is a 65-byte Schnorr signature with an explicit, disallowed +// sighash flag must be rejected by the script engine. +func TestVerifyTaprootKeyPathRejectsBadSigHash(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + internalKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + outputKey := txscript.ComputeTaprootKeyNoScript(internalKey.PubKey()) + pkScript, err := txscript.PayToTaprootScript(outputKey) + require.NoError(t, err) + + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + // Sign the key-path spend with an explicit, non-default sighash flag. + // This produces a 65-byte signature that BIP-322 does not allow. + witness, err := txscript.TaprootWitnessSignature( + finalTx, sigHashes, 0, utxo.Value, utxo.PkScript, + txscript.SigHashSingle, internalKey, + ) + require.NoError(t, err) + require.Len(t, witness[0], 65) + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrInvalidSigHashFlag) + require.False(t, valid) +} + +// TestVerifyMessageSimpleAcceptsP2WSHScriptData is the ECDSA/segwit-v0 +// analog of TestVerifyMessageSimpleAcceptsTaprootScriptPathData: a valid +// P2WSH script-path spend must not be rejected just because one of its +// (non-signature) stack inputs happens to look like a DER-encoded ECDSA +// signature with a disallowed sighash byte. +// +// The probe uses an HTLC-style witness script whose hashlock branch consumes a +// 32-byte preimage crafted to resemble a DER-encoded ECDSA signature: it starts +// with 0x30, its second byte equals len-3, and its final byte is SigHashNone. +// Because the restricted-sighash policy is enforced by the script engine, the +// preimage is never checked (it is consumed by OP_SHA256, not OP_CHECKSIG). The +// real signature in the witness uses SIGHASH_ALL, so BIP-322 gives no reason to +// reject the message. +func TestVerifyMessageSimpleAcceptsP2WSHScriptData(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + // Build a 32-byte preimage shaped like a DER ECDSA signature whose + // trailing byte is a (disallowed) explicit sighash flag. + preimage := make([]byte, 32) + preimage[0] = 0x30 + preimage[1] = byte(len(preimage) - 3) + preimage[len(preimage)-1] = byte(txscript.SigHashNone) + paymentHash := sha256.Sum256(preimage) + + htlcScript, receiverKey := makeHtlcScript(t, paymentHash, false) + pkScript := witnessScriptChallenge(t, htlcScript, txscript.OP_0) + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + receiverSigWithSighashAll, err := txscript.RawTxInWitnessSignature( + finalTx, sigHashes, 0, utxo.Value, htlcScript, + txscript.SigHashAll, receiverKey, + ) + require.NoError(t, err) + + witness := wire.TxWitness{ + receiverSigWithSighashAll, + preimage, + []byte{1}, // Select the preimage (hashlock) branch. + htlcScript, + } + finalTx.TxIn[0].Witness = witness + + // Sanity check: the witness is valid under raw consensus rules. + sigHashes = txscript.NewTxSigHashes(finalTx, prevOutFetcher) + vm, err := txscript.NewEngine( + utxo.PkScript, finalTx, 0, txscript.StandardVerifyFlags, nil, + sigHashes, utxo.Value, prevOutFetcher, + ) + require.NoError(t, err) + require.NoError(t, vm.Execute()) + + // BIP-322 verification must reach the same conclusion: valid. + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.NoError(t, err) + require.True(t, valid) +} + +// TestVerifyP2WPKHRejectsBadSigHash is the ECDSA analog of +// TestVerifyTaprootKeyPathRejectsBadSigHash: a P2WPKH spend whose signature +// uses a disallowed (non-SIGHASH_ALL) sighash flag must be rejected by the +// script engine. +func TestVerifyP2WPKHRejectsBadSigHash(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + pubKeyHash := address.Hash160(key.PubKey().SerializeCompressed()) + pkScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_0). + AddData(pubKeyHash). + Script() + require.NoError(t, err) + + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + // Sign with a disallowed (non-SIGHASH_ALL) sighash flag. + sig, err := txscript.RawTxInWitnessSignature( + finalTx, sigHashes, 0, utxo.Value, utxo.PkScript, + txscript.SigHashNone, key, + ) + require.NoError(t, err) + + witness := wire.TxWitness{sig, key.PubKey().SerializeCompressed()} + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrInvalidSigHashFlag) + require.False(t, valid) +} + +// TestVerifyTaprootScriptPathRejectsBadSigHash exercises the capability that a +// static witness inspection cannot provide: rejecting a disallowed sighash flag +// on a signature that is consumed by a CHECKSIG inside a custom tapscript. The +// signature is a real, otherwise-valid SIGHASH_SINGLE Schnorr signature on the +// hashlock branch of an HTLC; only the engine, which observes the actual +// signature opcode, can soundly reject it. +func TestVerifyTaprootScriptPathRejectsBadSigHash(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + preimage := make([]byte, 32) + for i := range preimage { + preimage[i] = byte(i + 1) + } + paymentHash := sha256.Sum256(preimage) + + htlcScript, receiverKey := makeHtlcScript(t, paymentHash, true) + pkScript, controlBlockBytes := taprootWitness(t, htlcScript) + + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + // Sign the hashlock branch with a disallowed sighash flag. This is a + // genuine, cryptographically valid signature; only its sighash type is + // disallowed by BIP-322. + witness := taprootHtlcWitness( + t, packet, htlcScript, preimage, controlBlockBytes, + txscript.SigHashSingle, receiverKey, + ) + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrInvalidSigHashFlag) + require.False(t, valid) +} + +// TestVerifyP2WSHMultisigRejectsBadSigHash asserts that the sighash rule is +// enforced for every signature consumed by an OP_CHECKMULTISIG: a 2-of-2 P2WSH +// multisig whose second signature uses a disallowed sighash flag must be +// rejected, even though the first signature uses SIGHASH_ALL. +func TestVerifyP2WSHMultisigRejectsBadSigHash(t *testing.T) { + t.Parallel() + + message := []byte("probe") + + key1, err := btcec.NewPrivateKey() + require.NoError(t, err) + key2, err := btcec.NewPrivateKey() + require.NoError(t, err) + + witnessScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_2). + AddData(key1.PubKey().SerializeCompressed()). + AddData(key2.PubKey().SerializeCompressed()). + AddOp(txscript.OP_2). + AddOp(txscript.OP_CHECKMULTISIG). + Script() + require.NoError(t, err) + + pkScript := witnessScriptChallenge(t, witnessScript, txscript.OP_0) + packet, err := BuildToSignPacketSimple(message, pkScript) + require.NoError(t, err) + + finalTx := packet.UnsignedTx.Copy() + utxo := packet.Inputs[0].WitnessUtxo + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + utxo.PkScript, utxo.Value, + ) + sigHashes := txscript.NewTxSigHashes(finalTx, prevOutFetcher) + + // The first signer uses SIGHASH_ALL (allowed); the second uses + // SIGHASH_SINGLE (disallowed). The engine inspects every signature + // consumed by the multisig, so the message must be rejected. + sig1, err := txscript.RawTxInWitnessSignature( + finalTx, sigHashes, 0, utxo.Value, witnessScript, + txscript.SigHashAll, key1, + ) + require.NoError(t, err) + sig2, err := txscript.RawTxInWitnessSignature( + finalTx, sigHashes, 0, utxo.Value, witnessScript, + txscript.SigHashSingle, key2, + ) + require.NoError(t, err) + + // The leading empty item is the well-known OP_CHECKMULTISIG dummy. + witness := wire.TxWitness{nil, sig1, sig2, witnessScript} + + valid, _, err := VerifyMessageSimple(message, pkScript, witness) + require.ErrorIs(t, err, ErrInvalidSigHashFlag) + require.False(t, valid) +} + +// TestVerifyMessageSimpleReservedNOPIsInconclusive tests that using a reserved +// OP_NOPx returns the ErrInconclusive error. +func TestVerifyMessageSimpleReservedNOPIsInconclusive(t *testing.T) { + t.Parallel() + + witnessScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_NOP5). + AddOp(txscript.OP_TRUE). + Script() + require.NoError(t, err) + + pkScript := witnessScriptChallenge(t, witnessScript, txscript.OP_0) + valid, _, err := VerifyMessageSimple( + []byte("probe"), pkScript, wire.TxWitness{witnessScript}, + ) + require.False(t, valid) + require.ErrorIs(t, err, ErrInconclusive) + require.False(t, errors.Is(err, ErrInvalidSignature)) +} + +// TestVerifyMessageFullFutureWitnessVersionIsInconclusive tests that future +// witness versions are returned as inconclusive. +func TestVerifyMessageFullFutureWitnessVersionIsInconclusive(t *testing.T) { + t.Parallel() + + witnessScript, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_TRUE). + Script() + require.NoError(t, err) + + pkScript := witnessScriptChallenge(t, witnessScript, txscript.OP_2) + valid, _, err := VerifyMessageFull( + []byte("probe"), pkScript, nil, wire.TxWitness{witnessScript}, + 0, 0, 0, + ) + require.False(t, valid) + require.ErrorIs(t, err, ErrInconclusive) + require.False(t, errors.Is(err, ErrInvalidSignature)) +} + +// TestVerifyMessageTxVersionIsInconclusive tests that future transaction +// versions (or the version 1) are not allowed and are validated as +// inconclusive. +func TestVerifyMessageTxVersionIsInconclusive(t *testing.T) { + t.Parallel() + + const ( + txVersion1 = 1 + txVersion3 = 3 + ) + + pkScript, witness, witnessBytes := opTrueChallenge(t) + valid, _, err := VerifyMessageFull( + []byte("probe"), pkScript, nil, witness, txVersion1, 0, 0, + ) + require.ErrorIs(t, err, ErrInconclusive) + require.False(t, valid) + + packet, err := BuildToSignPacketFull( + []byte("probe"), pkScript, txVersion3, 0, 0, + ) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + valid, _, err = VerifyMessagePoF([]byte("probe"), pkScript, packet) + require.ErrorIs(t, err, ErrInconclusive) + require.False(t, valid) +} + +// TestValidateUtxoCorrectness tests that validateUtxoCorrectness enforces the +// BIP-174 rule that an input spending a non-segwit output must convey it via +// a non-witness UTXO. Only a bare witness UTXO on a non-segwit input is +// rejected; segwit inputs (native or P2SH-nested) and inputs that also carry a +// non-witness UTXO are accepted. +func TestValidateUtxoCorrectness(t *testing.T) { + t.Parallel() + + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Build one output script of each relevant type. + p2wpkh, err := payToWitnessPubKeyHashScript(key) + require.NoError(t, err) + + p2wsh := witnessScriptChallenge( + t, []byte{txscript.OP_TRUE}, txscript.OP_0, + ) + + p2tr, err := payToTaprootScript(key) + require.NoError(t, err) + + p2pkh, err := payToPubKeyHashScript(key) + require.NoError(t, err) + + // A bare pay-to-pubkey output, another legacy (non-segwit) type. + p2pk, err := txscript.NewScriptBuilder(). + AddData(key.PubKey().SerializeCompressed()). + AddOp(txscript.OP_CHECKSIG).Script() + require.NoError(t, err) + + // A nested P2SH-P2WPKH output plus the finalized scriptSig that reveals + // its witness redeem script (a single push of the witness program). + nestedP2sh, witProg, err := payToNestedWitnessPubKeyHashScript(key) + require.NoError(t, err) + nestedSig, err := txscript.NewScriptBuilder(). + AddData(witProg).Script() + require.NoError(t, err) + + // A P2SH output that wraps a *non*-witness redeem script, plus the + // scriptSig revealing it. This is a legacy P2SH spend, not segwit. + legacyRedeem := []byte{txscript.OP_TRUE} + p2shLegacy, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_HASH160). + AddData(address.Hash160(legacyRedeem)). + AddOp(txscript.OP_EQUAL). + Script() + require.NoError(t, err) + p2shLegacySig, err := txscript.NewScriptBuilder(). + AddData(legacyRedeem). + Script() + require.NoError(t, err) + + // txOut builds a UTXO output for the given script. + txOut := func(pkScript []byte) *wire.TxOut { + return &wire.TxOut{Value: 1000, PkScript: pkScript} + } + + // prevTx builds a stand-in previous transaction with a single output of + // the given script, for use as a non-witness UTXO. + prevTx := func(pkScript []byte) *wire.MsgTx { + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + tx.AddTxOut(txOut(pkScript)) + return tx + } + + testCases := []struct { + name string + inputs []psbt.PInput + expectErr string + }{{ + name: "native p2wpkh witness utxo", + inputs: []psbt.PInput{{WitnessUtxo: txOut(p2wpkh)}}, + }, { + name: "native p2wsh witness utxo", + inputs: []psbt.PInput{{WitnessUtxo: txOut(p2wsh)}}, + }, { + name: "native p2tr witness utxo", + inputs: []psbt.PInput{{WitnessUtxo: txOut(p2tr)}}, + }, { + name: "nested p2sh-p2wpkh witness utxo", + inputs: []psbt.PInput{{ + WitnessUtxo: txOut(nestedP2sh), + FinalScriptSig: nestedSig, + }}, + }, { + name: "legacy p2pkh non-witness utxo only", + inputs: []psbt.PInput{{NonWitnessUtxo: prevTx(p2pkh)}}, + }, { + name: "segwit p2wpkh non-witness utxo only", + inputs: []psbt.PInput{{NonWitnessUtxo: prevTx(p2wpkh)}}, + }, { + name: "legacy p2pkh with both utxo fields", + inputs: []psbt.PInput{{ + WitnessUtxo: txOut(p2pkh), + NonWitnessUtxo: prevTx(p2pkh), + }}, + expectErr: "input 0", + }, { + name: "neither utxo field set", + inputs: []psbt.PInput{{}}, + }, { + name: "legacy p2pkh witness utxo only", + inputs: []psbt.PInput{{WitnessUtxo: txOut(p2pkh)}}, + expectErr: "input 0", + }, { + name: "bare p2pk witness utxo only", + inputs: []psbt.PInput{{WitnessUtxo: txOut(p2pk)}}, + expectErr: "input 0", + }, { + name: "legacy p2sh witness utxo only", + inputs: []psbt.PInput{{ + WitnessUtxo: txOut(p2shLegacy), + FinalScriptSig: p2shLegacySig, + }}, + expectErr: "input 0", + }, { + name: "nested p2sh witness utxo without redeem", + inputs: []psbt.PInput{{WitnessUtxo: txOut(nestedP2sh)}}, + expectErr: "input 0", + }, { + name: "valid segwit followed by bare legacy", + inputs: []psbt.PInput{ + {WitnessUtxo: txOut(p2wpkh)}, + {WitnessUtxo: txOut(p2pkh)}, + }, + expectErr: "input 1", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + packet := &psbt.Packet{Inputs: tc.inputs} + err := validateUtxoCorrectness(packet) + if tc.expectErr != "" { + require.ErrorContains(t, err, tc.expectErr) + require.ErrorContains( + t, err, "non-segwit script", + ) + + return + } + + require.NoError(t, err) + }) + } +} + +// TestVerifyMessagePoFWitnessUtxoHandling tests that the (non-) witness UTXO +// fields are verified correctly in a proof-of-funds transaction. +func TestVerifyMessagePoFWitnessUtxoHandling(t *testing.T) { + message := []byte("probe") + + // Use a native segwit OP_TRUE challenge for the first BIP-322 input so + // the test stays focused on the additional proof-of-funds input. + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + // Add a legacy P2PKH PoF input. For this input type, the PSBT must + // carry a NonWitnessUtxo (or find it via the BIP-322 same-tx + // optimization), not just a bare WitnessUtxo. + legacyKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + pubKey := legacyKey.PubKey().SerializeCompressed() + pubKeyHash := address.Hash160(pubKey) + legacyAddr, err := address.NewAddressPubKeyHash( + pubKeyHash, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + legacyPkScript, err := txscript.PayToAddrScript(legacyAddr) + require.NoError(t, err) + + legacyPrevHash := chainhash.HashH([]byte("legacy prevout")) + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: legacyPrevHash, + Index: 0, + }, + }) + + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: &wire.TxOut{ + Value: 1000, + PkScript: legacyPkScript, + }, + }) + require.Nil(t, packet.Inputs[1].NonWitnessUtxo) + + legacySigScript, err := txscript.SignatureScript( + packet.UnsignedTx, 1, legacyPkScript, txscript.SigHashAll, + legacyKey, true, + ) + require.NoError(t, err) + packet.Inputs[1].FinalScriptSig = legacySigScript + + // A legacy PoF input that carries only a witness UTXO is rejected up + // front by validateUtxoCorrectness during serialization: a non-segwit + // input must supply a non-witness UTXO, so no signature is produced. + signature, err := SerializeSignature(packet) + require.ErrorContains( + t, err, "witness UTXO for spending a non-segwit script", + ) + require.Empty(t, signature) + + // Additional P2WSH OP_TRUE input claims claimedHash:0, but supplies a + // different NonWitnessUtxo. The current verifier verifies against + // prevTx.TxOut[0] anyway. + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{}) + prevTx.AddTxOut(&wire.TxOut{Value: 1, PkScript: pkScript}) + + var claimedHash chainhash.Hash + claimedHash[0] = 0x42 + require.NotEqual(t, prevTx.TxHash(), claimedHash) + + // Update the second (PoF) input to a new one. + packet.UnsignedTx.TxIn[1].PreviousOutPoint = wire.OutPoint{ + Hash: claimedHash, + Index: 0, + } + packet.Inputs[1] = psbt.PInput{ + NonWitnessUtxo: prevTx, + FinalScriptWitness: witnessBytes, + } + + signature, err = SerializeSignature(packet) + require.NoError(t, err) + valid, _, err := verifyMessageForChallenge(message, pkScript, signature) + require.ErrorContains( + t, err, "non witness utxo does not match input prevout", + ) + require.False(t, valid) +} + +// TestVerifyMessagePoFRejectsDuplicateAdditionalInputs tests that a +// proof-of-funds transaction rejects duplicate additional inputs. +func TestVerifyMessagePoFRejectsDuplicateAdditionalInputs(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + prevTx := wire.NewMsgTx(2) + prevTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Index: 0xffffffff}, + }) + prevTx.AddTxOut(&wire.TxOut{ + Value: 1, + PkScript: pkScript, + }) + + duplicatePrevOut := wire.OutPoint{ + Hash: prevTx.TxHash(), + Index: 0, + } + for i := 0; i < 2; i++ { + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: duplicatePrevOut, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: &wire.TxOut{ + Value: 1, + PkScript: pkScript, + }, + FinalScriptWitness: witnessBytes, + }) + } + + signature, err := SerializeSignature(packet) + require.NoError(t, err) + + valid, _, err := verifyMessageForChallenge( + message, pkScript, signature, + ) + + require.Error(t, err) + require.False(t, valid) +} + +// TestVerifyMessagePoFRejectsNullAdditionalInput tests that a proof-of-funds +// transaction rejects null outpoint additional inputs. +func TestVerifyMessagePoFRejectsNullAdditionalInput(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{Index: 0xffffffff}, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: &wire.TxOut{ + Value: 1, + PkScript: pkScript, + }, + FinalScriptWitness: witnessBytes, + }) + + signature, err := SerializeSignature(packet) + require.NoError(t, err) + + valid, _, err := verifyMessageForChallenge( + message, pkScript, signature, + ) + + require.Error(t, err) + require.False(t, valid) +} + +// TestVerifyMessagePoFRejectsOversizeTransaction makes sure too large +// transactions (more than 1 MB of stripped size) are rejected. +func TestVerifyMessagePoFRejectsOversizeTransaction(t *testing.T) { + message := []byte("probe") + + pkScriptOpTrue, _, witnessBytesOpTrue := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScriptOpTrue, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytesOpTrue + + const numInputs = 30_000 + for i := 1; i < numInputs; i++ { + var h chainhash.Hash + h[0] = byte(i) + h[1] = byte(i >> 8) + h[2] = byte(i >> 16) + h[3] = byte(i >> 24) + packet.UnsignedTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: h, + Index: 0, + }, + }) + packet.Inputs = append(packet.Inputs, psbt.PInput{ + WitnessUtxo: &wire.TxOut{ + Value: 1, + PkScript: pkScriptOpTrue, + }, + FinalScriptWitness: witnessBytesOpTrue, + }) + } + + require.Greater(t, packet.UnsignedTx.SerializeSizeStripped(), numInputs) + + valid, _, err := VerifyMessagePoF(message, pkScriptOpTrue, packet) + require.ErrorIs(t, err, ErrInvalidToSign) + require.False(t, valid) +} + +// TestProbeVerifyMessagePoFDirectRejectsMalformedToSignOutput tests that the +// VerifyMessagePoF function properly rejects malformed transactions. +func TestProbeVerifyMessagePoFDirectRejectsMalformedToSignOutput(t *testing.T) { + message := []byte("probe") + pkScript, _, witnessBytes := opTrueChallenge(t) + + packet, err := BuildToSignPacketFull(message, pkScript, 0, 0, 0) + require.NoError(t, err) + + packet.Inputs[0].FinalScriptWitness = witnessBytes + packet.UnsignedTx.TxOut[0].PkScript = []byte{txscript.OP_TRUE} + + valid, _, err := VerifyMessagePoF(message, pkScript, packet) + + require.ErrorIs(t, err, ErrInvalidToSign) + require.False(t, valid) +} From 479af04dd0a8cb6035d1ad9af3274fdc5b4e4c06 Mon Sep 17 00:00:00 2001 From: Oli Date: Fri, 12 Jun 2026 11:35:31 +0200 Subject: [PATCH 7/7] make: include bip322 and other packages in tests --- Makefile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 0ea4a21de3..e79ae0e931 100644 --- a/Makefile +++ b/Makefile @@ -97,12 +97,14 @@ unit: @$(call print, "Running unit tests.") $(GOTEST_DEV) ./... -test.timeout=20m cd address && $(GOTEST_DEV) ./... -test.timeout=20m + cd bip322 && $(GOTEST_DEV) ./... -test.timeout=20m cd btcec && $(GOTEST_DEV) ./... -test.timeout=20m cd btcutil && $(GOTEST_DEV) ./... -test.timeout=20m cd chaincfg && $(GOTEST_DEV) ./... -test.timeout=20m cd chainhash && $(GOTEST_DEV) ./... -test.timeout=20m - cd txscript && $(GOTEST_DEV) ./... -test.timeout=20m cd psbt && $(GOTEST_DEV) ./... -test.timeout=20m + cd txscript && $(GOTEST_DEV) ./... -test.timeout=20m + cd v2transport && $(GOTEST_DEV) ./... -test.timeout=20m cd wire && $(GOTEST_DEV) ./... -test.timeout=20m #? unit-cover: Run unit coverage tests @@ -113,12 +115,14 @@ unit-cover: # We need to remove the /v2 pathing from the module to have it work # nicely with the CI tool we use to render live code coverage. cd address && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt + cd bip322 && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd btcec && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd btcutil && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd chaincfg && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd chainhash && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt - cd txscript && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd psbt && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt + cd txscript && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt + cd v2transport && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt cd wire && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt #? unit-race: Run unit race tests @@ -126,12 +130,14 @@ unit-race: @$(call print, "Running unit race tests.") env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd address && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... + cd bip322 && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd btcec && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd btcutil && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd chaincfg && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd chainhash && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... - cd txscript && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd psbt && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... + cd txscript && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... + cd v2transport && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... cd wire && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./... # =========