From e3ea32a98699febbfcd5ae604eac190153cba300 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Sat, 26 Sep 2020 18:30:56 +0900 Subject: [PATCH] multi: Add decred staking. Add two new input and four output script types. Decred ticket purchases consist of a stake submission, op returns, and change addresses. Although change addresses are allowed by consensus, they are no longer used in practice and so have been given the restrictions of a null pubkey and no value. Stake scripts are almost identical to p2pkh or p2sh except for an extra opcode in front. Inputs are currently only used in the form of one input three outputs with the first output, or stake submission, paying to a public key hash, or with two inputs and five outputs with the stake submission paying to a multisig script hash. The op returns are directed to the user in the case of one and the voting service provider and user in the case of two. One of the sstx commitment for a ticket must pay back to the trezor wallet. This is checked and an error is thrown if we don't find the expected public key hash. Because this adds the ability to create new types of outputs once the ticket votes, two new input script types are also needed. A successful vote will lead to a stake generation script that must be spent, and an unsuccessful vote will lead to a revocation script that must be spent. If we allowed stake change scripts to have a valid pubkey, that too would require another op code, but we disallow those for output. --- common/protob/messages-bitcoin.proto | 39 +- core/CHANGELOG.md | 2 + core/src/apps/bitcoin/addresses.py | 5 +- core/src/apps/bitcoin/common.py | 1 + core/src/apps/bitcoin/get_public_key.py | 2 +- core/src/apps/bitcoin/scripts_decred.py | 78 ++++ core/src/apps/bitcoin/sign_tx/approvers.py | 13 + core/src/apps/bitcoin/sign_tx/decred.py | 72 +++- core/src/apps/bitcoin/sign_tx/helpers.py | 20 + core/src/apps/bitcoin/sign_tx/layout.py | 13 + .../trezor/messages/DecredStakingSpendType.py | 10 + core/src/trezor/messages/SignTx.py | 3 + core/src/trezor/messages/TxInput.py | 4 + core/src/trezor/messages/TxInputType.py | 4 + core/src/trezor/ui/layouts/tt.py | 18 + core/tests/test_apps.bitcoin.signtx_decred.py | 371 ++++++++++++++++++ .../messages/DecredStakingSpendType.py | 10 + python/src/trezorlib/messages/SignTx.py | 3 + python/src/trezorlib/messages/TxInput.py | 4 + python/src/trezorlib/messages/TxInputType.py | 4 + python/src/trezorlib/messages/__init__.py | 1 + tests/device_tests/test_msg_signtx_decred.py | 126 ++++++ ...6c507f6a1531861dede2ab134e5c0b5dfe8c8.json | 22 ++ ...438148df176e9be1dde704ce866361149e254.json | 38 ++ tests/ui_tests/fixtures.json | 2 + 25 files changed, 841 insertions(+), 24 deletions(-) create mode 100644 core/src/apps/bitcoin/scripts_decred.py create mode 100644 core/src/trezor/messages/DecredStakingSpendType.py create mode 100644 core/tests/test_apps.bitcoin.signtx_decred.py create mode 100644 python/src/trezorlib/messages/DecredStakingSpendType.py create mode 100644 tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json create mode 100644 tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json diff --git a/common/protob/messages-bitcoin.proto b/common/protob/messages-bitcoin.proto index 2bc64026cb..fd03432074 100644 --- a/common/protob/messages-bitcoin.proto +++ b/common/protob/messages-bitcoin.proto @@ -31,6 +31,14 @@ enum OutputScriptType { PAYTOP2SHWITNESS = 5; // only for change output } +/** + * Type of script which will be used for decred stake transaction input + */ +enum DecredStakingSpendType { + SSGen = 0; + SSRTX = 1; +} + /** * Unit to be used when showing amounts on the display */ @@ -171,17 +179,18 @@ message VerifyMessage { * @next Failure */ message SignTx { - required uint32 outputs_count = 1; // number of transaction outputs - required uint32 inputs_count = 2; // number of transaction inputs - optional string coin_name = 3 [default='Bitcoin']; // coin to use - optional uint32 version = 4 [default=1]; // transaction version - optional uint32 lock_time = 5 [default=0]; // transaction lock_time - optional uint32 expiry = 6; // only for Decred and Zcash - optional bool overwintered = 7 [deprecated=true]; // deprecated in 2.3.2, the field is not needed as it can be derived from `version` - optional uint32 version_group_id = 8; // only for Zcash, nVersionGroupId - optional uint32 timestamp = 9; // only for Peercoin - optional uint32 branch_id = 10; // only for Zcash, BRANCH_ID - optional AmountUnit amount_unit = 11 [default=BITCOIN]; // show amounts in + required uint32 outputs_count = 1; // number of transaction outputs + required uint32 inputs_count = 2; // number of transaction inputs + optional string coin_name = 3 [default='Bitcoin']; // coin to use + optional uint32 version = 4 [default=1]; // transaction version + optional uint32 lock_time = 5 [default=0]; // transaction lock_time + optional uint32 expiry = 6; // only for Decred and Zcash + optional bool overwintered = 7 [deprecated=true]; // deprecated in 2.3.2, the field is not needed as it can be derived from `version` + optional uint32 version_group_id = 8; // only for Zcash, nVersionGroupId + optional uint32 timestamp = 9; // only for Peercoin + 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 } /** @@ -277,7 +286,7 @@ message TxAck { optional InputScriptType script_type = 6 [default=SPENDADDRESS]; // defines template of input script optional MultisigRedeemScriptType multisig = 7; // Filled if input is going to spend multisig tx optional uint64 amount = 8; // amount of previous transaction output (for segwit only) - optional uint32 decred_tree = 9; // only for Decred + optional uint32 decred_tree = 9; // only for Decred, 0 is a normal transaction while 1 is a stake transaction // optional uint32 decred_script_version = 10; // only for Decred // deprecated -> only 0 is supported // optional bytes prev_block_hash_bip115 = 11; // BIP-115 support dropped // optional uint32 prev_block_height_bip115 = 12; // BIP-115 support dropped @@ -286,6 +295,7 @@ message TxAck { optional bytes commitment_data = 15; // optional commitment data for the SLIP-0019 proof of ownership optional bytes orig_hash = 16; // tx_hash of the original transaction where this input was spent (used when creating a replacement transaction) optional uint32 orig_index = 17; // index of the input in the original transaction (used when creating a replacement transaction) + optional DecredStakingSpendType decred_staking_spend = 18; // if not None this holds the type of stake spend: revocation or stake generation } /** * Structure representing compiled transaction output @@ -293,7 +303,7 @@ message TxAck { message TxOutputBinType { required uint64 amount = 1; required bytes script_pubkey = 2; - optional uint32 decred_script_version = 3; // only for Decred + optional uint32 decred_script_version = 3; // only for Decred, currently only 0 is supported } /** * Structure representing transaction output @@ -329,13 +339,14 @@ message TxInput { optional InputScriptType script_type = 6 [default=SPENDADDRESS]; // defines template of input script optional MultisigRedeemScriptType multisig = 7; // Filled if input is going to spend multisig tx required uint64 amount = 8; // amount of previous transaction output - optional uint32 decred_tree = 9; // only for Decred + optional uint32 decred_tree = 9; // only for Decred, 0 is a normal transaction while 1 is a stake transaction reserved 10, 11, 12; // fields which are in use, or have been in the past, in TxInputType optional bytes witness = 13; // witness data, only set for EXTERNAL inputs optional bytes ownership_proof = 14; // SLIP-0019 proof of ownership, only set for EXTERNAL inputs optional bytes commitment_data = 15; // optional commitment data for the SLIP-0019 proof of ownership optional bytes orig_hash = 16; // tx_hash of the original transaction where this input was spent (used when creating a replacement transaction) optional uint32 orig_index = 17; // index of the input in the original transaction (used when creating a replacement transaction) + optional DecredStakingSpendType decred_staking_spend = 18; // if not None this holds the type of stake spend: revocation or stake generation } /** Data type for transaction output to be signed. diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 7c01692800..deb37db8b2 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Locking the device by holding finger on the homescreen for 2.5 seconds. [#1404] - Public key to ECDHSessionKey. [#1518] +- Decred staking. [#1249] ### Changed @@ -360,6 +361,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [#1193]: https://github.com/trezor/trezor-firmware/issues/1193 [#1206]: https://github.com/trezor/trezor-firmware/issues/1206 [#1246]: https://github.com/trezor/trezor-firmware/issues/1246 +[#1249]: https://github.com/trezor/trezor-firmware/issues/1249 [#1271]: https://github.com/trezor/trezor-firmware/issues/1271 [#1292]: https://github.com/trezor/trezor-firmware/issues/1292 [#1322]: https://github.com/trezor/trezor-firmware/issues/1322 diff --git a/core/src/apps/bitcoin/addresses.py b/core/src/apps/bitcoin/addresses.py index b7ee167107..50f2569dbb 100644 --- a/core/src/apps/bitcoin/addresses.py +++ b/core/src/apps/bitcoin/addresses.py @@ -27,10 +27,7 @@ def get_address( # Ensure that our public key is included in the multisig. multisig_pubkey_index(multisig, node.public_key()) - if ( - script_type == InputScriptType.SPENDADDRESS - or script_type == InputScriptType.SPENDMULTISIG - ): + if script_type in (InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG): if multisig: # p2sh multisig if coin.address_type_p2sh is None: raise wire.ProcessError("Multisig not enabled on this coin") diff --git a/core/src/apps/bitcoin/common.py b/core/src/apps/bitcoin/common.py index 28d80f59e4..5283185c04 100644 --- a/core/src/apps/bitcoin/common.py +++ b/core/src/apps/bitcoin/common.py @@ -43,6 +43,7 @@ CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES: Dict[ OutputScriptType.PAYTOP2SHWITNESS: InputScriptType.SPENDP2SHWITNESS, OutputScriptType.PAYTOWITNESS: InputScriptType.SPENDWITNESS, } + INTERNAL_INPUT_SCRIPT_TYPES = tuple(CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES.values()) CHANGE_OUTPUT_SCRIPT_TYPES = tuple(CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES.keys()) diff --git a/core/src/apps/bitcoin/get_public_key.py b/core/src/apps/bitcoin/get_public_key.py index 087a11ca7a..f6d89d9fe6 100644 --- a/core/src/apps/bitcoin/get_public_key.py +++ b/core/src/apps/bitcoin/get_public_key.py @@ -22,7 +22,7 @@ async def get_public_key(ctx: wire.Context, msg: GetPublicKey) -> PublicKey: node = keychain.derive(msg.address_n) if ( - script_type in [InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG] + script_type in (InputScriptType.SPENDADDRESS, InputScriptType.SPENDMULTISIG) and coin.xpub_magic is not None ): node_xpub = node.serialize_public(coin.xpub_magic) diff --git a/core/src/apps/bitcoin/scripts_decred.py b/core/src/apps/bitcoin/scripts_decred.py new file mode 100644 index 0000000000..3e27f1e945 --- /dev/null +++ b/core/src/apps/bitcoin/scripts_decred.py @@ -0,0 +1,78 @@ +from trezor import utils, wire +from trezor.crypto import base58 +from trezor.crypto.base58 import blake256d_32 + +from apps.common.writers import empty_bytearray, write_bytes_fixed, write_uint64_le + + +# A ticket purchase submission for an address hash. +def output_script_sstxsubmissionpkh(addr: str) -> bytearray: + try: + raw_address = base58.decode_check(addr, blake256d_32) + except ValueError: + raise wire.DataError("Invalid address") + + w = empty_bytearray(26) + w.append(0xBA) # OP_SSTX + w.append(0x76) # OP_DUP + w.append(0xA9) # OP_HASH160 + w.append(0x14) # OP_DATA_20 + write_bytes_fixed(w, raw_address[2:], 20) + w.append(0x88) # OP_EQUALVERIFY + w.append(0xAC) # OP_CHECKSIG + return w + + +# Ticket purchase change script. +def output_script_sstxchange(addr: str) -> bytearray: + try: + raw_address = base58.decode_check(addr, blake256d_32) + except ValueError: + raise wire.DataError("Invalid address") + + w = empty_bytearray(26) + w.append(0xBD) # OP_SSTXCHANGE + w.append(0x76) # OP_DUP + w.append(0xA9) # OP_HASH160 + w.append(0x14) # OP_DATA_20 + write_bytes_fixed(w, raw_address[2:], 20) + w.append(0x88) # OP_EQUALVERIFY + w.append(0xAC) # OP_CHECKSIG + return w + + +# Spend from a stake revocation. +def output_script_ssrtx(pkh: bytes) -> bytearray: + utils.ensure(len(pkh) == 20) + s = bytearray(26) + s[0] = 0xBC # OP_SSRTX + s[1] = 0x76 # OP_DUP + s[2] = 0xA9 # OP_HASH160 + s[3] = 0x14 # OP_DATA_20 + s[4:24] = pkh + s[24] = 0x88 # OP_EQUALVERIFY + s[25] = 0xAC # OP_CHECKSIG + return s + + +# Spend from a stake generation. +def output_script_ssgen(pkh: bytes) -> bytearray: + utils.ensure(len(pkh) == 20) + s = bytearray(26) + s[0] = 0xBB # OP_SSGEN + s[1] = 0x76 # OP_DUP + s[2] = 0xA9 # OP_HASH160 + s[3] = 0x14 # OP_DATA_20 + s[4:24] = pkh + s[24] = 0x88 # OP_EQUALVERIFY + s[25] = 0xAC # OP_CHECKSIG + return s + + +# Retrieve pkh bytes from a stake commitment OPRETURN. +def sstxcommitment_pkh(pkh: bytes, amount: int) -> bytes: + w = empty_bytearray(30) + write_bytes_fixed(w, pkh, 20) + write_uint64_le(w, amount) + write_bytes_fixed(w, b"\x00\x58", 2) # standard fee limits + return w diff --git a/core/src/apps/bitcoin/sign_tx/approvers.py b/core/src/apps/bitcoin/sign_tx/approvers.py index 79b6dbb2c8..b2b1fa9352 100644 --- a/core/src/apps/bitcoin/sign_tx/approvers.py +++ b/core/src/apps/bitcoin/sign_tx/approvers.py @@ -2,6 +2,7 @@ from micropython import const from trezor import wire from trezor.messages import OutputScriptType +from trezor.utils import ensure from apps.common import safety_checks @@ -76,6 +77,11 @@ class Approver: self.weight.add_output(script_pubkey) self.total_out += txo.amount + async def add_decred_sstx_submission( + self, txo: TxOutput, script_pubkey: bytes + ) -> None: + raise NotImplementedError + def add_orig_external_output(self, txo: TxOutput) -> None: self.orig_total_out += txo.amount @@ -125,6 +131,13 @@ class BasicApprover(Approver): else: await helpers.confirm_output(txo, self.coin, self.amount_unit) + async def add_decred_sstx_submission( + self, txo: TxOutput, script_pubkey: bytes + ) -> None: + ensure(self.coin.decred) + await super().add_external_output(txo, script_pubkey, None) + await helpers.confirm_decred_sstx_submission(txo, self.coin, self.amount_unit) + async def approve_tx(self, tx_info: TxInfo, orig_txs: List[OriginalTxInfo]) -> None: fee = self.total_in - self.total_out diff --git a/core/src/apps/bitcoin/sign_tx/decred.py b/core/src/apps/bitcoin/sign_tx/decred.py index 307c184c4d..346e8797f1 100644 --- a/core/src/apps/bitcoin/sign_tx/decred.py +++ b/core/src/apps/bitcoin/sign_tx/decred.py @@ -2,13 +2,13 @@ from micropython import const from trezor import wire from trezor.crypto.hashlib import blake256 -from trezor.messages import InputScriptType +from trezor.messages import DecredStakingSpendType, InputScriptType from trezor.messages.PrevOutput import PrevOutput from trezor.utils import HashWriter, ensure from apps.common.writers import write_bitcoin_varint -from .. import multisig, scripts, writers +from .. import multisig, scripts, scripts_decred, writers from ..common import ecdsa_hash_pubkey, ecdsa_sign from . import approvers, helpers, progress from .bitcoin import Bitcoin @@ -17,8 +17,10 @@ DECRED_SERIALIZE_FULL = const(0 << 16) DECRED_SERIALIZE_NO_WITNESS = const(1 << 16) DECRED_SERIALIZE_WITNESS_SIGNING = const(3 << 16) DECRED_SCRIPT_VERSION = const(0) - DECRED_SIGHASH_ALL = const(1) +OUTPUT_SCRIPT_NULL_SSTXCHANGE = ( + b"\xBD\x76\xA9\x14\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x88\xAC" +) if False: from typing import Optional, Union, List @@ -87,7 +89,12 @@ class Decred(Bitcoin): async def step2_approve_outputs(self) -> None: write_bitcoin_varint(self.serialized_tx, self.tx_info.tx.outputs_count) write_bitcoin_varint(self.h_prefix, self.tx_info.tx.outputs_count) - await super().step2_approve_outputs() + + 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) @@ -127,7 +134,15 @@ class Decred(Bitcoin): key_sign = self.keychain.derive(txi_sign.address_n) key_sign_pub = key_sign.public_key() - if txi_sign.script_type == InputScriptType.SPENDMULTISIG: + if txi_sign.decred_staking_spend == DecredStakingSpendType.SSRTX: + prev_pkscript = scripts_decred.output_script_ssrtx( + ecdsa_hash_pubkey(key_sign_pub, self.coin) + ) + elif txi_sign.decred_staking_spend == DecredStakingSpendType.SSGen: + prev_pkscript = scripts_decred.output_script_ssgen( + ecdsa_hash_pubkey(key_sign_pub, self.coin) + ) + elif txi_sign.script_type == InputScriptType.SPENDMULTISIG: assert txi_sign.multisig is not None prev_pkscript = scripts.output_script_multisig( multisig.multisig_get_pubkeys(txi_sign.multisig), @@ -208,6 +223,53 @@ class Decred(Bitcoin): writers.write_uint16(w, DECRED_SCRIPT_VERSION) writers.write_bytes_prefixed(w, script_pubkey) + def process_sstx_commitment_owned(self, txo: TxOutput) -> bytearray: + if not self.tx_info.output_is_change(txo): + raise wire.DataError("Invalid sstxcommitment path.") + node = self.keychain.derive(txo.address_n) + pkh = ecdsa_hash_pubkey(node.public_key(), self.coin) + op_return_data = scripts_decred.sstxcommitment_pkh(pkh, txo.amount) + txo.amount = 0 # Clear the amount, since this is an OP_RETURN. + return scripts_decred.output_script_paytoopreturn(op_return_data) + + async def approve_staking_ticket(self) -> None: + if self.tx_info.tx.outputs_count != 3: + raise wire.DataError("Ticket has wrong number of outputs.") + + # SSTX submission + txo = await helpers.request_tx_output(self.tx_req, 0, self.coin) + if txo.address is None: + raise wire.DataError("Missing address.") + 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) + + # SSTX commitment + txo = await helpers.request_tx_output(self.tx_req, 1, self.coin) + if txo.amount != self.approver.total_in: + raise wire.DataError("Wrong sstxcommitment amount.") + 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) + + # SSTX change + txo = await helpers.request_tx_output(self.tx_req, 2, self.coin) + if txo.address is None: + raise wire.DataError("Missing address.") + script_pubkey = scripts_decred.output_script_sstxchange(txo.address) + # Using change addresses is no longer common practice. Inputs are split + # beforehand and should be exact. SSTX change should pay zero amount to + # a zeroed hash. + if txo.amount != 0: + raise wire.DataError("Only value of 0 allowed for sstx change.") + if script_pubkey != OUTPUT_SCRIPT_NULL_SSTXCHANGE: + 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) + def write_tx_header( self, w: writers.Writer, diff --git a/core/src/apps/bitcoin/sign_tx/helpers.py b/core/src/apps/bitcoin/sign_tx/helpers.py index 7cfc0134d0..e4494824f4 100644 --- a/core/src/apps/bitcoin/sign_tx/helpers.py +++ b/core/src/apps/bitcoin/sign_tx/helpers.py @@ -58,6 +58,22 @@ class UiConfirmOutput(UiConfirm): __eq__ = utils.obj_eq +class UiConfirmDecredSSTXSubmission(UiConfirm): + def __init__( + self, output: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmountUnit + ): + self.output = output + self.coin = coin + self.amount_unit = amount_unit + + def confirm_dialog(self, ctx: wire.Context) -> Awaitable[Any]: + return layout.confirm_decred_sstx_submission( + ctx, self.output, self.coin, self.amount_unit + ) + + __eq__ = utils.obj_eq + + class UiConfirmReplacement(UiConfirm): def __init__(self, description: str, txid: bytes): self.description = description @@ -175,6 +191,10 @@ def confirm_output(output: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmount return (yield UiConfirmOutput(output, coin, amount_unit)) +def confirm_decred_sstx_submission(output: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmountUnit) -> Awaitable[None]: # type: ignore + return (yield UiConfirmDecredSSTXSubmission(output, coin, amount_unit)) + + def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # type: ignore return (yield UiConfirmReplacement(description, txid)) diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py index c6b3d1ecf5..568aab47a4 100644 --- a/core/src/apps/bitcoin/sign_tx/layout.py +++ b/core/src/apps/bitcoin/sign_tx/layout.py @@ -73,6 +73,19 @@ async def confirm_output( await require(layout) +async def confirm_decred_sstx_submission( + ctx: wire.Context, output: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmountUnit +) -> None: + assert output.address is not None + address_short = addresses.address_short(coin, output.address) + + await require( + layouts.confirm_decred_sstx_submission( + ctx, address_short, format_coin_amount(output.amount, coin, amount_unit) + ) + ) + + async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes) -> None: await require( layouts.confirm_replacement( diff --git a/core/src/trezor/messages/DecredStakingSpendType.py b/core/src/trezor/messages/DecredStakingSpendType.py new file mode 100644 index 0000000000..fa1d65be5c --- /dev/null +++ b/core/src/trezor/messages/DecredStakingSpendType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +SSGen: Literal[0] = 0 +SSRTX: Literal[1] = 1 diff --git a/core/src/trezor/messages/SignTx.py b/core/src/trezor/messages/SignTx.py index 5b85b26a10..4275db5601 100644 --- a/core/src/trezor/messages/SignTx.py +++ b/core/src/trezor/messages/SignTx.py @@ -27,6 +27,7 @@ class SignTx(p.MessageType): timestamp: int = None, branch_id: int = None, amount_unit: EnumTypeAmountUnit = 0, + decred_staking_ticket: bool = False, ) -> None: self.outputs_count = outputs_count self.inputs_count = inputs_count @@ -38,6 +39,7 @@ class SignTx(p.MessageType): self.timestamp = timestamp self.branch_id = branch_id self.amount_unit = amount_unit + self.decred_staking_ticket = decred_staking_ticket @classmethod def get_fields(cls) -> Dict: @@ -52,4 +54,5 @@ class SignTx(p.MessageType): 9: ('timestamp', p.UVarintType, None), 10: ('branch_id', p.UVarintType, None), 11: ('amount_unit', p.EnumType("AmountUnit", (0, 1, 2, 3)), 0), # default=BITCOIN + 12: ('decred_staking_ticket', p.BoolType, False), # default=false } diff --git a/core/src/trezor/messages/TxInput.py b/core/src/trezor/messages/TxInput.py index 47d8d59c89..c3810624cb 100644 --- a/core/src/trezor/messages/TxInput.py +++ b/core/src/trezor/messages/TxInput.py @@ -9,6 +9,7 @@ if __debug__: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + EnumTypeDecredStakingSpendType = Literal[0, 1] except ImportError: pass @@ -32,6 +33,7 @@ class TxInput(p.MessageType): commitment_data: bytes = None, orig_hash: bytes = None, orig_index: int = None, + decred_staking_spend: EnumTypeDecredStakingSpendType = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.prev_hash = prev_hash @@ -47,6 +49,7 @@ class TxInput(p.MessageType): self.commitment_data = commitment_data self.orig_hash = orig_hash self.orig_index = orig_index + self.decred_staking_spend = decred_staking_spend @classmethod def get_fields(cls) -> Dict: @@ -65,4 +68,5 @@ class TxInput(p.MessageType): 15: ('commitment_data', p.BytesType, None), 16: ('orig_hash', p.BytesType, None), 17: ('orig_index', p.UVarintType, None), + 18: ('decred_staking_spend', p.EnumType("DecredStakingSpendType", (0, 1)), None), } diff --git a/core/src/trezor/messages/TxInputType.py b/core/src/trezor/messages/TxInputType.py index 2f80a3605b..e07b4351ee 100644 --- a/core/src/trezor/messages/TxInputType.py +++ b/core/src/trezor/messages/TxInputType.py @@ -9,6 +9,7 @@ if __debug__: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + EnumTypeDecredStakingSpendType = Literal[0, 1] except ImportError: pass @@ -32,6 +33,7 @@ class TxInputType(p.MessageType): commitment_data: bytes = None, orig_hash: bytes = None, orig_index: int = None, + decred_staking_spend: EnumTypeDecredStakingSpendType = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.prev_hash = prev_hash @@ -47,6 +49,7 @@ class TxInputType(p.MessageType): self.commitment_data = commitment_data self.orig_hash = orig_hash self.orig_index = orig_index + self.decred_staking_spend = decred_staking_spend @classmethod def get_fields(cls) -> Dict: @@ -65,4 +68,5 @@ class TxInputType(p.MessageType): 15: ('commitment_data', p.BytesType, None), 16: ('orig_hash', p.BytesType, None), 17: ('orig_index', p.UVarintType, None), + 18: ('decred_staking_spend', p.EnumType("DecredStakingSpendType", (0, 1)), None), } diff --git a/core/src/trezor/ui/layouts/tt.py b/core/src/trezor/ui/layouts/tt.py index f92bbc2613..c76463d023 100644 --- a/core/src/trezor/ui/layouts/tt.py +++ b/core/src/trezor/ui/layouts/tt.py @@ -44,6 +44,7 @@ __all__ = ( "show_xpub", "show_warning", "confirm_output", + "confirm_decred_sstx_submission", "confirm_hex", "confirm_total", "confirm_joint_total", @@ -327,6 +328,23 @@ def confirm_output( ) +def confirm_decred_sstx_submission( + ctx: wire.GenericContext, + address: str, + amount: str, +) -> LayoutType: + text = Text("Purchase ticket", ui.ICON_SEND, ui.GREEN) + text.normal(amount) + text.normal("with voting rights to") + text.mono(*_split_address(address)) + return interact( + ctx, + Confirm(text), + "confirm_decred_sstx_submission", + ButtonRequestType.ConfirmOutput, + ) + + def confirm_hex( ctx: wire.GenericContext, br_type: str, diff --git a/core/tests/test_apps.bitcoin.signtx_decred.py b/core/tests/test_apps.bitcoin.signtx_decred.py new file mode 100644 index 0000000000..61ce051228 --- /dev/null +++ b/core/tests/test_apps.bitcoin.signtx_decred.py @@ -0,0 +1,371 @@ +from common import * + +from trezor.utils import chunks +from trezor.crypto import bip32, bip39 +from trezor.messages.SignTx import SignTx +from trezor.messages.TxAckInput import TxAckInput +from trezor.messages.TxAckInputWrapper import TxAckInputWrapper +from trezor.messages.TxInput import TxInput +from trezor.messages.TxAckOutput import TxAckOutput +from trezor.messages.TxAckOutputWrapper import TxAckOutputWrapper +from trezor.messages.TxOutput import TxOutput +from trezor.messages.TxAckPrevMeta import TxAckPrevMeta +from trezor.messages.PrevTx import PrevTx +from trezor.messages.TxAckPrevInput import TxAckPrevInput +from trezor.messages.TxAckPrevInputWrapper import TxAckPrevInputWrapper +from trezor.messages.PrevInput import PrevInput +from trezor.messages.TxAckPrevOutput import TxAckPrevOutput +from trezor.messages.TxAckPrevOutputWrapper import TxAckPrevOutputWrapper +from trezor.messages.PrevOutput import PrevOutput +from trezor.messages.TxRequest import TxRequest +from trezor.messages.RequestType import TXINPUT, TXOUTPUT, TXMETA, TXFINISHED +from trezor.messages.TxRequestDetailsType import TxRequestDetailsType +from trezor.messages.TxRequestSerializedType import TxRequestSerializedType +from trezor.messages import AmountUnit +from trezor.messages import OutputScriptType + +from apps.common import coins +from apps.common.keychain import Keychain +from apps.bitcoin.keychain import get_schemas_for_coin +from apps.bitcoin.sign_tx import decred, helpers +from apps.bitcoin.sign_tx.approvers import BasicApprover + + +EMPTY_SERIALIZED = TxRequestSerializedType(serialized_tx=bytearray()) + +coin_decred = coins.by_name("Decred") + +ptx1 = PrevTx(version=1, lock_time=0, inputs_count=2, outputs_count=1, extra_data_len=0) +pinp1 = PrevInput( + script_sig=unhexlify( + "483045022072ba61305fe7cb542d142b8f3299a7b10f9ea61f6ffaab5dca8142601869d53c0221009a8027ed79eb3b9bc13577ac2853269323434558528c6b6a7e542be46e7e9a820141047a2d177c0f3626fc68c53610b0270fa6156181f46586c679ba6a88b34c6f4874686390b4d92e5769fbb89c8050b984f4ec0b257a0e5c4ff8bd3b035a51709503" + ), + prev_hash=unhexlify( + "c16a03f1cf8f99f6b5297ab614586cacec784c2d259af245909dedb0e39eddcf" + ), + prev_index=1, + sequence=0xFFFF_FFFF, +) +pinp2 = PrevInput( + script_sig=unhexlify( + "48304502200fd63adc8f6cb34359dc6cca9e5458d7ea50376cbd0a74514880735e6d1b8a4c0221008b6ead7fe5fbdab7319d6dfede3a0bc8e2a7c5b5a9301636d1de4aa31a3ee9b101410486ad608470d796236b003635718dfc07c0cac0cfc3bfc3079e4f491b0426f0676e6643a39198e8e7bdaffb94f4b49ea21baa107ec2e237368872836073668214" + ), + prev_hash=unhexlify( + "1ae39a2f8d59670c8fc61179148a8e61e039d0d9e8ab08610cb69b4a19453eaf" + ), + prev_index=1, + sequence=0xFFFF_FFFF, +) +pout1 = PrevOutput( + script_pubkey=unhexlify("76a91424a56db43cf6f2b02e838ea493f95d8d6047423188ac"), + amount=200000 + 200000 - 10000, + decred_script_version=0, +) + + +@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin") +class TestSignTxDecred(unittest.TestCase): + # pylint: disable=C0301 + + def test_one_one_fee(self): + + inp1 = TxInput( + address_n=[44 | 0x80000000, 42 | 0x80000000, 0 | 0x80000000, 0, 0], + prev_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + prev_index=0, + amount=390000, + multisig=None, + sequence=0xFFFF_FFFF, + ) + out1 = TxOutput( + address="DsaHnKa418BeeQmyhpQEGG4cxGAPrneydfv", + amount=390000 - 10000, + script_type=OutputScriptType.PAYTOADDRESS, + multisig=None, + ) + tx = SignTx( + coin_name="Decred", version=1, lock_time=0, inputs_count=1, outputs_count=1 + ) + + messages = [ + None, + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("0100000001") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("4ac247307a054c37525169a78d690a7a7f87d709bef7d722aae85584f59c8fdf0000000000ffffffff01") + ), + ), + TxAckOutput(tx=TxAckOutputWrapper(output=out1)), + helpers.UiConfirmOutput(out1, coin_decred, AmountUnit.BITCOIN), + True, + helpers.UiConfirmTotal(380000 + 10000, 10000, coin_decred, AmountUnit.BITCOIN), + True, + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("60cc05000000000000001976a914664b0cd46741a695a38f8ed37db2a20327471beb88ac0000000000000000") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXMETA, + details=TxRequestDetailsType( + request_index=None, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevMeta(tx=ptx1), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType( + request_index=0, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevInput(tx=TxAckPrevInputWrapper(input=pinp1)), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType( + request_index=1, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevInput(tx=TxAckPrevInputWrapper(input=pinp2)), + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType( + request_index=0, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevOutput(tx=TxAckPrevOutputWrapper(output=pout1)), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("01") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXFINISHED, + details=TxRequestDetailsType(request_index=None, tx_hash=None), + serialized=TxRequestSerializedType( + signature_index=0, + signature=unhexlify( + "3044022078a5c388838796562eb9dad176b00e6d9425bc360083f633a14948685ca8a5ce02202a1b49cd44104a9d40aee8f988281a8aac94a497b5bc7337c77cc7ddbab16f23" + ), + serialized_tx=unhexlify("70f305000000000000000000ffffffff6a473044022078a5c388838796562eb9dad176b00e6d9425bc360083f633a14948685ca8a5ce02202a1b49cd44104a9d40aee8f988281a8aac94a497b5bc7337c77cc7ddbab16f23012103fc15aa2f684457332c0ef1fe44d908ab97208102a1792caa13bcc5e886c4b321"), + ), + ), + ] + + seed = bip39.seed( + "alcohol woman abuse must during monitor noble actual mixed trade anger aisle", + "", + ) + ns = get_schemas_for_coin(coin_decred) + keychain = Keychain(seed, coin_decred.curve_name, ns) + approver = BasicApprover(tx, coin_decred) + signer = decred.Decred(tx, keychain, coin_decred, approver).signer() + + for request, response in chunks(messages, 2): + res = signer.send(request) + if isinstance(res, tuple): + _, res = res + + self.assertEqual(res, response) + + with self.assertRaises(StopIteration): + signer.send(None) + + def test_purchase_ticket(self): + inp1 = TxInput( + address_n=[44 | 0x80000000, 42 | 0x80000000, 0 | 0x80000000, 0, 0], + prev_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + prev_index=0, + amount=390000, + multisig=None, + sequence=0xFFFF_FFFF, + ) + out1 = TxOutput( + address="DsaHnKa418BeeQmyhpQEGG4cxGAPrneydfv", + amount=390000 - 10000, + script_type=OutputScriptType.PAYTOADDRESS, + multisig=None, + ) + out2 = TxOutput( + address_n=[44 | 0x80000000, 42 | 0x80000000, 0 | 0x80000000, 0, 0], + amount=390000, + script_type=OutputScriptType.PAYTOADDRESS, + multisig=None, + ) + out3 = TxOutput( + address="DsQxuVRvS4eaJ42dhQEsCXauMWjvopWgrVg", + amount=0, + script_type=OutputScriptType.PAYTOADDRESS, + multisig=None, + ) + tx = SignTx( + coin_name="Decred", version=1, lock_time=0, inputs_count=1, outputs_count=3, decred_staking_ticket=True + ) + + messages = [ + None, + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("0100000001") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("4ac247307a054c37525169a78d690a7a7f87d709bef7d722aae85584f59c8fdf0000000000ffffffff03") + ), + ), + TxAckOutput(tx=TxAckOutputWrapper(output=out1)), + helpers.UiConfirmDecredSSTXSubmission(out1, coin_decred, AmountUnit.BITCOIN), + True, + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType(request_index=1, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("60cc05000000000000001aba76a914664b0cd46741a695a38f8ed37db2a20327471beb88ac") + ), + ), + TxAckOutput(tx=TxAckOutputWrapper(output=out2)), + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType(request_index=2, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("00000000000000000000206a1e762e46655536d93ad13f88a49bde9a2df45fe62e70f30500000000000058") + ), + ), + TxAckOutput(tx=TxAckOutputWrapper(output=out3)), + helpers.UiConfirmTotal(380000 + 10000, 10000, coin_decred, AmountUnit.BITCOIN), + True, + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("000000000000000000001abd76a914000000000000000000000000000000000000000088ac0000000000000000") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXMETA, + details=TxRequestDetailsType( + request_index=None, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevMeta(tx=ptx1), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType( + request_index=0, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevInput(tx=TxAckPrevInputWrapper(input=pinp1)), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType( + request_index=1, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevInput(tx=TxAckPrevInputWrapper(input=pinp2)), + TxRequest( + request_type=TXOUTPUT, + details=TxRequestDetailsType( + request_index=0, + tx_hash=unhexlify( + "df8f9cf58455e8aa22d7f7be09d7877f7a0a698da7695152374c057a3047c24a" + ), + ), + serialized=EMPTY_SERIALIZED, + ), + TxAckPrevOutput(tx=TxAckPrevOutputWrapper(output=pout1)), + TxRequest( + request_type=TXINPUT, + details=TxRequestDetailsType(request_index=0, tx_hash=None), + serialized=TxRequestSerializedType( + serialized_tx=unhexlify("01") + ), + ), + TxAckInput(tx=TxAckInputWrapper(input=inp1)), + TxRequest( + request_type=TXFINISHED, + details=TxRequestDetailsType(), + serialized=TxRequestSerializedType( + signature_index=0, + signature=unhexlify( + "3045022100d2a6baadc88ea67ec94a1f6dca70882e647e9af68d24e1bc72f9c27359e5e6ff02207b8a939e7cf82e79e2947e8fe59a14c11ee0b3a9cd1ff084d9bd54e23291b6be" + ), + serialized_tx=unhexlify("70f305000000000000000000ffffffff6b483045022100d2a6baadc88ea67ec94a1f6dca70882e647e9af68d24e1bc72f9c27359e5e6ff02207b8a939e7cf82e79e2947e8fe59a14c11ee0b3a9cd1ff084d9bd54e23291b6be012103fc15aa2f684457332c0ef1fe44d908ab97208102a1792caa13bcc5e886c4b321") + ), + ), + ] + + seed = bip39.seed( + "alcohol woman abuse must during monitor noble actual mixed trade anger aisle", + "", + ) + ns = get_schemas_for_coin(coin_decred) + keychain = Keychain(seed, coin_decred.curve_name, ns) + approver = BasicApprover(tx, coin_decred) + signer = decred.Decred(tx, keychain, coin_decred, approver).signer() + + for request, response in chunks(messages, 2): + res = signer.send(request) + if isinstance(res, tuple): + _, res = res + + self.assertEqual(res, response) + + with self.assertRaises(StopIteration): + signer.send(None) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/src/trezorlib/messages/DecredStakingSpendType.py b/python/src/trezorlib/messages/DecredStakingSpendType.py new file mode 100644 index 0000000000..fa1d65be5c --- /dev/null +++ b/python/src/trezorlib/messages/DecredStakingSpendType.py @@ -0,0 +1,10 @@ +# Automatically generated by pb2py +# fmt: off +if __debug__: + try: + from typing_extensions import Literal # noqa: F401 + except ImportError: + pass + +SSGen: Literal[0] = 0 +SSRTX: Literal[1] = 1 diff --git a/python/src/trezorlib/messages/SignTx.py b/python/src/trezorlib/messages/SignTx.py index 4a656d2289..d1e4b71985 100644 --- a/python/src/trezorlib/messages/SignTx.py +++ b/python/src/trezorlib/messages/SignTx.py @@ -28,6 +28,7 @@ class SignTx(p.MessageType): timestamp: int = None, branch_id: int = None, amount_unit: EnumTypeAmountUnit = 0, + decred_staking_ticket: bool = False, ) -> None: self.outputs_count = outputs_count self.inputs_count = inputs_count @@ -40,6 +41,7 @@ class SignTx(p.MessageType): self.timestamp = timestamp self.branch_id = branch_id self.amount_unit = amount_unit + self.decred_staking_ticket = decred_staking_ticket @classmethod def get_fields(cls) -> Dict: @@ -55,4 +57,5 @@ class SignTx(p.MessageType): 9: ('timestamp', p.UVarintType, None), 10: ('branch_id', p.UVarintType, None), 11: ('amount_unit', p.EnumType("AmountUnit", (0, 1, 2, 3)), 0), # default=BITCOIN + 12: ('decred_staking_ticket', p.BoolType, False), # default=false } diff --git a/python/src/trezorlib/messages/TxInput.py b/python/src/trezorlib/messages/TxInput.py index 5dfc8fde0e..021557a1c2 100644 --- a/python/src/trezorlib/messages/TxInput.py +++ b/python/src/trezorlib/messages/TxInput.py @@ -9,6 +9,7 @@ if __debug__: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + EnumTypeDecredStakingSpendType = Literal[0, 1] except ImportError: pass @@ -32,6 +33,7 @@ class TxInput(p.MessageType): commitment_data: bytes = None, orig_hash: bytes = None, orig_index: int = None, + decred_staking_spend: EnumTypeDecredStakingSpendType = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.prev_hash = prev_hash @@ -47,6 +49,7 @@ class TxInput(p.MessageType): self.commitment_data = commitment_data self.orig_hash = orig_hash self.orig_index = orig_index + self.decred_staking_spend = decred_staking_spend @classmethod def get_fields(cls) -> Dict: @@ -65,4 +68,5 @@ class TxInput(p.MessageType): 15: ('commitment_data', p.BytesType, None), 16: ('orig_hash', p.BytesType, None), 17: ('orig_index', p.UVarintType, None), + 18: ('decred_staking_spend', p.EnumType("DecredStakingSpendType", (0, 1)), None), } diff --git a/python/src/trezorlib/messages/TxInputType.py b/python/src/trezorlib/messages/TxInputType.py index 70708a31c5..6becb2335b 100644 --- a/python/src/trezorlib/messages/TxInputType.py +++ b/python/src/trezorlib/messages/TxInputType.py @@ -9,6 +9,7 @@ if __debug__: from typing import Dict, List # noqa: F401 from typing_extensions import Literal # noqa: F401 EnumTypeInputScriptType = Literal[0, 1, 2, 3, 4] + EnumTypeDecredStakingSpendType = Literal[0, 1] except ImportError: pass @@ -32,6 +33,7 @@ class TxInputType(p.MessageType): commitment_data: bytes = None, orig_hash: bytes = None, orig_index: int = None, + decred_staking_spend: EnumTypeDecredStakingSpendType = None, ) -> None: self.address_n = address_n if address_n is not None else [] self.prev_hash = prev_hash @@ -47,6 +49,7 @@ class TxInputType(p.MessageType): self.commitment_data = commitment_data self.orig_hash = orig_hash self.orig_index = orig_index + self.decred_staking_spend = decred_staking_spend @classmethod def get_fields(cls) -> Dict: @@ -65,4 +68,5 @@ class TxInputType(p.MessageType): 15: ('commitment_data', p.BytesType, None), 16: ('orig_hash', p.BytesType, None), 17: ('orig_index', p.UVarintType, None), + 18: ('decred_staking_spend', p.EnumType("DecredStakingSpendType", (0, 1)), None), } diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 14b3171da4..d0efebf800 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -314,6 +314,7 @@ from . import CardanoCertificateType from . import CardanoPoolRelayType from . import DebugLinkShowTextStyle from . import DebugSwipeDirection +from . import DecredStakingSpendType from . import FailureType from . import InputScriptType from . import LiskTransactionType diff --git a/tests/device_tests/test_msg_signtx_decred.py b/tests/device_tests/test_msg_signtx_decred.py index 0024431338..cd3f6aa77f 100644 --- a/tests/device_tests/test_msg_signtx_decred.py +++ b/tests/device_tests/test_msg_signtx_decred.py @@ -44,6 +44,12 @@ TXHASH_3f7c39 = bytes.fromhex( TXHASH_16da18 = bytes.fromhex( "16da185052740d85a630e79c140558215b64e26c500212b90e16b55d13ca06a8" ) +TXHASH_8b6890 = bytes.fromhex( + "8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254" +) +TXHASH_1f00fc = bytes.fromhex( + "1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8" +) @pytest.mark.altcoin @@ -92,6 +98,126 @@ class TestMsgSigntxDecred: == "0100000001edd579e9462ee0e80127a817e0500d4f942a4cf8f2d6530e0c0a9ab3f04862e10100000000ffffffff01802b530b0000000000001976a914819d291a2f7fbf770e784bfd78b5ce92c58e95ea88ac00000000000000000100c2eb0b0000000000000000ffffffff6a473044022009e394c7dec76ab6988270b467839b1462ad781556bce37383b76e026418ce6302204f7f6ef535d2986b095d7c96232a0990a0b9ce3004894b39c167bb18e5833ac30121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" ) + @pytest.mark.skip_t1 + def test_purchase_ticket_decred(self, client): + inp1 = proto.TxInputType( + address_n=parse_path("m/44'/1'/0'/0/0"), + prev_hash=TXHASH_e16248, + prev_index=1, + amount=200000000, + decred_tree=0, + script_type=proto.InputScriptType.SPENDADDRESS, + ) + + out1 = proto.TxOutputType( + address="TscqTv1he8MZrV321SfRghw7LFBCJDKB3oz", + script_type=proto.OutputScriptType.PAYTOADDRESS, + amount=199900000, + ) + out2 = proto.TxOutputType( + address_n=parse_path("m/44'/1'/0'/0/0"), + amount=200000000, + script_type=proto.OutputScriptType.PAYTOADDRESS, + ) + out3 = proto.TxOutputType( + address="TsR28UZRprhgQQhzWns2M6cAwchrNVvbYq2", + amount=0, + script_type=proto.OutputScriptType.PAYTOADDRESS, + ) + + with client: + client.set_expected_responses( + [ + request_input(0), + request_output(0), + proto.ButtonRequest(code=B.ConfirmOutput), + request_output(1), + request_output(2), + proto.ButtonRequest(code=B.SignTx), + request_input(0), + request_meta(TXHASH_e16248), + request_input(0, TXHASH_e16248), + request_output(0, TXHASH_e16248), + request_output(1, TXHASH_e16248), + request_input(0), + request_finished(), + ] + ) + _, serialized_tx = btc.sign_tx( + client, + "Decred Testnet", + [inp1], + [out1, out2, out3], + prev_txes=TX_API, + decred_staking_ticket=True, + ) + + assert ( + serialized_tx.hex() + == "0100000001edd579e9462ee0e80127a817e0500d4f942a4cf8f2d6530e0c0a9ab3f04862e10100000000ffffffff03603bea0b0000000000001aba76a914819d291a2f7fbf770e784bfd78b5ce92c58e95ea88ac00000000000000000000206a1edc1a98d791735eb9a8715a2a219c23680edcedad00c2eb0b000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac00000000000000000100c2eb0b0000000000000000ffffffff6b4830450221008ced5411a6d92b761bdd8b9f7fbc5bfae3c31f9369050c218977f4540ab1ec9602206e89c821878ebfd959d1c4a63100eec5b1154c8d9508c039bb78e333498a73b40121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" + ) + + @pytest.mark.skip_t1 + def test_spend_from_stake_generation_and_revocation_decred(self, client): + inp1 = proto.TxInputType( + address_n=parse_path("m/44'/1'/0'/0/0"), + prev_hash=TXHASH_8b6890, + prev_index=2, + amount=200000000, + script_type=proto.InputScriptType.SPENDADDRESS, + decred_staking_spend=proto.DecredStakingSpendType.SSGen, + decred_tree=1, + ) + + inp2 = proto.TxInputType( + address_n=parse_path("m/44'/1'/0'/0/0"), + prev_hash=TXHASH_1f00fc, + prev_index=0, + amount=200000000, + script_type=proto.InputScriptType.SPENDADDRESS, + decred_staking_spend=proto.DecredStakingSpendType.SSRTX, + decred_tree=1, + ) + + out1 = proto.TxOutputType( + address="TscqTv1he8MZrV321SfRghw7LFBCJDKB3oz", + amount=399900000, + script_type=proto.OutputScriptType.PAYTOADDRESS, + ) + + with client: + client.set_expected_responses( + [ + request_input(0), + request_input(1), + request_output(0), + proto.ButtonRequest(code=B.ConfirmOutput), + proto.ButtonRequest(code=B.SignTx), + request_input(0), + request_meta(TXHASH_8b6890), + request_input(0, TXHASH_8b6890), + request_input(1, TXHASH_8b6890), + request_output(0, TXHASH_8b6890), + request_output(1, TXHASH_8b6890), + request_output(2, TXHASH_8b6890), + request_input(1), + request_meta(TXHASH_1f00fc), + request_input(0, TXHASH_1f00fc), + request_output(0, TXHASH_1f00fc), + request_input(0), + request_input(1), + request_finished(), + ] + ) + _, serialized_tx = btc.sign_tx( + client, "Decred Testnet", [inp1, inp2], [out1], prev_txes=TX_API + ) + + assert ( + serialized_tx.hex() + == "010000000254e249113666e84c70de1dbee976f18d1438e4b7c58b376ffe64370ac190688b0200000001ffffffffc8e8dfb5c0e534b12adede611853a1f607c5b6912e03f577487c0d5354fc001f0000000001ffffffff0160fdd5170000000000001976a914819d291a2f7fbf770e784bfd78b5ce92c58e95ea88ac00000000000000000200c2eb0b0000000000000000ffffffff6b483045022100bdcb877c97d72db74eca06fefa21a7f7b00afcd5d916fce2155ed7df1ca5546102201e1f9efd7d652b449474c2c70171bfc4535544927bed62021f7334447d1ea4740121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd000c2eb0b0000000000000000ffffffff6a473044022030c5743c442bd696d19dcf73d54e95526e726de965c2e2b4b9fd70248eaae21d02201305a3bcc2bb0e33122277763990e3b48f317d61264a68d190fb8acfc004cc640121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" + ) + def test_send_decred_change(self, client): inp1 = proto.TxInputType( # TscqTv1he8MZrV321SfRghw7LFBCJDKB3oz diff --git a/tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json b/tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json new file mode 100644 index 0000000000..76606bf906 --- /dev/null +++ b/tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json @@ -0,0 +1,22 @@ +{ + "bin_outputs": [ + { + "amount": 200000000, + "decred_script_version": 0, + "script_pubkey": "bc76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac", + "decred_tree": 1 + } + ], + "expiry": 0, + "inputs": [ + { + "decred_tree": 1, + "prev_hash": "569e3a8fe2354abd9ff309dc5f469dd091cbd1de6f394cc212be73fb52bb6fed", + "prev_index": 0, + "script_sig": "483045022100da17695ed60f17cc9e8a40d1199654a326e541ed77130983d5c281a21e4d743b022058420d3d905d542f290fd793176e02186e31ba8f294b52d922c5aad5ffd3d2c90121026233f32eb24b0571c703f9a89a7b581921f0379a8bca8f63fa23c033bafa0b48", + "sequence": 4294967295 + } + ], + "lock_time": 0, + "version": 1 +} diff --git a/tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json b/tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json new file mode 100644 index 0000000000..a7ae77e5e0 --- /dev/null +++ b/tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json @@ -0,0 +1,38 @@ +{ + "bin_outputs": [ + { + "amount": 0, + "decred_script_version": 0, + "script_pubkey": "6a24f1dc4222fe139509d84614643c7b24c469c6738b19e8094ba6e69fe315000000cd770800" + }, + { + "amount": 0, + "decred_script_version": 0, + "script_pubkey": "6a06010009000000" + }, + { + "amount": 200000000, + "decred_script_version": 0, + "script_pubkey": "bb76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac" + } + ], + "expiry": 0, + "inputs": [ + { + "decred_tree": 1, + "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "prev_index": 0, + "script_sig": "0000", + "sequence": 4294967295 + }, + { + "decred_tree": 1, + "prev_hash": "7f87fb8c64578ec18658e6f5860ed6187d161f284da4bacc4d2b57e5fe43567b", + "prev_index": 0, + "script_sig": "473044022006b7b98c8a0d7670264552321507fc977812a6f8fe4326f224b07cb8507dc11b0220573f7c0c9c37c1ffd0a952a72b963d47cc6884b82f7eeca7fd47161652dc9aec01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "sequence": 4294967295 + } + ], + "lock_time": 0, + "version": 1 +} diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index f80ee1092d..9ae3becb1a 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -460,8 +460,10 @@ "test_msg_signtx_dash.py-test_send_dash": "291f1a3ace22877641494a1a470a1a4a8dab6e363fc4402dadaeb52c1288c72b", "test_msg_signtx_dash.py-test_send_dash_dip2_input": "cf7fc7e6fe3a9e4063e743da6fc44c27dac013917bc00cfc63d13a183c091d91", "test_msg_signtx_decred.py-test_decred_multisig_change": "9a7e9e1adcb0ba6770e3965df8324f2b7bc46d6bcd866db9289e8e1d62ef486e", +"test_msg_signtx_decred.py-test_purchase_ticket_decred": "4d39aac92f68bd28a19077eb3447cf2fda79be5ec88940009df7c34a6aedfe25", "test_msg_signtx_decred.py-test_send_decred": "862f30f42b35d29e0cc25205621eef2c20ce40816da4fe171725905d05867194", "test_msg_signtx_decred.py-test_send_decred_change": "6b44d98d39753a65e4aee69185d7dcecaafd405403f47835d0706ce52083b2ca", +"test_msg_signtx_decred.py-test_spend_from_stake_generation_and_revocation_decred": "338e788a0f042dbc378c8ad7a2c7dfdeca9341f4fe5bfe0eda6e838f776260fc", "test_msg_signtx_external.py::test_p2pkh_presigned": "075b9a41516faba90ddd8a6ed894ed4b60de1c11dd96400a57d37e64adbc73c4", "test_msg_signtx_external.py::test_p2pkh_with_proof": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_signtx_external.py::test_p2wpkh_in_p2sh_presigned": "f88ace4e725d81fbe79bc243d427f4d2284c478cc605b32c17336226bacb7600",