diff --git a/api/firmware/btc.go b/api/firmware/btc.go index 07cbe94..71c45f8 100644 --- a/api/firmware/btc.go +++ b/api/firmware/btc.go @@ -18,6 +18,7 @@ import ( "encoding/binary" "errors" "fmt" + "slices" "strings" "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" @@ -372,6 +373,14 @@ func (device *Device) nonAtomicBTCSign( } } + if !device.version.AtLeast(semver.NewSemVer(9, 24, 0)) { + if slices.ContainsFunc(tx.Outputs, func(output *messages.BTCSignOutputRequest) bool { + return output.Type == messages.BTCOutputType_OP_RETURN + }) { + return nil, UnsupportedError("9.24.0") + } + } + supportsAntiklepto := device.version.AtLeast(semver.NewSemVer(9, 4, 0)) containsSilentPaymentOutputs := false diff --git a/api/firmware/messages/btc.pb.go b/api/firmware/messages/btc.pb.go index fe32e77..311e39d 100644 --- a/api/firmware/messages/btc.pb.go +++ b/api/firmware/messages/btc.pb.go @@ -95,12 +95,13 @@ func (BTCCoin) EnumDescriptor() ([]byte, []int) { type BTCOutputType int32 const ( - BTCOutputType_UNKNOWN BTCOutputType = 0 - BTCOutputType_P2PKH BTCOutputType = 1 - BTCOutputType_P2SH BTCOutputType = 2 - BTCOutputType_P2WPKH BTCOutputType = 3 - BTCOutputType_P2WSH BTCOutputType = 4 - BTCOutputType_P2TR BTCOutputType = 5 + BTCOutputType_UNKNOWN BTCOutputType = 0 + BTCOutputType_P2PKH BTCOutputType = 1 + BTCOutputType_P2SH BTCOutputType = 2 + BTCOutputType_P2WPKH BTCOutputType = 3 + BTCOutputType_P2WSH BTCOutputType = 4 + BTCOutputType_P2TR BTCOutputType = 5 + BTCOutputType_OP_RETURN BTCOutputType = 6 ) // Enum value maps for BTCOutputType. @@ -112,14 +113,16 @@ var ( 3: "P2WPKH", 4: "P2WSH", 5: "P2TR", + 6: "OP_RETURN", } BTCOutputType_value = map[string]int32{ - "UNKNOWN": 0, - "P2PKH": 1, - "P2SH": 2, - "P2WPKH": 3, - "P2WSH": 4, - "P2TR": 5, + "UNKNOWN": 0, + "P2PKH": 1, + "P2SH": 2, + "P2WPKH": 3, + "P2WSH": 4, + "P2TR": 5, + "OP_RETURN": 6, } ) @@ -2710,7 +2713,7 @@ const file_btc_proto_rawDesc = "" + "\x04TBTC\x10\x01\x12\a\n" + "\x03LTC\x10\x02\x12\b\n" + "\x04TLTC\x10\x03\x12\b\n" + - "\x04RBTC\x10\x04*R\n" + + "\x04RBTC\x10\x04*a\n" + "\rBTCOutputType\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05P2PKH\x10\x01\x12\b\n" + @@ -2718,7 +2721,8 @@ const file_btc_proto_rawDesc = "" + "\n" + "\x06P2WPKH\x10\x03\x12\t\n" + "\x05P2WSH\x10\x04\x12\b\n" + - "\x04P2TR\x10\x05b\x06proto3" + "\x04P2TR\x10\x05\x12\r\n" + + "\tOP_RETURN\x10\x06b\x06proto3" var ( file_btc_proto_rawDescOnce sync.Once diff --git a/api/firmware/messages/btc.proto b/api/firmware/messages/btc.proto index 24669ff..9793542 100644 --- a/api/firmware/messages/btc.proto +++ b/api/firmware/messages/btc.proto @@ -175,6 +175,7 @@ enum BTCOutputType { P2WPKH = 3; P2WSH = 4; P2TR = 5; + OP_RETURN = 6; } message BTCSignOutputRequest { diff --git a/api/firmware/psbt.go b/api/firmware/psbt.go index ebaccd1..c8e3819 100644 --- a/api/firmware/psbt.go +++ b/api/firmware/psbt.go @@ -224,6 +224,21 @@ func payloadFromPkScript(pkScript []byte) (messages.BTCOutputType, []byte, error outputType = messages.BTCOutputType_P2TR payload = pkScript[2:] + case len(pkScript) > 0 && pkScript[0] == txscript.OP_RETURN: + outputType = messages.BTCOutputType_OP_RETURN + + tokenizer := txscript.MakeScriptTokenizer(0, pkScript[1:]) + if !tokenizer.Next() { + return 0, nil, errp.New("naked OP_RETURN is not supported") + } + payload = tokenizer.Data() + // OP_0 is an empty data push + if payload == nil && tokenizer.Opcode() != txscript.OP_0 { + return 0, nil, errp.New("no data push found after OP_RETURN") + } + if !tokenizer.Done() { + return 0, nil, errp.New("only one data push supported after OP_RETURN") + } default: return 0, nil, errp.New("unrecognized output type") } diff --git a/api/firmware/psbt_test.go b/api/firmware/psbt_test.go index ba6d733..50f2ee1 100644 --- a/api/firmware/psbt_test.go +++ b/api/firmware/psbt_test.go @@ -84,6 +84,7 @@ func TestPayloadFromPkScript(t *testing.T) { tests := []struct { name string address string + pkScriptHex string // Used for OP_RETURN instead of address expectedType messages.BTCOutputType expectedPayload string }{ @@ -117,18 +118,41 @@ func TestPayloadFromPkScript(t *testing.T) { expectedPayload: "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c", expectedType: messages.BTCOutputType_P2TR, }, + { + name: "OP_RETURN empty", + pkScriptHex: "6a00", + expectedType: messages.BTCOutputType_OP_RETURN, + expectedPayload: "", + }, + { + name: "OP_RETURN 3 bytes", + pkScriptHex: "6a03aabbcc", + expectedType: messages.BTCOutputType_OP_RETURN, + expectedPayload: "aabbcc", + }, + { + name: "OP_RETURN 80 bytes (OP_PUSHDATA1)", + pkScriptHex: "6a4c50aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + expectedType: messages.BTCOutputType_OP_RETURN, + expectedPayload: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - addr, err := btcutil.DecodeAddress(tt.address, &chaincfg.MainNetParams) - require.NoError(t, err) + var pkScript []byte + var err error - pkScript, err := txscript.PayToAddrScript(addr) - require.NoError(t, err) + if tt.pkScriptHex != "" { + pkScript = unhex(tt.pkScriptHex) + } else { + addr, err := btcutil.DecodeAddress(tt.address, &chaincfg.MainNetParams) + require.NoError(t, err) + pkScript, err = txscript.PayToAddrScript(addr) + require.NoError(t, err) + } outputType, payload, err := payloadFromPkScript(pkScript) - require.NoError(t, err) assert.Equal(t, tt.expectedType, outputType) assert.Equal(t, tt.expectedPayload, hex.EncodeToString(payload))