From faf09b48d04d4e2680eb95a831cf7fcdefbc41dc Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Thu, 10 Apr 2025 14:02:28 +0200 Subject: [PATCH] feat(tests): Support non-bitcoinlike coins in make_payment_request. --- core/embed/upymod/qstrdefsport.h | 2 +- tests/click_tests/test_autolock.py | 2 +- tests/device_tests/bitcoin/coinjoin_req.py | 26 ++++++ .../bitcoin/test_authorize_coinjoin.py | 2 +- .../bitcoin/test_signtx_payreq.py | 36 ++++++-- .../device_tests/{bitcoin => }/payment_req.py | 91 ++++++++----------- 6 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 tests/device_tests/bitcoin/coinjoin_req.py rename tests/device_tests/{bitcoin => }/payment_req.py (55%) diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h index 3120510a06..8b14e67759 100644 --- a/core/embed/upymod/qstrdefsport.h +++ b/core/embed/upymod/qstrdefsport.h @@ -85,7 +85,6 @@ Q(apps.bitcoin.sign_tx.helpers) Q(apps.bitcoin.sign_tx.layout) Q(apps.bitcoin.sign_tx.matchcheck) Q(apps.bitcoin.sign_tx.omni) -Q(apps.bitcoin.sign_tx.payment_request) Q(apps.bitcoin.sign_tx.progress) Q(apps.bitcoin.sign_tx.sig_hasher) Q(apps.bitcoin.sign_tx.tx_info) @@ -107,6 +106,7 @@ Q(apps.common.keychain) Q(apps.common.passphrase) Q(apps.common.paths) Q(apps.common.payment_request) +Q(payment_request) Q(apps.common.readers) Q(apps.common.request_pin) Q(apps.common.safety_checks) diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py index 9de92b57ac..68e9690fa4 100644 --- a/tests/click_tests/test_autolock.py +++ b/tests/click_tests/test_autolock.py @@ -27,7 +27,7 @@ from trezorlib.tools import parse_path from .. import common from .. import translations as TR -from ..device_tests.bitcoin.payment_req import make_coinjoin_request +from ..device_tests.bitcoin.coinjoin_req import make_coinjoin_request from ..tx_cache import TxCache from . import recovery from .common import go_next, unlock_gesture diff --git a/tests/device_tests/bitcoin/coinjoin_req.py b/tests/device_tests/bitcoin/coinjoin_req.py new file mode 100644 index 0000000000..10bf71e768 --- /dev/null +++ b/tests/device_tests/bitcoin/coinjoin_req.py @@ -0,0 +1,26 @@ +from trezorlib import messages + + +def make_coinjoin_request( + coordinator_name, + inputs, + input_script_pubkeys, + outputs, + output_script_pubkeys, + no_fee_indices, + fee_rate=500_000, # 0.5 % + no_fee_threshold=1_000_000, + min_registrable_amount=5_000, +): + # Process inputs. + for i, txi in enumerate(inputs): + # Set no_fee flag in coinjoin_flags. + txi.coinjoin_flags |= (i in no_fee_indices) << 1 + + return messages.CoinJoinRequest( + fee_rate=fee_rate, + no_fee_threshold=no_fee_threshold, + min_registrable_amount=min_registrable_amount, + mask_public_key=b"", + signature=b"", + ) diff --git a/tests/device_tests/bitcoin/test_authorize_coinjoin.py b/tests/device_tests/bitcoin/test_authorize_coinjoin.py index 15028d83b3..95093a2547 100644 --- a/tests/device_tests/bitcoin/test_authorize_coinjoin.py +++ b/tests/device_tests/bitcoin/test_authorize_coinjoin.py @@ -25,7 +25,7 @@ from trezorlib.tools import parse_path from ...common import is_core from ...tx_cache import TxCache -from .payment_req import make_coinjoin_request +from .coinjoin_req import make_coinjoin_request from .signtx import ( assert_tx_matches, request_finished, diff --git a/tests/device_tests/bitcoin/test_signtx_payreq.py b/tests/device_tests/bitcoin/test_signtx_payreq.py index e02cb2b6c6..02fd05765b 100644 --- a/tests/device_tests/bitcoin/test_signtx_payreq.py +++ b/tests/device_tests/bitcoin/test_signtx_payreq.py @@ -24,7 +24,7 @@ from trezorlib.exceptions import TrezorFailure from trezorlib.tools import parse_path from ...input_flows import InputFlowPaymentRequestDetails -from .payment_req import CoinPurchaseMemo, RefundMemo, TextMemo, make_payment_request +from ..payment_req import CoinPurchaseMemo, RefundMemo, TextMemo, make_payment_request from .signtx import forge_prevtx # address at seed "all all all..." path m/84h/1h/0h/0/0 @@ -146,13 +146,24 @@ def test_payment_request(client: Client, payment_request_params): for i, params in enumerate(payment_request_params): request_outputs = [] for txo_index in params.txo_indices: - outputs[txo_index].payment_req_index = i - request_outputs.append(outputs[txo_index]) + output = outputs[txo_index] + output.payment_req_index = i + request_outputs.append((output.amount, output.address)) nonce = misc.get_nonce(client) if params.get_nonce else None + for memo in params.memos: + if isinstance(memo, RefundMemo): + memo.address_resp = btc.get_authenticated_address( + client, "Testnet", memo.address_n + ) + elif isinstance(memo, CoinPurchaseMemo): + memo.address_resp = btc.get_authenticated_address( + client, memo.coin_name, memo.address_n + ) payment_reqs.append( make_payment_request( client, recipient_name="trezor.io", + slip44=1, outputs=request_outputs, change_addresses=["tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9"], memos=params.memos, @@ -194,7 +205,8 @@ def test_payment_request_details(client: Client): make_payment_request( client, recipient_name="trezor.io", - outputs=outputs[:2], + slip44=1, + outputs=[(txo.amount, txo.address) for txo in outputs[:2]], memos=[TextMemo("Invoice #87654321.")], nonce=nonce, ) @@ -224,7 +236,8 @@ def test_payment_req_wrong_amount(client: Client): payment_req = make_payment_request( client, recipient_name="trezor.io", - outputs=outputs[:2], + slip44=1, + outputs=[(txo.amount, txo.address) for txo in outputs[:2]], nonce=misc.get_nonce(client), ) @@ -245,13 +258,15 @@ def test_payment_req_wrong_amount(client: Client): def test_payment_req_wrong_mac_refund(client: Client): # Test wrong MAC in payment request memo. memo = RefundMemo(parse_path("m/44h/1h/0h/1/0")) + memo.address_resp = btc.get_authenticated_address(client, "Testnet", memo.address_n) outputs[0].payment_req_index = 0 outputs[1].payment_req_index = 0 outputs[2].payment_req_index = None payment_req = make_payment_request( client, recipient_name="trezor.io", - outputs=outputs[:2], + slip44=1, + outputs=[(txo.amount, txo.address) for txo in outputs[:2]], memos=[memo], nonce=misc.get_nonce(client), ) @@ -282,13 +297,17 @@ def test_payment_req_wrong_mac_purchase(client: Client): slip44=5, address_n=parse_path("m/44h/5h/0h/1/0"), ) + memo.address_resp = btc.get_authenticated_address( + client, memo.coin_name, memo.address_n + ) outputs[0].payment_req_index = 0 outputs[1].payment_req_index = 0 outputs[2].payment_req_index = None payment_req = make_payment_request( client, recipient_name="trezor.io", - outputs=outputs[:2], + slip44=1, + outputs=[(txo.amount, txo.address) for txo in outputs[:2]], memos=[memo], nonce=misc.get_nonce(client), ) @@ -317,7 +336,8 @@ def test_payment_req_wrong_output(client: Client): payment_req = make_payment_request( client, recipient_name="trezor.io", - outputs=outputs[:2], + slip44=1, + outputs=[(txo.amount, txo.address) for txo in outputs[:2]], nonce=misc.get_nonce(client), ) diff --git a/tests/device_tests/bitcoin/payment_req.py b/tests/device_tests/payment_req.py similarity index 55% rename from tests/device_tests/bitcoin/payment_req.py rename to tests/device_tests/payment_req.py index 746937ae56..62c1f44047 100644 --- a/tests/device_tests/bitcoin/payment_req.py +++ b/tests/device_tests/payment_req.py @@ -1,19 +1,32 @@ -from collections import namedtuple +from dataclasses import dataclass from hashlib import sha256 from ecdsa import SECP256k1, SigningKey -from trezorlib import btc, messages +from trezorlib import messages -from ...common import compact_size +from ..common import compact_size -SLIP44 = 1 # Testnet -TextMemo = namedtuple("TextMemo", "text") -RefundMemo = namedtuple("RefundMemo", "address_n") -CoinPurchaseMemo = namedtuple( - "CoinPurchaseMemo", "amount, coin_name, slip44, address_n" -) +@dataclass +class TextMemo: + text: str + + +@dataclass +class RefundMemo: + address_n: list[int] + address_resp: messages.Address | messages.EthereumAddress | None = None + + +@dataclass +class CoinPurchaseMemo: + amount: int + coin_name: str + slip44: int + address_n: list[int] + address_resp: messages.Address | messages.EthereumAddress | None = None + payment_req_signer = SigningKey.from_string( b"?S\ti\x8b\xc5o{,\xab\x03\x194\xea\xa8[_:\xeb\xdf\xce\xef\xe50\xf17D\x98`\xb9dj", @@ -27,7 +40,13 @@ def hash_bytes_prefixed(hasher, data): def make_payment_request( - client, recipient_name, outputs, change_addresses=None, memos=None, nonce=None + client, + recipient_name, + slip44, + outputs, + change_addresses=None, + memos=None, + nonce=None, ): h_pr = sha256(b"SL\x00\x24") @@ -51,25 +70,19 @@ def make_payment_request( h_pr.update(memo_type.to_bytes(4, "little")) hash_bytes_prefixed(h_pr, memo.text.encode()) elif isinstance(memo, RefundMemo): - address_resp = btc.get_authenticated_address( - client, "Testnet", memo.address_n - ) msg_memo = messages.RefundMemo( - address=address_resp.address, mac=address_resp.mac + address=memo.address_resp.address, mac=memo.address_resp.mac ) msg_memos.append(messages.PaymentRequestMemo(refund_memo=msg_memo)) memo_type = 2 h_pr.update(memo_type.to_bytes(4, "little")) - hash_bytes_prefixed(h_pr, address_resp.address.encode()) + hash_bytes_prefixed(h_pr, memo.address_resp.address.encode()) elif isinstance(memo, CoinPurchaseMemo): - address_resp = btc.get_authenticated_address( - client, memo.coin_name, memo.address_n - ) msg_memo = messages.CoinPurchaseMemo( coin_type=memo.slip44, amount=memo.amount, - address=address_resp.address, - mac=address_resp.mac, + address=memo.address_resp.address, + mac=memo.address_resp.mac, ) msg_memos.append(messages.PaymentRequestMemo(coin_purchase_memo=msg_memo)) @@ -77,17 +90,18 @@ def make_payment_request( h_pr.update(memo_type.to_bytes(4, "little")) h_pr.update(memo.slip44.to_bytes(4, "little")) hash_bytes_prefixed(h_pr, memo.amount.encode()) - hash_bytes_prefixed(h_pr, address_resp.address.encode()) + hash_bytes_prefixed(h_pr, memo.address_resp.address.encode()) else: raise ValueError - h_pr.update(SLIP44.to_bytes(4, "little")) + h_pr.update(slip44.to_bytes(4, "little")) change_address = iter(change_addresses or []) h_outputs = sha256() - for txo in outputs: - h_outputs.update(txo.amount.to_bytes(8, "little")) - address = txo.address or next(change_address) + for amount, address in outputs: + h_outputs.update(amount.to_bytes(8, "little")) + if not address: + address = next(change_address) h_outputs.update(len(address).to_bytes(1, "little")) h_outputs.update(address.encode()) @@ -95,33 +109,8 @@ def make_payment_request( return messages.PaymentRequest( recipient_name=recipient_name, - amount=sum(txo.amount for txo in outputs if txo.address), + amount=sum(amount for amount, address in outputs if address), memos=msg_memos, nonce=nonce, signature=payment_req_signer.sign_digest_deterministic(h_pr.digest()), ) - - -def make_coinjoin_request( - coordinator_name, - inputs, - input_script_pubkeys, - outputs, - output_script_pubkeys, - no_fee_indices, - fee_rate=500_000, # 0.5 % - no_fee_threshold=1_000_000, - min_registrable_amount=5_000, -): - # Process inputs. - for i, txi in enumerate(inputs): - # Set no_fee flag in coinjoin_flags. - txi.coinjoin_flags |= (i in no_fee_indices) << 1 - - return messages.CoinJoinRequest( - fee_rate=fee_rate, - no_fee_threshold=no_fee_threshold, - min_registrable_amount=min_registrable_amount, - mask_public_key=b"", - signature=b"", - )