diff --git a/common/protob/messages-bitcoin.proto b/common/protob/messages-bitcoin.proto index 5c4b639df..df6eb2793 100644 --- a/common/protob/messages-bitcoin.proto +++ b/common/protob/messages-bitcoin.proto @@ -197,6 +197,7 @@ message SignTx { optional uint32 branch_id = 10; // only for Zcash, BRANCH_ID optional AmountUnit amount_unit = 11 [default=BITCOIN]; // show amounts in optional bool decred_staking_ticket = 12 [default=false]; // only for Decred, this is signing a ticket purchase + optional bool serialize = 13 [default=true]; // serialize the full transaction, as opposed to only outputting the signatures } /** diff --git a/core/.changelog.d/2507.added b/core/.changelog.d/2507.added new file mode 100644 index 000000000..6df5e9765 --- /dev/null +++ b/core/.changelog.d/2507.added @@ -0,0 +1 @@ +Add serialize option to SignTx. diff --git a/core/src/apps/bitcoin/sign_tx/bitcoin.py b/core/src/apps/bitcoin/sign_tx/bitcoin.py index 805e98466..2abfac4d2 100644 --- a/core/src/apps/bitcoin/sign_tx/bitcoin.py +++ b/core/src/apps/bitcoin/sign_tx/bitcoin.py @@ -111,6 +111,7 @@ class Bitcoin: # transaction and signature serialization _SERIALIZED_TX_BUFFER[:] = bytes() self.serialized_tx = _SERIALIZED_TX_BUFFER + self.serialize = tx.serialize self.tx_req = TxRequest() self.tx_req.details = TxRequestDetailsType() self.tx_req.serialized = TxRequestSerializedType() @@ -247,18 +248,25 @@ class Bitcoin: await self.verify_original_txs() async def step4_serialize_inputs(self) -> None: - self.write_tx_header(self.serialized_tx, self.tx_info.tx, bool(self.segwit)) - write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) + if self.serialize: + self.write_tx_header(self.serialized_tx, self.tx_info.tx, bool(self.segwit)) + write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) + for i in range(self.tx_info.tx.inputs_count): progress.advance() if i in self.external: - await self.serialize_external_input(i) + if self.serialize: + await self.serialize_external_input(i) elif i in self.segwit: - await self.serialize_segwit_input(i) + if self.serialize: + await self.serialize_segwit_input(i) else: await self.sign_nonsegwit_input(i) async def step5_serialize_outputs(self) -> None: + if not self.serialize: + return + write_compact_size(self.serialized_tx, self.tx_info.tx.outputs_count) for i in range(self.tx_info.tx.outputs_count): progress.advance() @@ -273,16 +281,19 @@ class Bitcoin: progress.advance() if i in self.segwit: if i in self.external: - txi = await helpers.request_tx_input(self.tx_req, i, self.coin) - self.serialized_tx.extend(txi.witness or b"\0") + if self.serialize: + txi = await helpers.request_tx_input(self.tx_req, i, self.coin) + self.serialized_tx.extend(txi.witness or b"\0") else: await self.sign_segwit_input(i) else: # add empty witness for non-segwit inputs - self.serialized_tx.append(0) + if self.serialize: + self.serialized_tx.append(0) async def step7_finish(self) -> None: - self.write_tx_footer(self.serialized_tx, self.tx_info.tx) + if self.serialize: + self.write_tx_footer(self.serialized_tx, self.tx_info.tx) await helpers.request_tx_finish(self.tx_req) async def process_internal_input(self, txi: TxInput) -> None: @@ -596,31 +607,32 @@ class Bitcoin: if txi.script_type == InputScriptType.SPENDTAPROOT: signature = self.sign_taproot_input(i, txi) - scripts.write_witness_p2tr( - self.serialized_tx, signature, self.get_sighash_type(txi) - ) + if self.serialize: + scripts.write_witness_p2tr( + self.serialized_tx, signature, self.get_sighash_type(txi) + ) else: public_key, signature = self.sign_bip143_input(i, txi) - - if txi.multisig: - # find out place of our signature based on the pubkey - signature_index = multisig.multisig_pubkey_index( - txi.multisig, public_key - ) - scripts.write_witness_multisig( - self.serialized_tx, - txi.multisig, - signature, - signature_index, - self.get_sighash_type(txi), - ) - else: - scripts.write_witness_p2wpkh( - self.serialized_tx, - signature, - public_key, - self.get_sighash_type(txi), - ) + if self.serialize: + if txi.multisig: + # find out place of our signature based on the pubkey + signature_index = multisig.multisig_pubkey_index( + txi.multisig, public_key + ) + scripts.write_witness_multisig( + self.serialized_tx, + txi.multisig, + signature, + signature_index, + self.get_sighash_type(txi), + ) + else: + scripts.write_witness_p2wpkh( + self.serialized_tx, + signature, + public_key, + self.get_sighash_type(txi), + ) self.set_serialized_signature(i, signature) @@ -707,10 +719,11 @@ class Bitcoin: # compute the signature from the tx digest signature = ecdsa_sign(node, tx_digest) - # serialize input with correct signature - self.write_tx_input_derived( - self.serialized_tx, txi, node.public_key(), signature - ) + if self.serialize: + # serialize input with correct signature + self.write_tx_input_derived( + self.serialized_tx, txi, node.public_key(), signature + ) self.set_serialized_signature(i, signature) async def serialize_output(self, i: int) -> None: diff --git a/core/src/apps/bitcoin/sign_tx/bitcoinlike.py b/core/src/apps/bitcoin/sign_tx/bitcoinlike.py index ed8b7f27b..dfa347301 100644 --- a/core/src/apps/bitcoin/sign_tx/bitcoinlike.py +++ b/core/src/apps/bitcoin/sign_tx/bitcoinlike.py @@ -30,7 +30,8 @@ class Bitcoinlike(Bitcoin): multisig.multisig_pubkey_index(txi.multisig, public_key) # serialize input with correct signature - self.write_tx_input_derived(self.serialized_tx, txi, public_key, signature) + if self.serialize: + self.write_tx_input_derived(self.serialized_tx, txi, public_key, signature) self.set_serialized_signature(i_sign, signature) async def sign_nonsegwit_input(self, i_sign: int) -> None: diff --git a/core/src/apps/bitcoin/sign_tx/decred.py b/core/src/apps/bitcoin/sign_tx/decred.py index 25e87b029..f9718f218 100644 --- a/core/src/apps/bitcoin/sign_tx/decred.py +++ b/core/src/apps/bitcoin/sign_tx/decred.py @@ -149,8 +149,11 @@ class Decred(Bitcoin): approver = DecredApprover(tx, coin) super().__init__(tx, keychain, coin, approver) - self.write_tx_header(self.serialized_tx, self.tx_info.tx, witness_marker=True) - write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) + if self.serialize: + self.write_tx_header( + self.serialized_tx, self.tx_info.tx, witness_marker=True + ) + write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) writers.write_uint32( self.h_prefix, self.tx_info.tx.version | DECRED_SERIALIZE_NO_WITNESS @@ -164,22 +167,25 @@ class Decred(Bitcoin): return DecredSigHasher(self.h_prefix) async def step2_approve_outputs(self) -> None: - write_compact_size(self.serialized_tx, self.tx_info.tx.outputs_count) write_compact_size(self.h_prefix, self.tx_info.tx.outputs_count) + if self.serialize: + write_compact_size(self.serialized_tx, self.tx_info.tx.outputs_count) if self.tx_info.tx.decred_staking_ticket: await self.approve_staking_ticket() else: await super().step2_approve_outputs() - self.write_tx_footer(self.serialized_tx, self.tx_info.tx) self.write_tx_footer(self.h_prefix, self.tx_info.tx) + if self.serialize: + self.write_tx_footer(self.serialized_tx, self.tx_info.tx) async def process_internal_input(self, txi: TxInput) -> None: await super().process_internal_input(txi) # Decred serializes inputs early. - self.write_tx_input(self.serialized_tx, txi, bytes()) + if self.serialize: + self.write_tx_input(self.serialized_tx, txi, bytes()) async def process_external_input(self, txi: TxInput) -> None: raise wire.DataError("External inputs not supported") @@ -194,10 +200,12 @@ class Decred(Bitcoin): orig_txo: TxOutput | None, ) -> None: await super().approve_output(txo, script_pubkey, orig_txo) - self.write_tx_output(self.serialized_tx, txo, script_pubkey) + if self.serialize: + self.write_tx_output(self.serialized_tx, txo, script_pubkey) async def step4_serialize_inputs(self) -> None: - write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) + if self.serialize: + write_compact_size(self.serialized_tx, self.tx_info.tx.inputs_count) prefix_hash = self.h_prefix.get_digest() @@ -259,10 +267,11 @@ class Decred(Bitcoin): signature = ecdsa_sign(key_sign, sig_hash) # serialize input with correct signature - self.write_tx_input_witness( - self.serialized_tx, txi_sign, key_sign_pub, signature - ) self.set_serialized_signature(i_sign, signature) + if self.serialize: + self.write_tx_input_witness( + self.serialized_tx, txi_sign, key_sign_pub, signature + ) async def step5_serialize_outputs(self) -> None: pass @@ -325,7 +334,8 @@ class Decred(Bitcoin): script_pubkey = scripts_decred.output_script_sstxsubmissionpkh(txo.address) await self.approver.add_decred_sstx_submission(txo, script_pubkey) self.tx_info.add_output(txo, script_pubkey) - self.write_tx_output(self.serialized_tx, txo, script_pubkey) + if self.serialize: + self.write_tx_output(self.serialized_tx, txo, script_pubkey) # SSTX commitment txo = await helpers.request_tx_output(self.tx_req, 1, self.coin) @@ -334,7 +344,8 @@ class Decred(Bitcoin): script_pubkey = self.process_sstx_commitment_owned(txo) self.approver.add_change_output(txo, script_pubkey) self.tx_info.add_output(txo, script_pubkey) - self.write_tx_output(self.serialized_tx, txo, script_pubkey) + if self.serialize: + self.write_tx_output(self.serialized_tx, txo, script_pubkey) # SSTX change txo = await helpers.request_tx_output(self.tx_req, 2, self.coin) @@ -350,7 +361,8 @@ class Decred(Bitcoin): raise wire.DataError("Only zeroed addresses accepted for sstx change.") self.approver.add_change_output(txo, script_pubkey) self.tx_info.add_output(txo, script_pubkey) - self.write_tx_output(self.serialized_tx, txo, script_pubkey) + if self.serialize: + self.write_tx_output(self.serialized_tx, txo, script_pubkey) def write_tx_header( self, diff --git a/core/src/apps/bitcoin/sign_tx/zcash_v4.py b/core/src/apps/bitcoin/sign_tx/zcash_v4.py index d92cd7aa8..79c759b44 100644 --- a/core/src/apps/bitcoin/sign_tx/zcash_v4.py +++ b/core/src/apps/bitcoin/sign_tx/zcash_v4.py @@ -139,12 +139,13 @@ class ZcashV4(Bitcoinlike): return Zip243SigHasher() async def step7_finish(self) -> None: - self.write_tx_footer(self.serialized_tx, self.tx_info.tx) + if self.serialize: + self.write_tx_footer(self.serialized_tx, self.tx_info.tx) - write_uint64(self.serialized_tx, 0) # valueBalance - write_compact_size(self.serialized_tx, 0) # nShieldedSpend - write_compact_size(self.serialized_tx, 0) # nShieldedOutput - write_compact_size(self.serialized_tx, 0) # nJoinSplit + write_uint64(self.serialized_tx, 0) # valueBalance + write_compact_size(self.serialized_tx, 0) # nShieldedSpend + write_compact_size(self.serialized_tx, 0) # nShieldedOutput + write_compact_size(self.serialized_tx, 0) # nJoinSplit await helpers.request_tx_finish(self.tx_req) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 484c17c09..ddc0700f6 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -594,6 +594,7 @@ if TYPE_CHECKING: branch_id: "int | None" amount_unit: "AmountUnit" decred_staking_ticket: "bool" + serialize: "bool" def __init__( self, @@ -609,6 +610,7 @@ if TYPE_CHECKING: branch_id: "int | None" = None, amount_unit: "AmountUnit | None" = None, decred_staking_ticket: "bool | None" = None, + serialize: "bool | None" = None, ) -> None: pass diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 1c3cd854d..2c08eb50d 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -1172,6 +1172,7 @@ class SignTx(protobuf.MessageType): 10: protobuf.Field("branch_id", "uint32", repeated=False, required=False), 11: protobuf.Field("amount_unit", "AmountUnit", repeated=False, required=False), 12: protobuf.Field("decred_staking_ticket", "bool", repeated=False, required=False), + 13: protobuf.Field("serialize", "bool", repeated=False, required=False), } def __init__( @@ -1189,6 +1190,7 @@ class SignTx(protobuf.MessageType): branch_id: Optional["int"] = None, amount_unit: Optional["AmountUnit"] = AmountUnit.BITCOIN, decred_staking_ticket: Optional["bool"] = False, + serialize: Optional["bool"] = True, ) -> None: self.outputs_count = outputs_count self.inputs_count = inputs_count @@ -1202,6 +1204,7 @@ class SignTx(protobuf.MessageType): self.branch_id = branch_id self.amount_unit = amount_unit self.decred_staking_ticket = decred_staking_ticket + self.serialize = serialize class TxRequest(protobuf.MessageType): diff --git a/tests/device_tests/bitcoin/test_authorize_coinjoin.py b/tests/device_tests/bitcoin/test_authorize_coinjoin.py index 399c3a384..59fdcc09d 100644 --- a/tests/device_tests/bitcoin/test_authorize_coinjoin.py +++ b/tests/device_tests/bitcoin/test_authorize_coinjoin.py @@ -188,19 +188,11 @@ def test_sign_tx(client: Client): request_output(2), request_output(3), request_output(4), - request_input(0), - request_input(0), - request_input(1), - request_output(0), - request_output(1), - request_output(2), - request_output(3), - request_output(4), request_input(1), request_finished(), ] ) - _, serialized_tx = btc.sign_tx( + signatures, serialized_tx = btc.sign_tx( client, "Testnet", inputs, @@ -208,12 +200,15 @@ def test_sign_tx(client: Client): prev_txes=TX_CACHE_TESTNET, payment_reqs=[payment_req], preauthorized=True, + serialize=False, ) - # Transaction does not exist on the blockchain, not using assert_tx_matches() + assert serialized_tx == b"" + assert len(signatures) == 2 + assert signatures[0] is None assert ( - serialized_tx.hex() - == "010000000001028abbd1cf69e00fbf60fa3ba475dccdbdba4a859ffa6bfd1ee820a75b1be2b7e50000000000ffffffff0ab6ad3ba09261cfb4fa1d3680cb19332a8fe4d9de9ea89aa565bd83a2c082f90100000000ffffffff0550c3000000000000225120e0458118b80a08042d84c4f0356d86863fe2bffc034e839c166ad4e8da7e26ef50c3000000000000225120bdb100a4e7ba327d364642dc653b9e6b51783bde6ea0df2ccbc1a78e3cc1329511e56d0000000000225120c5c7c63798b59dc16e97d916011e99da5799d1b3dd81c2f2e93392477417e71e72bf00000000000022512062fdf14323b9ccda6f5b03c5c2c28e35839a3909a2e14d32b595c63d53c7b88f51900000000000001976a914a579388225827d9f2fe9014add644487808c695d88ac000140c017fce789fa8db54a2ae032012d2dd6d7c76cc1c1a6f00e29b86acbf93022da8aa559009a574792c7b09b2535d288d6e03c6ed169902ed8c4c97626a83fbc1100000000" + signatures[1].hex() + == "c017fce789fa8db54a2ae032012d2dd6d7c76cc1c1a6f00e29b86acbf93022da8aa559009a574792c7b09b2535d288d6e03c6ed169902ed8c4c97626a83fbc11" ) # Test for a second time. @@ -249,7 +244,7 @@ def test_sign_tx_large(client: Client): own_output_count = 30 total_output_count = 1200 output_denom = 10_000 # sats - max_expected_delay = 250 # seconds + max_expected_delay = 60 # seconds with client: btc.authorize_coinjoin( @@ -338,6 +333,7 @@ def test_sign_tx_large(client: Client): prev_txes=TX_CACHE_TESTNET, payment_reqs=[payment_req], preauthorized=True, + serialize=False, ) delay = time.time() - start assert delay <= max_expected_delay