diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e00c3c6..93fd7acbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately. - simulator: simulate a Nova device - Add API call to fetch multiple xpubs at once - Add the option for the simulator to write its memory to file. +- Bitcoin: add support for OP_RETURN outputs ### 9.23.1 - EVM: add HyperEVM (HYPE) and SONIC (S) to known networks diff --git a/messages/btc.proto b/messages/btc.proto index 24669fff7..979354253 100644 --- a/messages/btc.proto +++ b/messages/btc.proto @@ -175,6 +175,7 @@ enum BTCOutputType { P2WPKH = 3; P2WSH = 4; P2TR = 5; + OP_RETURN = 6; } message BTCSignOutputRequest { diff --git a/py/bitbox02/CHANGELOG.md b/py/bitbox02/CHANGELOG.md index 7ad876a07..e277e2e41 100644 --- a/py/bitbox02/CHANGELOG.md +++ b/py/bitbox02/CHANGELOG.md @@ -4,6 +4,7 @@ # 7.1.0 - Add `btc_xpubs()` to fetch multiple xpubs at once +- Bitcoin: add support for OP_RETURN outputs # 7.0.0 - get_info: add optional device initialized boolean to returned tuple diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py index 753cde511..5a0c20abe 100644 --- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py +++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py @@ -467,6 +467,16 @@ def btc_sign( # Attaching output info supported since v9.22.0. self._require_atleast(semver.VersionInfo(9, 22, 0)) + if any( + map( + lambda output: isinstance(output, BTCOutputExternal) + and output.type == btc.BTCOutputType.OP_RETURN, + outputs, + ) + ): + # OP_RETURN supported sice v9.24.0 + self._require_atleast(semver.VersionInfo(9, 24, 0)) + supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0) sigs: List[Tuple[int, bytes]] = [] diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py index 45148fdd3..251ae667b 100644 --- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py +++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py @@ -15,7 +15,7 @@ from . import antiklepto_pb2 as antiklepto__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"\xdf\x01\n\x0f\x42TCXpubsRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x41\n\txpub_type\x18\x02 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCXpubsRequest.XPubType\x12/\n\x08keypaths\x18\x03 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"+\n\x08XPubType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04TPUB\x10\x02\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xab\x02\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a{\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\tB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb9\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x12\x36\n\x05xpubs\x18\t \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCXpubsRequestH\x00\x42\t\n\x07request\"\xc4\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12\x32\n\x04pubs\x18\x06 \x01(\x0b\x32\".shiftcrypto.bitbox02.PubsResponseH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*R\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"\xdf\x01\n\x0f\x42TCXpubsRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x41\n\txpub_type\x18\x02 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCXpubsRequest.XPubType\x12/\n\x08keypaths\x18\x03 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"+\n\x08XPubType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04TPUB\x10\x02\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xab\x02\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a{\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\tB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb9\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x12\x36\n\x05xpubs\x18\t \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCXpubsRequestH\x00\x42\t\n\x07request\"\xc4\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12\x32\n\x04pubs\x18\x06 \x01(\x0b\x32\".shiftcrypto.bitbox02.PubsResponseH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*a\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x12\r\n\tOP_RETURN\x10\x06\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc_pb2', globals()) @@ -25,7 +25,7 @@ _BTCCOIN._serialized_start=5551 _BTCCOIN._serialized_end=5608 _BTCOUTPUTTYPE._serialized_start=5610 - _BTCOUTPUTTYPE._serialized_end=5692 + _BTCOUTPUTTYPE._serialized_end=5707 _BTCSCRIPTCONFIG._serialized_start=68 _BTCSCRIPTCONFIG._serialized_end=650 _BTCSCRIPTCONFIG_MULTISIG._serialized_start=293 diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi index 05022bdd1..c7f89ac2d 100644 --- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi +++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi @@ -70,6 +70,7 @@ class _BTCOutputTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._ P2WPKH: _BTCOutputType.ValueType # 3 P2WSH: _BTCOutputType.ValueType # 4 P2TR: _BTCOutputType.ValueType # 5 + OP_RETURN: _BTCOutputType.ValueType # 6 class BTCOutputType(_BTCOutputType, metaclass=_BTCOutputTypeEnumTypeWrapper): ... @@ -79,6 +80,7 @@ P2SH: BTCOutputType.ValueType # 2 P2WPKH: BTCOutputType.ValueType # 3 P2WSH: BTCOutputType.ValueType # 4 P2TR: BTCOutputType.ValueType # 5 +OP_RETURN: BTCOutputType.ValueType # 6 global___BTCOutputType = BTCOutputType @typing.final diff --git a/py/send_message.py b/py/send_message.py index a611c2726..dfb74ae43 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -788,6 +788,43 @@ def _sign_btc_policy(self) -> None: for input_index, sig in sigs: print("Signature for input {}: {}".format(input_index, sig.hex())) + def _sign_btc_op_return( + self, + format_unit: "bitbox02.btc.BTCSignInitRequest.FormatUnit.V" = bitbox02.btc.BTCSignInitRequest.FormatUnit.DEFAULT, + ) -> None: + # pylint: disable=no-member + bip44_account: int = 0 + HARDENED + inputs, outputs = _btc_demo_inputs_outputs(bip44_account) + outputs.append( + bitbox02.BTCOutputExternal( + output_type=bitbox02.btc.OP_RETURN, + output_payload=b"hello world", + value=0, + ) + ) + sigs = self._device.btc_sign( + bitbox02.btc.BTC, + [ + bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH + ), + keypath=[84 + HARDENED, 0 + HARDENED, bip44_account], + ), + bitbox02.btc.BTCScriptConfigWithKeypath( + script_config=bitbox02.btc.BTCScriptConfig( + simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH + ), + keypath=[49 + HARDENED, 0 + HARDENED, bip44_account], + ), + ], + inputs=inputs, + outputs=outputs, + format_unit=format_unit, + ) + for input_index, sig in sigs: + print("Signature for input {}: {}".format(input_index, sig.hex())) + def _sign_btc_tx_from_raw(self) -> None: """ Experiment with testnet transactions. @@ -896,6 +933,7 @@ def _sign_btc_tx(self) -> None: ("Taproot inputs", self._sign_btc_taproot_inputs), ("Taproot output", self._sign_btc_taproot_output), ("Policy", self._sign_btc_policy), + ("OP_RETURN", self._sign_btc_op_return), ("From testnet tx ID", self._sign_btc_tx_from_raw), ) choice = ask_user(choices) diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs index 0eaac7109..86a48e158 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Shift Crypto AG +// Copyright 2022-2025 Shift Crypto AG // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,10 +25,11 @@ pub use pb::btc_sign_init_request::FormatUnit; pub use pb::{BtcCoin, BtcOutputType}; use super::script_configs::{ValidatedScriptConfig, ValidatedScriptConfigWithKeypath}; -use super::{multisig, params::Params, script}; +use super::{multisig, params::Params}; use sha2::{Digest, Sha256}; +use bitcoin::ScriptBuf; use bitcoin::bech32; use bitcoin::hashes::Hash; @@ -210,7 +211,8 @@ impl Payload { } } - /// Converts a payload to an address. + /// Converts a payload to an address. Returns an error for invalid input or if an address does + /// not exist for the output type (e.g. OP_RETURN). pub fn address(&self, params: &Params) -> Result { let payload = self.data.as_slice(); match self.output_type { @@ -251,54 +253,51 @@ impl Payload { } encode_segwit_addr(params.bech32_hrp, 1, payload) } + BtcOutputType::OpReturn => Err(()), } } - /// Computes the pkScript from a pubkey hash or script hash or pubkey, depending on the output type. + /// Computes the pkScript from a pubkey hash or script hash or pubkey, depending on the output + /// type. pub fn pk_script(&self, params: &Params) -> Result, Error> { let payload = self.data.as_slice(); - match self.output_type { - BtcOutputType::Unknown => Err(Error::InvalidInput), + let script = match self.output_type { + BtcOutputType::Unknown => return Err(Error::InvalidInput), BtcOutputType::P2pkh => { - if payload.len() != HASH160_LEN { - return Err(Error::Generic); - } - let mut result = vec![script::OP_DUP, script::OP_HASH160]; - script::push_data(&mut result, payload); - result.extend_from_slice(&[script::OP_EQUALVERIFY, script::OP_CHECKSIG]); - Ok(result) + let pk_hash = + bitcoin::PubkeyHash::from_slice(payload).map_err(|_| Error::Generic)?; + + ScriptBuf::new_p2pkh(&pk_hash) } BtcOutputType::P2sh => { - if payload.len() != HASH160_LEN { - return Err(Error::Generic); - } - let mut result = vec![script::OP_HASH160]; - script::push_data(&mut result, payload); - result.push(script::OP_EQUAL); - Ok(result) + let script_hash = + bitcoin::ScriptHash::from_slice(payload).map_err(|_| Error::Generic)?; + ScriptBuf::new_p2sh(&script_hash) } - BtcOutputType::P2wpkh | BtcOutputType::P2wsh => { - if (self.output_type == BtcOutputType::P2wpkh && payload.len() != HASH160_LEN) - || (self.output_type == BtcOutputType::P2wsh && payload.len() != SHA256_LEN) - { - return Err(Error::Generic); - } - let mut result = vec![script::OP_0]; - script::push_data(&mut result, payload); - Ok(result) + BtcOutputType::P2wpkh => { + let wpkh = bitcoin::WPubkeyHash::from_slice(payload).map_err(|_| Error::Generic)?; + ScriptBuf::new_p2wpkh(&wpkh) + } + BtcOutputType::P2wsh => { + let wsh = bitcoin::WScriptHash::from_slice(payload).map_err(|_| Error::Generic)?; + ScriptBuf::new_p2wsh(&wsh) } BtcOutputType::P2tr => { if !params.taproot_support { return Err(Error::InvalidInput); } - if payload.len() != 32 { - return Err(Error::Generic); - } - let mut result = vec![script::OP_1]; - script::push_data(&mut result, payload); - Ok(result) + let tweaked = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked( + bitcoin::XOnlyPublicKey::from_slice(payload).map_err(|_| Error::Generic)?, + ); + ScriptBuf::new_p2tr_tweaked(tweaked) } - } + BtcOutputType::OpReturn => { + let pushbytes: &bitcoin::script::PushBytes = + payload.try_into().map_err(|_| Error::InvalidInput)?; + ScriptBuf::new_op_return(pushbytes) + } + }; + Ok(script.into_bytes()) } } @@ -622,4 +621,86 @@ mod tests { b"\x25\x0e\xc8\x02\xb6\xd3\xdb\x98\x42\xd1\xbd\xbe\x0e\xe4\x8d\x52\xf9\xa4\xb4\x6e\x60\xcb\xbb\xab\x3b\xcc\x4e\xe9\x15\x73\xfc\xe8" ); } + + #[test] + fn test_pkscript() { + let params = super::super::params::get(pb::BtcCoin::Btc); + + let payload = Payload { + data: vec![], + output_type: BtcOutputType::Unknown, + }; + assert_eq!(payload.pk_script(params), Err(Error::InvalidInput)); + + struct Test { + payload: &'static str, + output_type: BtcOutputType, + expected_pkscript: &'static str, + } + + let tests = [ + Test { + payload: "669c6cb1883c50a1b10c34bd1693c1f34fe3d798", + output_type: BtcOutputType::P2pkh, + expected_pkscript: "76a914669c6cb1883c50a1b10c34bd1693c1f34fe3d79888ac", + }, + Test { + payload: "b59e844a19063a882b3c34b64b941a8acdad74ee", + output_type: BtcOutputType::P2sh, + expected_pkscript: "a914b59e844a19063a882b3c34b64b941a8acdad74ee87", + }, + Test { + payload: "b7cfb87a9806bb232e64f64e714785bd8366596b", + output_type: BtcOutputType::P2wpkh, + expected_pkscript: "0014b7cfb87a9806bb232e64f64e714785bd8366596b", + }, + Test { + payload: "526e8e589b4bf1de80774986d972aed96ae70f17572d35fe89e61e9e88e2dd4a", + output_type: BtcOutputType::P2wsh, + expected_pkscript: "0020526e8e589b4bf1de80774986d972aed96ae70f17572d35fe89e61e9e88e2dd4a", + }, + Test { + payload: "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c", + output_type: BtcOutputType::P2tr, + expected_pkscript: "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c", + }, + Test { + payload: "aabbcc", + output_type: BtcOutputType::OpReturn, + expected_pkscript: "6a03aabbcc", + }, + Test { + payload: "", + output_type: BtcOutputType::OpReturn, + expected_pkscript: "6a00", + }, + Test { + // 80 byte payload + payload: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + output_type: BtcOutputType::OpReturn, + expected_pkscript: "6a4c50aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ]; + + for test in tests { + // OK + let payload = Payload { + data: hex::decode(test.payload).unwrap(), + output_type: test.output_type, + }; + assert_eq!( + hex::encode(payload.pk_script(params).unwrap()), + test.expected_pkscript, + ); + + // Payload of wrong size. Does not apply to OpReturn, almost any size is accepted. + if test.output_type != BtcOutputType::OpReturn { + let payload = Payload { + data: hex::decode(&test.payload[2..]).unwrap(), + output_type: test.output_type, + }; + assert_eq!(payload.pk_script(params), Err(Error::Generic)); + } + } + } } diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/script.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/script.rs index d66c3f082..dbcb66af4 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/script.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/script.rs @@ -14,15 +14,6 @@ use alloc::vec::Vec; -// https://en.bitcoin.it/wiki/Script -pub const OP_0: u8 = 0; -pub const OP_1: u8 = 0x51; -pub const OP_HASH160: u8 = 0xa9; -pub const OP_DUP: u8 = 0x76; -pub const OP_EQUALVERIFY: u8 = 0x88; -pub const OP_CHECKSIG: u8 = 0xac; -pub const OP_EQUAL: u8 = 0x87; - /// Serialize a number in the VarInt encoding. /// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer pub fn serialize_varint(value: u64) -> Vec { @@ -45,12 +36,6 @@ pub fn serialize_varint(value: u64) -> Vec { out } -/// Performs a data push onto `v`: the varint length of data followed by data. -pub fn push_data(v: &mut Vec, data: &[u8]) { - v.extend_from_slice(&serialize_varint(data.len() as _)); - v.extend_from_slice(data); -} - #[cfg(test)] mod tests { use super::*; @@ -111,26 +96,4 @@ mod tests { b"\xff\xff\xff\xff\xff\xff\xff\xff\xff" ); } - - #[test] - fn test_push_data() { - assert_eq!( - { - let mut v = Vec::new(); - push_data(&mut v, b""); - v - }, - vec![0] - ); - - // Data with length 255. - assert_eq!( - { - let mut v = Vec::new(); - push_data(&mut v, b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); - v - }, - b"\xfd\xff\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_vec(), - ); - } } diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs index c7f8d37ae..11dc37613 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Shift Crypto AG +// Copyright 2022-2025 Shift Crypto AG // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -853,12 +853,18 @@ async fn _process( }; } - if tx_output.value == 0 { + let output_type = pb::BtcOutputType::try_from(tx_output.r#type)?; + + // We don't allow regular outputs to have 0 value. + // OP_RETURN outputs however we require to have 0 value. + if output_type == pb::BtcOutputType::OpReturn { + if tx_output.value != 0 { + return Err(Error::InvalidInput); + } + } else if tx_output.value == 0 { return Err(Error::InvalidInput); } - let output_type = pb::BtcOutputType::try_from(tx_output.r#type)?; - // Get payload. If the output is marked ours, we compute the payload from the keystore, // otherwise it is provided in tx_output.payload. let payload: common::Payload = if tx_output.ours { @@ -949,16 +955,21 @@ async fn _process( if !is_change { // Verify output if it is not a change output. // Assemble address to display, get user confirmation. - let address = if let Some(sp) = tx_output.silent_payment.as_ref() { - sp.address.clone() - } else { - payload.address(coin_params)? + let address = || -> Result { + if let Some(sp) = tx_output.silent_payment.as_ref() { + Ok(sp.address.clone()) + } else { + Ok(payload.address(coin_params)?) + } }; if let Some(output_payment_request_index) = tx_output.payment_request_index { if output_payment_request_index != 0 { return Err(Error::InvalidInput); } + if output_type == pb::BtcOutputType::OpReturn { + return Err(Error::InvalidInput); + } if payment_request_seen { return Err(Error::InvalidInput); } @@ -970,7 +981,7 @@ async fn _process( coin_params, &payment_request, tx_output.value, - &address, + &address()?, ) .is_err() { @@ -979,6 +990,16 @@ async fn _process( } payment_request_seen = true; + } else if output_type == pb::BtcOutputType::OpReturn { + // OP_RETURN value was validated to be 0 above, so we don't need to show the amount. + crate::workflow::verify_message::verify( + hal, + "OP_RETURN", + "OP_RETURN", + &tx_output.payload, + false, + ) + .await?; } else { // When sending coins back to the same account (non-change), or another account of // the same keystore (change or non-change), we show a prefix to let the user know. @@ -1001,9 +1022,9 @@ async fn _process( hal.ui() .verify_recipient( &(if let Some(prefix) = prefix { - format!("{}: {}", prefix, address) + format!("{}: {}", prefix, address()?) } else { - address + address()? }), &format_amount(coin_params, format_unit, tx_output.value)?, ) @@ -3683,4 +3704,102 @@ mod tests { ] ); } + + #[test] + fn test_op_return() { + let transaction = + alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc))); + + // Attach OP_RETURN output + { + let mut tx = transaction.borrow_mut(); + tx.outputs.push(pb::BtcSignOutputRequest { + r#type: pb::BtcOutputType::OpReturn as _, + value: 0, + payload: b"hello world".to_vec(), + ..Default::default() + }); + } + + mock_host_responder(transaction.clone()); + mock_unlocked(); + bitbox02::random::fake_reset(); + let init_request = transaction.borrow().init_request(); + + let mut mock_hal = TestingHal::new(); + let result = block_on(process(&mut mock_hal, &init_request)); + + match result { + Ok(Response::BtcSignNext(next)) => { + assert!(next.has_signature); + assert_eq!( + hex::encode(next.signature), + "f49c71b89ec3510ebebae9aff9f967ad9bb6cc0c4cddbdf851f97e47e9922646622459e522b0751fa246e49a8e48417344a5384a9f68c1c85cd03804b35e1e1e", + ); + } + _ => panic!("wrong result"), + } + + assert!(mock_hal.ui.contains_confirm("OP_RETURN", "hello world")); + } + + #[test] + fn test_op_return_nonascii() { + let transaction = + alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc))); + + // Attach OP_RETURN output + { + let mut tx = transaction.borrow_mut(); + tx.outputs.push(pb::BtcSignOutputRequest { + r#type: pb::BtcOutputType::OpReturn as _, + value: 0, + payload: vec![1, 2, 3, 4, 5], + ..Default::default() + }); + } + + mock_host_responder(transaction.clone()); + mock_unlocked(); + bitbox02::random::fake_reset(); + let init_request = transaction.borrow().init_request(); + + let mut mock_hal = TestingHal::new(); + let result = block_on(process(&mut mock_hal, &init_request)); + assert!(result.is_ok()); + + assert!( + mock_hal + .ui + .contains_confirm("OP_RETURN\ndata (hex)", "0102030405") + ); + } + + #[test] + fn test_op_return_fail_nonzero_value() { + let transaction = + alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc))); + + // Attach OP_RETURN output + { + let mut tx = transaction.borrow_mut(); + tx.outputs.push(pb::BtcSignOutputRequest { + r#type: pb::BtcOutputType::OpReturn as _, + value: 100, + payload: b"hello world".to_vec(), + ..Default::default() + }); + } + + mock_host_responder(transaction.clone()); + mock_unlocked(); + bitbox02::random::fake_reset(); + let init_request = transaction.borrow().init_request(); + + let mut mock_hal = TestingHal::new(); + assert_eq!( + block_on(process(&mut mock_hal, &init_request)), + Err(Error::InvalidInput) + ); + } } diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs index 018adf959..3a5f532c2 100644 --- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs +++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs @@ -1060,6 +1060,7 @@ pub enum BtcOutputType { P2wpkh = 3, P2wsh = 4, P2tr = 5, + OpReturn = 6, } impl BtcOutputType { /// String value of the enum field names used in the ProtoBuf definition. @@ -1074,6 +1075,7 @@ impl BtcOutputType { BtcOutputType::P2wpkh => "P2WPKH", BtcOutputType::P2wsh => "P2WSH", BtcOutputType::P2tr => "P2TR", + BtcOutputType::OpReturn => "OP_RETURN", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1085,6 +1087,7 @@ impl BtcOutputType { "P2WPKH" => Some(Self::P2wpkh), "P2WSH" => Some(Self::P2wsh), "P2TR" => Some(Self::P2tr), + "OP_RETURN" => Some(Self::OpReturn), _ => None, } }