mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 07:28:10 +00:00
feat(tests): Add payment request device tests.
This commit is contained in:
parent
e5281b87b5
commit
c9008c7766
100
tests/device_tests/bitcoin/payment_req.py
Normal file
100
tests/device_tests/bitcoin/payment_req.py
Normal file
@ -0,0 +1,100 @@
|
||||
from collections import namedtuple
|
||||
from hashlib import sha256
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
|
||||
from trezorlib import btc, messages
|
||||
|
||||
TextMemo = namedtuple("TextMemo", "text")
|
||||
RefundMemo = namedtuple("RefundMemo", "address_n")
|
||||
CoinPurchaseMemo = namedtuple(
|
||||
"CoinPurchaseMemo", "amount, coin_name, slip44, address_n"
|
||||
)
|
||||
|
||||
payment_req_signer = SigningKey.from_string(
|
||||
b"?S\ti\x8b\xc5o{,\xab\x03\x194\xea\xa8[_:\xeb\xdf\xce\xef\xe50\xf17D\x98`\xb9dj",
|
||||
curve=SECP256k1,
|
||||
)
|
||||
|
||||
|
||||
def hash_bytes_prefixed(hasher, data):
|
||||
hasher.update(len(data).to_bytes(1, "little"))
|
||||
hasher.update(data)
|
||||
|
||||
|
||||
def make_payment_request(
|
||||
client, recipient_name, outputs, change_addresses=None, memos=None, nonce=None
|
||||
):
|
||||
slip44 = 1 # Testnet
|
||||
|
||||
h_pr = sha256(b"SL\x00\x24")
|
||||
|
||||
if nonce:
|
||||
hash_bytes_prefixed(h_pr, nonce)
|
||||
else:
|
||||
h_pr.update(b"\0")
|
||||
|
||||
hash_bytes_prefixed(h_pr, recipient_name.encode())
|
||||
|
||||
if memos is None:
|
||||
memos = []
|
||||
|
||||
h_pr.update(len(memos).to_bytes(1, "little"))
|
||||
msg_memos = []
|
||||
for memo in memos:
|
||||
if isinstance(memo, TextMemo):
|
||||
msg_memo = messages.TextMemo(text=memo.text)
|
||||
msg_memos.append(messages.PaymentRequestMemo(text_memo=msg_memo))
|
||||
memo_type = 1
|
||||
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
|
||||
)
|
||||
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())
|
||||
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,
|
||||
)
|
||||
msg_memos.append(messages.PaymentRequestMemo(coin_purchase_memo=msg_memo))
|
||||
|
||||
memo_type = 3
|
||||
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())
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
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)
|
||||
h_outputs.update(len(address).to_bytes(1, "little"))
|
||||
h_outputs.update(address.encode())
|
||||
|
||||
h_pr.update(h_outputs.digest())
|
||||
|
||||
return messages.TxAckPaymentRequest(
|
||||
recipient_name=recipient_name,
|
||||
amount=sum(txo.amount for txo in outputs if txo.address),
|
||||
memos=msg_memos,
|
||||
nonce=nonce,
|
||||
signature=payment_req_signer.sign_digest_deterministic(h_pr.digest()),
|
||||
)
|
303
tests/device_tests/bitcoin/test_signtx_payreq.py
Normal file
303
tests/device_tests/bitcoin/test_signtx_payreq.py
Normal file
@ -0,0 +1,303 @@
|
||||
# This file is part of the Trezor project.
|
||||
#
|
||||
# Copyright (C) 2020 SatoshiLabs and contributors
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License version 3
|
||||
# as published by the Free Software Foundation.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the License along with this library.
|
||||
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
|
||||
from trezorlib import btc, messages, misc
|
||||
from trezorlib.exceptions import TrezorFailure
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
from ...tx_cache import TxCache
|
||||
from .payment_req import CoinPurchaseMemo, RefundMemo, TextMemo, make_payment_request
|
||||
|
||||
TX_API = TxCache("Testnet")
|
||||
|
||||
TXHASH_091446 = bytes.fromhex(
|
||||
"09144602765ce3dd8f4329445b20e3684e948709c5cdcaf12da3bb079c99448a"
|
||||
)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skip_t1
|
||||
|
||||
|
||||
def case(id, *args, altcoin=False):
|
||||
if altcoin:
|
||||
marks = pytest.mark.altcoin
|
||||
else:
|
||||
marks = ()
|
||||
return pytest.param(*args, id=id, marks=marks)
|
||||
|
||||
|
||||
inputs = [
|
||||
messages.TxInputType(
|
||||
address_n=parse_path("84'/1'/0'/0/0"),
|
||||
amount=12300000,
|
||||
prev_hash=TXHASH_091446,
|
||||
prev_index=0,
|
||||
script_type=messages.InputScriptType.SPENDWITNESS,
|
||||
)
|
||||
]
|
||||
|
||||
outputs = [
|
||||
messages.TxOutputType(
|
||||
address="2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp",
|
||||
amount=5000000,
|
||||
script_type=messages.OutputScriptType.PAYTOADDRESS,
|
||||
),
|
||||
messages.TxOutputType(
|
||||
address="tb1q694ccp5qcc0udmfwgp692u2s2hjpq5h407urtu",
|
||||
script_type=messages.OutputScriptType.PAYTOADDRESS,
|
||||
amount=2000000,
|
||||
),
|
||||
messages.TxOutputType(
|
||||
# tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9
|
||||
address_n=parse_path("84h/1h/0h/0/0"),
|
||||
amount=12300000 - 5000000 - 2000000 - 11000,
|
||||
script_type=messages.OutputScriptType.PAYTOWITNESS,
|
||||
),
|
||||
]
|
||||
|
||||
memos1 = [
|
||||
CoinPurchaseMemo(
|
||||
amount="15.9636 DASH",
|
||||
coin_name="Dash",
|
||||
slip44=5,
|
||||
address_n=parse_path("44'/5'/0'/1/0"),
|
||||
),
|
||||
]
|
||||
|
||||
memos2 = [
|
||||
CoinPurchaseMemo(
|
||||
amount="3.1896 DASH",
|
||||
coin_name="Dash",
|
||||
slip44=5,
|
||||
address_n=parse_path("44'/5'/0'/1/0"),
|
||||
),
|
||||
CoinPurchaseMemo(
|
||||
amount="831.570802 GRS",
|
||||
coin_name="Groestlcoin",
|
||||
slip44=17,
|
||||
address_n=parse_path("44'/17'/0'/0/3"),
|
||||
),
|
||||
]
|
||||
|
||||
memos3 = [TextMemo("Invoice #87654321."), RefundMemo(parse_path("44'/1'/0'/0/1"))]
|
||||
|
||||
PaymentRequestParams = namedtuple(
|
||||
"PaymentRequestParams", ["txo_indices", "memos", "get_nonce"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payment_request_params",
|
||||
(
|
||||
case(
|
||||
"out0", (PaymentRequestParams([0], memos1, get_nonce=True),), altcoin=True
|
||||
),
|
||||
case(
|
||||
"out1", (PaymentRequestParams([1], memos2, get_nonce=True),), altcoin=True
|
||||
),
|
||||
case("out2", (PaymentRequestParams([2], [], get_nonce=True),)),
|
||||
case(
|
||||
"out0+out1",
|
||||
(
|
||||
PaymentRequestParams([0], [], get_nonce=False),
|
||||
PaymentRequestParams([1], [], get_nonce=True),
|
||||
),
|
||||
),
|
||||
case(
|
||||
"out01",
|
||||
(PaymentRequestParams([0, 1], memos3, get_nonce=True),),
|
||||
),
|
||||
case("out012", (PaymentRequestParams([0, 1, 2], [], get_nonce=True),)),
|
||||
case("out12", (PaymentRequestParams([1, 2], [], get_nonce=True),)),
|
||||
),
|
||||
)
|
||||
def test_payment_request(client, payment_request_params):
|
||||
for txo in outputs:
|
||||
txo.payment_req_index = None
|
||||
|
||||
payment_reqs = []
|
||||
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])
|
||||
nonce = misc.get_nonce(client) if params.get_nonce else None
|
||||
payment_reqs.append(
|
||||
make_payment_request(
|
||||
client,
|
||||
recipient_name="trezor.io",
|
||||
outputs=request_outputs,
|
||||
change_addresses=["tb1qkvwu9g3k2pdxewfqr7syz89r3gj557l3uuf9r9"],
|
||||
memos=params.memos,
|
||||
nonce=nonce,
|
||||
)
|
||||
)
|
||||
|
||||
_, serialized_tx = btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=payment_reqs,
|
||||
)
|
||||
|
||||
assert (
|
||||
serialized_tx.hex()
|
||||
== "010000000001018a44999c07bba32df1cacdc50987944e68e3205b4429438fdde35c76024614090000000000ffffffff03404b4c000000000017a9147a55d61848e77ca266e79a39bfc85c580a6426c98780841e0000000000160014d16b8c0680c61fc6ed2e407455715055e41052f528b4500000000000160014b31dc2a236505a6cb9201fa0411ca38a254a7bf10247304402204adea8ae600878c5912310f546d600359f6cde8087ebd23f20f8acc7ecb2ede70220603334476c8fb478d8c539f027f9bff5f126e4438df757f9b4ba528adcb56c48012103adc58245cf28406af0ef5cc24b8afba7f1be6c72f279b642d85c48798685f86200000000"
|
||||
)
|
||||
|
||||
# Ensure that the nonce has been invalidated.
|
||||
with pytest.raises(TrezorFailure, match="Invalid nonce in payment request"):
|
||||
btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=payment_reqs,
|
||||
)
|
||||
|
||||
|
||||
def test_payment_req_wrong_amount(client):
|
||||
# Test wrong total amount in payment request.
|
||||
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],
|
||||
nonce=misc.get_nonce(client),
|
||||
)
|
||||
|
||||
# Decrease the total amount of the payment request.
|
||||
payment_req.amount -= 1
|
||||
|
||||
with pytest.raises(TrezorFailure, match="Invalid amount in payment request"):
|
||||
btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=[payment_req],
|
||||
)
|
||||
|
||||
|
||||
def test_payment_req_wrong_mac_refund(client):
|
||||
# Test wrong MAC in payment request memo.
|
||||
memo = RefundMemo(parse_path("44'/1'/0'/1/0"))
|
||||
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],
|
||||
memos=[memo],
|
||||
nonce=misc.get_nonce(client),
|
||||
)
|
||||
|
||||
# Corrupt the MAC value.
|
||||
mac = bytearray(payment_req.memos[0].refund_memo.mac)
|
||||
mac[0] ^= 1
|
||||
payment_req.memos[0].refund_memo.mac = mac
|
||||
|
||||
with pytest.raises(TrezorFailure, match="Invalid address MAC"):
|
||||
btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=[payment_req],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.altcoin
|
||||
def test_payment_req_wrong_mac_purchase(client):
|
||||
# Test wrong MAC in payment request memo.
|
||||
memo = CoinPurchaseMemo(
|
||||
amount="22.34904 DASH",
|
||||
coin_name="Dash",
|
||||
slip44=5,
|
||||
address_n=parse_path("44'/5'/0'/1/0"),
|
||||
)
|
||||
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],
|
||||
memos=[memo],
|
||||
nonce=misc.get_nonce(client),
|
||||
)
|
||||
|
||||
# Corrupt the MAC value.
|
||||
mac = bytearray(payment_req.memos[0].coin_purchase_memo.mac)
|
||||
mac[0] ^= 1
|
||||
payment_req.memos[0].coin_purchase_memo.mac = mac
|
||||
|
||||
with pytest.raises(TrezorFailure, match="Invalid address MAC"):
|
||||
btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=[payment_req],
|
||||
)
|
||||
|
||||
|
||||
def test_payment_req_wrong_output(client):
|
||||
# Test wrong output in payment request.
|
||||
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],
|
||||
nonce=misc.get_nonce(client),
|
||||
)
|
||||
|
||||
# Use a different address in the second output.
|
||||
fake_outputs = [
|
||||
outputs[0],
|
||||
messages.TxOutputType(
|
||||
address="tb1qnspxpr2xj9s2jt6qlhuvdnxw6q55jvygcf89r2",
|
||||
script_type=outputs[1].script_type,
|
||||
amount=outputs[1].amount,
|
||||
payment_req_index=outputs[1].payment_req_index,
|
||||
),
|
||||
outputs[2],
|
||||
]
|
||||
|
||||
with pytest.raises(TrezorFailure, match="Invalid signature in payment request"):
|
||||
btc.sign_tx(
|
||||
client,
|
||||
"Testnet",
|
||||
inputs,
|
||||
fake_outputs,
|
||||
prev_txes=TX_API,
|
||||
payment_reqs=[payment_req],
|
||||
)
|
@ -310,6 +310,18 @@
|
||||
"bitcoin-test_signtx_mixed_inputs.py::test_non_segwit_segwit_non_segwit_inputs": "6bbb1dc3e786d7ccc05fa62405d979d768b36753d8e4b18159e0bc9638d43596",
|
||||
"bitcoin-test_signtx_mixed_inputs.py::test_segwit_non_segwit_inputs": "34cbf0075c03f13db8285b0ca9fd3e32dc3380ef95116d873754ec10c9801b99",
|
||||
"bitcoin-test_signtx_mixed_inputs.py::test_segwit_non_segwit_segwit_inputs": "75b7f389048ad2f3124a60dd541e62718b38c079cee2aa76dfcb00cf2e31ae69",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_req_wrong_amount": "1d0da9c044d6aa5523f94e91e882f8839457d51097093fc2e5938285341a1949",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_req_wrong_mac_purchase": "1c100ce4b7c1e47e72428f390de0846c1ff933e9f07894872644a369a9422738",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_req_wrong_mac_refund": "1c100ce4b7c1e47e72428f390de0846c1ff933e9f07894872644a369a9422738",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_req_wrong_output": "887f26b4ddbb365d903b27fb9c2fab54760b2ba6f5f5252a82d8e5b4fc611fbf",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out0+out1]": "565607d0c63ec4777b08e271906f0ef69fc48c98b5bcb02d11d1fe6538dcfada",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out012]": "0ddf119ff7be991d6066b07f39a81423f77abf0e4f4fa39acd445b1cf8c6bd19",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out01]": "ea70f803eae59c3410798ac7d964829c9f8ed223a815c66835a48ff97663d901",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out0]": "90d2ea85361e790f4c9b74c691a8b66332346cd313b48d756381088fd4c1c39c",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out12]": "d97489a0d4d6b02e95fc1d282cbfd8d4c8a58db8e055ddbf95cde4488ca071c3",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out1]": "819651b20d2f23935cac3f6516e453b5c23eddfa29e601b16cdbc193049398ed",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request[out2]": "1f670f6afe51b1ab4d675f0abf0cecc351272e0cef9bcfe1396ca7b10468d03f",
|
||||
"bitcoin-test_signtx_payreq.py::test_payment_request_details": "155a1355e819da04bbebba18be17f5da4dac128586a8eb960dfdab9ce465c032",
|
||||
"bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[]": "1c100ce4b7c1e47e72428f390de0846c1ff933e9f07894872644a369a9422738",
|
||||
"bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[hello world]": "1c100ce4b7c1e47e72428f390de0846c1ff933e9f07894872644a369a9422738",
|
||||
"bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[x]": "1c100ce4b7c1e47e72428f390de0846c1ff933e9f07894872644a369a9422738",
|
||||
|
Loading…
Reference in New Issue
Block a user