diff --git a/core/.changelog.d/4161.fixed b/core/.changelog.d/4161.fixed new file mode 100644 index 0000000000..4fc79ac044 --- /dev/null +++ b/core/.changelog.d/4161.fixed @@ -0,0 +1 @@ +[T2T1] Fix spending decred stake outputs. diff --git a/core/src/apps/bitcoin/scripts_decred.py b/core/src/apps/bitcoin/scripts_decred.py index cc3f136017..d8671d580f 100644 --- a/core/src/apps/bitcoin/scripts_decred.py +++ b/core/src/apps/bitcoin/scripts_decred.py @@ -1,8 +1,10 @@ +from micropython import const from typing import TYPE_CHECKING from trezor import utils from trezor.crypto import base58 from trezor.crypto.base58 import blake256d_32 +from trezor.enums import DecredStakingSpendType from trezor.wire import DataError from . import scripts @@ -13,6 +15,14 @@ from .scripts import ( # noqa: F401 ) from .writers import write_compact_size +# These are decred specific opcodes related to staking. +_OP_SSTX = const(0xBA) +_OP_SSGEN = const(0xBB) +_OP_SSRTX = const(0xBC) +_OP_SSTXCHANGE = const(0xBD) + +_STAKE_TREE = const(1) + if TYPE_CHECKING: from trezor.enums import InputScriptType from trezor.messages import MultisigRedeemScriptType @@ -98,7 +108,7 @@ def output_script_sstxsubmissionpkh(addr: str) -> bytearray: raise DataError("Invalid address") w = utils.empty_bytearray(26) - w.append(0xBA) # OP_SSTX + w.append(_OP_SSTX) scripts.write_output_script_p2pkh(w, raw_address[2:]) return w @@ -111,7 +121,7 @@ def output_script_sstxchange(addr: str) -> bytearray: raise DataError("Invalid address") w = utils.empty_bytearray(26) - w.append(0xBD) # OP_SSTXCHANGE + w.append(_OP_SSTXCHANGE) scripts.write_output_script_p2pkh(w, raw_address[2:]) return w @@ -120,7 +130,7 @@ def output_script_sstxchange(addr: str) -> bytearray: def write_output_script_ssrtx_prefixed(w: Writer, pkh: bytes) -> None: utils.ensure(len(pkh) == 20) write_compact_size(w, 26) - w.append(0xBC) # OP_SSRTX + w.append(_OP_SSRTX) scripts.write_output_script_p2pkh(w, pkh) @@ -128,7 +138,7 @@ def write_output_script_ssrtx_prefixed(w: Writer, pkh: bytes) -> None: def write_output_script_ssgen_prefixed(w: Writer, pkh: bytes) -> None: utils.ensure(len(pkh) == 20) write_compact_size(w, 26) - w.append(0xBB) # OP_SSGEN + w.append(_OP_SSGEN) scripts.write_output_script_p2pkh(w, pkh) @@ -141,3 +151,64 @@ def sstxcommitment_pkh(pkh: bytes, amount: int) -> bytes: write_uint64_le(w, amount) write_bytes_fixed(w, b"\x00\x58", 2) # standard fee limits return w + + +def output_script_p2pkh(pubkeyhash: bytes) -> bytearray: + s = utils.empty_bytearray(25) + scripts.write_output_script_p2pkh(s, pubkeyhash) + return s + + +def output_script_p2sh(scripthash: bytes) -> bytearray: + # A9 14 87 + utils.ensure(len(scripthash) == 20) + s = bytearray(23) + s[0] = 0xA9 # OP_HASH_160 + s[1] = 0x14 # pushing 20 bytes + s[2:22] = scripthash + s[22] = 0x87 # OP_EQUAL + return s + + +def output_derive_script( + tree: int | None, stakeType: int | None, addr: str, coin: CoinInfo +) -> bytes: + from trezor.crypto import base58 + + from apps.common import address_type + + try: + raw_address = base58.decode_check(addr, blake256d_32) + except ValueError: + raise DataError("Invalid address") + + isStakeOutput = False + if tree is not None: + if stakeType is not None: + if tree == _STAKE_TREE: + isStakeOutput = True + + if isStakeOutput: + assert stakeType is not None + if stakeType == DecredStakingSpendType.SSGen: + script = utils.empty_bytearray(26) + script.append(_OP_SSGEN) + scripts.write_output_script_p2pkh(script, raw_address[2:]) + return script + elif stakeType == DecredStakingSpendType.SSRTX: + script = utils.empty_bytearray(26) + script.append(_OP_SSRTX) + scripts.write_output_script_p2pkh(script, raw_address[2:]) + return script + + elif address_type.check(coin.address_type, raw_address): + # p2pkh + pubkeyhash = address_type.strip(coin.address_type, raw_address) + script = output_script_p2pkh(pubkeyhash) + return script + elif address_type.check(coin.address_type_p2sh, raw_address): + scripthash = address_type.strip(coin.address_type_p2sh, raw_address) + script = output_script_p2sh(scripthash) + return script + + raise DataError("Invalid address type") diff --git a/core/src/apps/bitcoin/sign_tx/decred.py b/core/src/apps/bitcoin/sign_tx/decred.py index 6c842f38ba..39b99380ab 100644 --- a/core/src/apps/bitcoin/sign_tx/decred.py +++ b/core/src/apps/bitcoin/sign_tx/decred.py @@ -9,8 +9,8 @@ from trezor.wire import DataError, ProcessError from apps.bitcoin.sign_tx.tx_weight import TxWeightCalculator from apps.common.writers import write_compact_size -from .. import scripts_decred, writers -from ..common import ecdsa_hash_pubkey +from .. import addresses, scripts_decred, writers +from ..common import ecdsa_hash_pubkey, input_is_external from ..writers import write_uint32 from . import helpers from .approvers import BasicApprover @@ -423,3 +423,21 @@ class Decred(Bitcoin): pubkey, signature, ) + + # scriptPubKey derivation + # === + + def input_derive_script( + self, txi: TxInput, node: bip32.HDNode | None = None + ) -> bytes: + if input_is_external(txi): + assert txi.script_pubkey is not None # checked in _sanitize_tx_input + return txi.script_pubkey + + if node is None: + node = self.keychain.derive(txi.address_n) + + address = addresses.get_address(txi.script_type, self.coin, node, txi.multisig) + return scripts_decred.output_derive_script( + txi.decred_tree, txi.decred_staking_spend, address, self.coin + ) diff --git a/tests/device_tests/bitcoin/test_decred.py b/tests/device_tests/bitcoin/test_decred.py index fe4a6ec6c2..82a0f29b28 100644 --- a/tests/device_tests/bitcoin/test_decred.py +++ b/tests/device_tests/bitcoin/test_decred.py @@ -43,11 +43,11 @@ FAKE_TXHASH_9ac7d2 = bytes.fromhex( FAKE_TXHASH_48f5b8 = bytes.fromhex( "48f5b85f8b1cf796d0d07388ced491f154e2d26b0615529d2d6ba9c170542df3" ) -FAKE_TXHASH_f8e2f2 = bytes.fromhex( - "f8e2f2b4eab772f6e3743cba92db341f64b84d9c16ae375c7690fbf0bf02fc7b" +FAKE_TXHASH_8b6890 = bytes.fromhex( + "8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254" ) -FAKE_TXHASH_51bc9c = bytes.fromhex( - "51bc9c71f10a81eef3caedb5333062eb4b1f70998adf02916fe98fdc04c572e8" +FAKE_TXHASH_1f00fc = bytes.fromhex( + "1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8" ) pytestmark = [ @@ -174,7 +174,7 @@ def test_spend_from_stake_generation_and_revocation_decred(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/1h/0h/0/0"), - prev_hash=FAKE_TXHASH_f8e2f2, + prev_hash=FAKE_TXHASH_8b6890, prev_index=2, amount=200_000_000, script_type=messages.InputScriptType.SPENDADDRESS, @@ -184,7 +184,7 @@ def test_spend_from_stake_generation_and_revocation_decred(client: Client): inp2 = messages.TxInputType( address_n=parse_path("m/44h/1h/0h/0/0"), - prev_hash=FAKE_TXHASH_51bc9c, + prev_hash=FAKE_TXHASH_1f00fc, prev_index=0, amount=200_000_000, script_type=messages.InputScriptType.SPENDADDRESS, @@ -208,16 +208,16 @@ def test_spend_from_stake_generation_and_revocation_decred(client: Client): (is_core(client), messages.ButtonRequest(code=B.ConfirmOutput)), messages.ButtonRequest(code=B.SignTx), request_input(0), - request_meta(FAKE_TXHASH_f8e2f2), - request_input(0, FAKE_TXHASH_f8e2f2), - request_input(1, FAKE_TXHASH_f8e2f2), - request_output(0, FAKE_TXHASH_f8e2f2), - request_output(1, FAKE_TXHASH_f8e2f2), - request_output(2, FAKE_TXHASH_f8e2f2), + request_meta(FAKE_TXHASH_8b6890), + request_input(0, FAKE_TXHASH_8b6890), + request_input(1, FAKE_TXHASH_8b6890), + request_output(0, FAKE_TXHASH_8b6890), + request_output(1, FAKE_TXHASH_8b6890), + request_output(2, FAKE_TXHASH_8b6890), request_input(1), - request_meta(FAKE_TXHASH_51bc9c), - request_input(0, FAKE_TXHASH_51bc9c), - request_output(0, FAKE_TXHASH_51bc9c), + request_meta(FAKE_TXHASH_1f00fc), + request_input(0, FAKE_TXHASH_1f00fc), + request_output(0, FAKE_TXHASH_1f00fc), request_input(0), request_input(1), request_finished(), @@ -229,7 +229,7 @@ def test_spend_from_stake_generation_and_revocation_decred(client: Client): assert ( serialized_tx.hex() - == "01000000027bfc02bff0fb90765c37ae169c4db8641f34db92ba3c74e3f672b7eab4f2e2f80200000001ffffffffe872c504dc8fe96f9102df8a99701f4beb623033b5edcaf3ee810af1719cbc510000000001ffffffff0160fdd5170000000000001976a914819d291a2f7fbf770e784bfd78b5ce92c58e95ea88ac00000000000000000200c2eb0b0000000000000000ffffffff6b483045022100f74f652a073bdaf2197ede47b4df0d90609bbfd0dc8a94199d36ebb1429de09b022040366292a8812135ec7572a94eb6e969fa1fa97a52c03f08a337f20bc4fb71de0121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd000c2eb0b0000000000000000ffffffff6b483045022100ca385c05a008239c038e107989bbc30eec1ecd5a66e4973265eb21df034c77a9022070c3dceb24b39cb6e9f8c973572b955b37a4754e9caa704cdd37113c46e2b2970121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" + == "010000000254e249113666e84c70de1dbee976f18d1438e4b7c58b376ffe64370ac190688b0200000001ffffffffc8e8dfb5c0e534b12adede611853a1f607c5b6912e03f577487c0d5354fc001f0000000001ffffffff0160fdd5170000000000001976a914819d291a2f7fbf770e784bfd78b5ce92c58e95ea88ac00000000000000000200c2eb0b0000000000000000ffffffff6b483045022100bdcb877c97d72db74eca06fefa21a7f7b00afcd5d916fce2155ed7df1ca5546102201e1f9efd7d652b449474c2c70171bfc4535544927bed62021f7334447d1ea4740121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd000c2eb0b0000000000000000ffffffff6a473044022030c5743c442bd696d19dcf73d54e95526e726de965c2e2b4b9fd70248eaae21d02201305a3bcc2bb0e33122277763990e3b48f317d61264a68d190fb8acfc004cc640121030e669acac1f280d1ddf441cd2ba5e97417bf2689e4bbec86df4f831bf9f7ffd0" ) diff --git a/tests/txcache/decred_testnet/51bc9c71f10a81eef3caedb5333062eb4b1f70998adf02916fe98fdc04c572e8.json b/tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json similarity index 88% rename from tests/txcache/decred_testnet/51bc9c71f10a81eef3caedb5333062eb4b1f70998adf02916fe98fdc04c572e8.json rename to tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json index 65653df49e..76606bf906 100644 --- a/tests/txcache/decred_testnet/51bc9c71f10a81eef3caedb5333062eb4b1f70998adf02916fe98fdc04c572e8.json +++ b/tests/txcache/decred_testnet/1f00fc54530d7c4877f5032e91b6c507f6a1531861dede2ab134e5c0b5dfe8c8.json @@ -3,7 +3,7 @@ { "amount": 200000000, "decred_script_version": 0, - "script_pubkey": "76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac", + "script_pubkey": "bc76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac", "decred_tree": 1 } ], diff --git a/tests/txcache/decred_testnet/f8e2f2b4eab772f6e3743cba92db341f64b84d9c16ae375c7690fbf0bf02fc7b.json b/tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json similarity index 93% rename from tests/txcache/decred_testnet/f8e2f2b4eab772f6e3743cba92db341f64b84d9c16ae375c7690fbf0bf02fc7b.json rename to tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json index 25961397d8..a7ae77e5e0 100644 --- a/tests/txcache/decred_testnet/f8e2f2b4eab772f6e3743cba92db341f64b84d9c16ae375c7690fbf0bf02fc7b.json +++ b/tests/txcache/decred_testnet/8b6890c10a3764fe6f378bc5b7e438148df176e9be1dde704ce866361149e254.json @@ -13,7 +13,7 @@ { "amount": 200000000, "decred_script_version": 0, - "script_pubkey": "76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac" + "script_pubkey": "bb76a914dc1a98d791735eb9a8715a2a219c23680edcedad88ac" } ], "expiry": 0,