feat(core): Implement Bitcoin payment requests.

matejcik/one-of
Andrew Kozlik 3 years ago committed by Andrew Kozlik
parent d0c3a6a2fa
commit 0e58218f5f

@ -0,0 +1 @@
Support Bitcoin payment requests.

@ -276,6 +276,8 @@ apps.bitcoin.sign_tx.matchcheck
import apps.bitcoin.sign_tx.matchcheck import apps.bitcoin.sign_tx.matchcheck
apps.bitcoin.sign_tx.omni apps.bitcoin.sign_tx.omni
import apps.bitcoin.sign_tx.omni import apps.bitcoin.sign_tx.omni
apps.bitcoin.sign_tx.payment_request
import apps.bitcoin.sign_tx.payment_request
apps.bitcoin.sign_tx.progress apps.bitcoin.sign_tx.progress
import apps.bitcoin.sign_tx.progress import apps.bitcoin.sign_tx.progress
apps.bitcoin.sign_tx.sig_hasher apps.bitcoin.sign_tx.sig_hasher

@ -3,20 +3,24 @@ from typing import TYPE_CHECKING
from trezor import wire from trezor import wire
from trezor.enums import OutputScriptType from trezor.enums import OutputScriptType
from trezor.ui.components.common.confirm import INFO
from apps.common import safety_checks from apps.common import safety_checks
from ..authorization import FEE_PER_ANONYMITY_DECIMALS from ..authorization import FEE_PER_ANONYMITY_DECIMALS
from ..keychain import validate_path_against_script_type from ..keychain import validate_path_against_script_type
from . import helpers, tx_weight from . import helpers, tx_weight
from .payment_request import PaymentRequestVerifier
from .tx_info import OriginalTxInfo, TxInfo from .tx_info import OriginalTxInfo, TxInfo
if TYPE_CHECKING: if TYPE_CHECKING:
from trezor.messages import SignTx from trezor.messages import SignTx
from trezor.messages import TxInput from trezor.messages import TxInput
from trezor.messages import TxOutput from trezor.messages import TxOutput
from trezor.messages import TxAckPaymentRequest
from apps.common.coininfo import CoinInfo from apps.common.coininfo import CoinInfo
from apps.common.keychain import Keychain
from ..authorization import CoinJoinAuthorization from ..authorization import CoinJoinAuthorization
@ -29,6 +33,8 @@ class Approver:
def __init__(self, tx: SignTx, coin: CoinInfo) -> None: def __init__(self, tx: SignTx, coin: CoinInfo) -> None:
self.coin = coin self.coin = coin
self.weight = tx_weight.TxWeightCalculator() self.weight = tx_weight.TxWeightCalculator()
self.payment_req_verifier: PaymentRequestVerifier | None = None
self.show_payment_req_details = False
# amounts in the current transaction # amounts in the current transaction
self.total_in = 0 # sum of input amounts self.total_in = 0 # sum of input amounts
@ -67,10 +73,27 @@ class Approver:
self.orig_total_in += txi.amount self.orig_total_in += txi.amount
self.orig_external_in += txi.amount self.orig_external_in += txi.amount
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None: def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
self.weight.add_output(script_pubkey) self.weight.add_output(script_pubkey)
self.total_out += txo.amount self.total_out += txo.amount
async def add_payment_request(
self, msg: TxAckPaymentRequest, keychain: Keychain
) -> None:
self.finish_payment_request()
self.payment_req_verifier = PaymentRequestVerifier(msg, self.coin, keychain)
def finish_payment_request(self) -> None:
if self.payment_req_verifier:
self.payment_req_verifier.verify()
self.payment_req_verifier = None
self.show_payment_req_details = False
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
self._add_output(txo, script_pubkey)
self.change_out += txo.amount self.change_out += txo.amount
if self.payment_req_verifier:
self.payment_req_verifier.add_change_output(txo)
def add_orig_change_output(self, txo: TxOutput) -> None: def add_orig_change_output(self, txo: TxOutput) -> None:
self.orig_total_out += txo.amount self.orig_total_out += txo.amount
@ -82,8 +105,9 @@ class Approver:
script_pubkey: bytes, script_pubkey: bytes,
orig_txo: TxOutput | None = None, orig_txo: TxOutput | None = None,
) -> None: ) -> None:
self.weight.add_output(script_pubkey) self._add_output(txo, script_pubkey)
self.total_out += txo.amount if self.payment_req_verifier:
self.payment_req_verifier.add_external_output(txo)
def add_orig_external_output(self, txo: TxOutput) -> None: def add_orig_external_output(self, txo: TxOutput) -> None:
self.orig_total_out += txo.amount self.orig_total_out += txo.amount
@ -94,7 +118,7 @@ class Approver:
raise NotImplementedError raise NotImplementedError
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None: async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
raise NotImplementedError self.finish_payment_request()
class BasicApprover(Approver): class BasicApprover(Approver):
@ -164,9 +188,21 @@ class BasicApprover(Approver):
raise wire.ProcessError( raise wire.ProcessError(
"Adding new OP_RETURN outputs in replacement transactions is not supported." "Adding new OP_RETURN outputs in replacement transactions is not supported."
) )
else: elif txo.payment_req_index is None or self.show_payment_req_details:
# Ask user to confirm output, unless it is part of a payment
# request, which gets confirmed separately.
await helpers.confirm_output(txo, self.coin, self.amount_unit) await helpers.confirm_output(txo, self.coin, self.amount_unit)
async def add_payment_request(
self, msg: TxAckPaymentRequest, keychain: Keychain
) -> None:
await super().add_payment_request(msg, keychain)
if msg.amount is None:
raise wire.DataError("Missing payment request amount.")
result = await helpers.confirm_payment_request(msg, self.coin, self.amount_unit)
self.show_payment_req_details = result is INFO
async def approve_orig_txids( async def approve_orig_txids(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo] self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
) -> None: ) -> None:
@ -188,6 +224,8 @@ class BasicApprover(Approver):
await helpers.confirm_replacement(description, orig.orig_hash) await helpers.confirm_replacement(description, orig.orig_hash)
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None: async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
await super().approve_tx(tx_info, orig_txs)
fee = self.total_in - self.total_out fee = self.total_in - self.total_out
# some coins require negative fees for reward TX # some coins require negative fees for reward TX
@ -315,18 +353,19 @@ class CoinJoinApprover(Approver):
def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None: def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
super().add_change_output(txo, script_pubkey) super().add_change_output(txo, script_pubkey)
self._add_output(txo, script_pubkey)
self.our_weight.add_output(script_pubkey) self.our_weight.add_output(script_pubkey)
self.group_our_count += 1 self.group_our_count += 1
async def add_external_output( async def add_payment_request(
self, self, msg: TxAckPaymentRequest, keychain: Keychain
txo: TxOutput,
script_pubkey: bytes,
orig_txo: TxOutput | None = None,
) -> None: ) -> None:
await super().add_external_output(txo, script_pubkey, orig_txo) await super().add_payment_request(msg, keychain)
self._add_output(txo, script_pubkey)
if msg.recipient_name != self.authorization.params.coordinator:
raise wire.DataError("CoinJoin coordinator mismatch in payment request.")
if msg.memos:
raise wire.DataError("Memos not allowed in CoinJoin payment request.")
async def approve_orig_txids( async def approve_orig_txids(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo] self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
@ -334,6 +373,8 @@ class CoinJoinApprover(Approver):
pass pass
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None: async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
await super().approve_tx(tx_info, orig_txs)
# The mining fee of the transaction as a whole. # The mining fee of the transaction as a whole.
mining_fee = self.total_in - self.total_out mining_fee = self.total_in - self.total_out
@ -383,6 +424,12 @@ class CoinJoinApprover(Approver):
) )
def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None: def _add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
super()._add_output(txo, script_pubkey)
# All CoinJoin outputs must be accompanied by a signed payment request.
if txo.payment_req_index is None:
raise wire.DataError("Missing payment request.")
# Assumption: CoinJoin outputs are grouped by amount. (If this assumption is # Assumption: CoinJoin outputs are grouped by amount. (If this assumption is
# not satisfied, then we will compute a lower coordinator fee, which may lead # not satisfied, then we will compute a lower coordinator fee, which may lead
# us to wrongfully decline the transaction.) # us to wrongfully decline the transaction.)

@ -126,6 +126,9 @@ class Bitcoin:
self.h_inputs: bytes | None = None self.h_inputs: bytes | None = None
self.h_external_inputs: bytes | None = None self.h_external_inputs: bytes | None = None
# The index of the payment request being processed.
self.payment_req_index: int | None = None
progress.init(tx.inputs_count, tx.outputs_count) progress.init(tx.inputs_count, tx.outputs_count)
def create_hash_writer(self) -> HashWriter: def create_hash_writer(self) -> HashWriter:
@ -427,6 +430,19 @@ class Bitcoin:
script_pubkey: bytes, script_pubkey: bytes,
orig_txo: TxOutput | None, orig_txo: TxOutput | None,
) -> None: ) -> None:
if txo.payment_req_index != self.payment_req_index:
if txo.payment_req_index is None:
# TODO not needed
self.approver.finish_payment_request()
else:
tx_ack_payment_req = await helpers.request_payment_req(
self.tx_req, txo.payment_req_index
)
await self.approver.add_payment_request(
tx_ack_payment_req, self.keychain
)
self.payment_req_index = txo.payment_req_index
if self.tx_info.output_is_change(txo): if self.tx_info.output_is_change(txo):
# Output is change and does not need approval. # Output is change and does not need approval.
self.approver.add_change_output(txo, script_pubkey) self.approver.add_change_output(txo, script_pubkey)

@ -9,6 +9,7 @@ from trezor.messages import (
SignTx, SignTx,
TxAckInput, TxAckInput,
TxAckOutput, TxAckOutput,
TxAckPaymentRequest,
TxAckPrevExtraData, TxAckPrevExtraData,
TxAckPrevInput, TxAckPrevInput,
TxAckPrevMeta, TxAckPrevMeta,
@ -63,6 +64,25 @@ class UiConfirmDecredSSTXSubmission(UiConfirm):
) )
class UiConfirmPaymentRequest(UiConfirm):
def __init__(
self,
payment_req: TxAckPaymentRequest,
coin: CoinInfo,
amount_unit: AmountUnit,
):
self.payment_req = payment_req
self.amount_unit = amount_unit
self.coin = coin
def confirm_dialog(self, ctx: wire.Context) -> Awaitable[Any]:
return layout.confirm_payment_request(
ctx, self.payment_req, self.coin, self.amount_unit
)
__eq__ = utils.obj_eq
class UiConfirmReplacement(UiConfirm): class UiConfirmReplacement(UiConfirm):
def __init__(self, description: str, txid: bytes): def __init__(self, description: str, txid: bytes):
self.description = description self.description = description
@ -187,6 +207,10 @@ def confirm_decred_sstx_submission(output: TxOutput, coin: CoinInfo, amount_unit
return (yield UiConfirmDecredSSTXSubmission(output, coin, amount_unit)) return (yield UiConfirmDecredSSTXSubmission(output, coin, amount_unit))
def confirm_payment_request(payment_req: TxAckPaymentRequest, coin: CoinInfo, amount_unit: AmountUnit) -> Awaitable[Any]: # type: ignore
return (yield UiConfirmPaymentRequest(payment_req, coin, amount_unit))
def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # type: ignore def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # type: ignore
return (yield UiConfirmReplacement(description, txid)) return (yield UiConfirmReplacement(description, txid))
@ -292,6 +316,15 @@ def request_tx_prev_output(tx_req: TxRequest, i: int, coin: CoinInfo, tx_hash: b
return ack.tx.output return ack.tx.output
def request_payment_req(tx_req: TxRequest, i: int) -> Awaitable[TxAckPaymentRequest]: # type: ignore
assert tx_req.details is not None
tx_req.request_type = RequestType.TXPAYMENTREQ
tx_req.details.request_index = i
ack = yield TxAckPaymentRequest, tx_req
_clear_tx_request(tx_req)
return sanitize_payment_req(ack)
def request_tx_finish(tx_req: TxRequest) -> Awaitable[None]: # type: ignore def request_tx_finish(tx_req: TxRequest) -> Awaitable[None]: # type: ignore
tx_req.request_type = RequestType.TXFINISHED tx_req.request_type = RequestType.TXFINISHED
yield None, tx_req yield None, tx_req
@ -459,3 +492,13 @@ def sanitize_tx_output(txo: TxOutput, coin: CoinInfo) -> TxOutput:
raise wire.DataError("Missing orig_index field.") raise wire.DataError("Missing orig_index field.")
return txo return txo
def sanitize_payment_req(payment_req: TxAckPaymentRequest) -> TxAckPaymentRequest:
for memo in payment_req.memos:
if (memo.text_memo, memo.refund_memo, memo.coin_purchase_memo).count(None) != 2:
raise wire.DataError(
"Exactly one memo type must be specified in each PaymentRequestMemo."
)
return payment_req

@ -2,7 +2,7 @@ from micropython import const
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ubinascii import hexlify from ubinascii import hexlify
from trezor import utils from trezor import ui, utils, wire
from trezor.enums import AmountUnit, ButtonRequestType, OutputScriptType from trezor.enums import AmountUnit, ButtonRequestType, OutputScriptType
from trezor.strings import format_amount, format_timestamp from trezor.strings import format_amount, format_timestamp
from trezor.ui import layouts from trezor.ui import layouts
@ -15,8 +15,9 @@ if not utils.BITCOIN_ONLY:
if TYPE_CHECKING: if TYPE_CHECKING:
from trezor import wire from typing import Any
from trezor.messages import TxOutput
from trezor.messages import TxAckPaymentRequest, TxOutput
from trezor.ui.layouts import LayoutType from trezor.ui.layouts import LayoutType
from apps.common.coininfo import CoinInfo from apps.common.coininfo import CoinInfo
@ -66,8 +67,19 @@ async def confirm_output(
else: else:
assert output.address is not None assert output.address is not None
address_short = addresses.address_short(coin, output.address) address_short = addresses.address_short(coin, output.address)
if output.payment_req_index is not None:
title = "Confirm details"
icon = ui.ICON_CONFIRM
else:
title = "Confirm sending"
icon = ui.ICON_SEND
layout = layouts.confirm_output( layout = layouts.confirm_output(
ctx, address_short, format_coin_amount(output.amount, coin, amount_unit) ctx,
address_short,
format_coin_amount(output.amount, coin, amount_unit),
title=title,
icon=icon,
) )
await layout await layout
@ -84,6 +96,33 @@ async def confirm_decred_sstx_submission(
) )
async def confirm_payment_request(
ctx: wire.Context,
msg: TxAckPaymentRequest,
coin: CoinInfo,
amount_unit: AmountUnit,
) -> Any:
memo_texts = []
for m in msg.memos:
if m.text_memo is not None:
memo_texts.append(m.text_memo.text)
elif m.refund_memo is not None:
pass
elif m.coin_purchase_memo is not None:
memo_texts.append(f"Buying {m.coin_purchase_memo.amount}.")
else:
raise wire.DataError("Unrecognized memo type in payment request memo.")
assert msg.amount is not None
return await layouts.confirm_payment_request(
ctx,
msg.recipient_name,
format_coin_amount(msg.amount, coin, amount_unit),
memo_texts,
)
async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes) -> None: async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes) -> None:
await layouts.confirm_replacement( await layouts.confirm_replacement(
ctx, ctx,

@ -0,0 +1,98 @@
from micropython import const
from typing import TYPE_CHECKING
from storage import cache
from trezor import wire
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.utils import HashWriter
from apps.common import coininfo
from apps.common.address_mac import check_address_mac
from apps.common.keychain import Keychain
from .. import writers
if TYPE_CHECKING:
from trezor.messages import TxAckPaymentRequest, TxOutput
_MEMO_TYPE_TEXT = const(1)
_MEMO_TYPE_REFUND = const(2)
_MEMO_TYPE_COIN_PURCHASE = const(3)
class PaymentRequestVerifier:
if __debug__:
# secp256k1 public key of m/0h for "all all ... all" seed.
PUBLIC_KEY = b"\x03\x0f\xdf^(\x9bZ\xefSb\x90\x95:\xe8\x1c\xe6\x0e\x84\x1f\xf9V\xf3f\xac\x12?\xa6\x9d\xb3\xc7\x9f!\xb0"
else:
PUBLIC_KEY = b""
def __init__(
self, msg: TxAckPaymentRequest, coin: coininfo.CoinInfo, keychain: Keychain
) -> None:
self.h_outputs = HashWriter(sha256())
self.amount = 0
self.expected_amount = msg.amount
self.signature = msg.signature
self.h_pr = HashWriter(sha256())
if msg.nonce:
nonce = bytes(msg.nonce)
if cache.get(cache.APP_COMMON_NONCE) != nonce:
raise wire.DataError("Invalid nonce in payment request.")
cache.delete(cache.APP_COMMON_NONCE)
else:
nonce = b""
if msg.memos:
wire.DataError("Missing nonce in payment request.")
writers.write_bytes_fixed(self.h_pr, b"SL\x00\x24", 4)
writers.write_bytes_prefixed(self.h_pr, nonce)
writers.write_bytes_prefixed(self.h_pr, msg.recipient_name.encode())
writers.write_bitcoin_varint(self.h_pr, len(msg.memos))
for m in msg.memos:
if m.text_memo is not None:
memo = m.text_memo
writers.write_uint32(self.h_pr, _MEMO_TYPE_TEXT)
writers.write_bytes_prefixed(self.h_pr, memo.text.encode())
elif m.refund_memo is not None:
memo = m.refund_memo
# Unlike in a coin purchase memo, the coin type is implied by the payment request.
check_address_mac(memo.address, memo.mac, coin.slip44, keychain)
writers.write_uint32(self.h_pr, _MEMO_TYPE_REFUND)
writers.write_bytes_prefixed(self.h_pr, memo.address.encode())
elif m.coin_purchase_memo is not None:
memo = m.coin_purchase_memo
check_address_mac(memo.address, memo.mac, memo.coin_type, keychain)
writers.write_uint32(self.h_pr, _MEMO_TYPE_COIN_PURCHASE)
writers.write_uint32(self.h_pr, memo.coin_type)
writers.write_bytes_prefixed(self.h_pr, memo.amount.encode())
writers.write_bytes_prefixed(self.h_pr, memo.address.encode())
writers.write_uint32(self.h_pr, coin.slip44)
def verify(self) -> None:
if self.expected_amount is not None and self.amount != self.expected_amount:
raise wire.DataError("Invalid amount in payment request.")
hash_outputs = writers.get_tx_hash(self.h_outputs)
writers.write_bytes_fixed(self.h_pr, hash_outputs, 32)
if not secp256k1.verify(
self.PUBLIC_KEY, self.signature, self.h_pr.get_digest()
):
raise wire.DataError("Invalid signature in payment request.")
def _add_output(self, txo: TxOutput) -> None:
# For change outputs txo.address filled in by output_derive_script().
assert txo.address is not None
writers.write_uint64(self.h_outputs, txo.amount)
writers.write_bytes_prefixed(self.h_outputs, txo.address.encode())
def add_external_output(self, txo: TxOutput) -> None:
self._add_output(txo)
self.amount += txo.amount
def add_change_output(self, txo: TxOutput) -> None:
self._add_output(txo)

@ -47,6 +47,8 @@ def find_message_handler_module(msg_type: int) -> str:
return "apps.management.change_pin" return "apps.management.change_pin"
if msg_type == MessageType.ChangeWipeCode: if msg_type == MessageType.ChangeWipeCode:
return "apps.management.change_wipe_code" return "apps.management.change_wipe_code"
elif msg_type == MessageType.GetNonce:
return "apps.management.get_nonce"
if utils.MODEL == "T" and msg_type == MessageType.SdProtect: if utils.MODEL == "T" and msg_type == MessageType.SdProtect:
return "apps.management.sd_protect" return "apps.management.sd_protect"

@ -20,7 +20,7 @@ from ...components.common.confirm import (
) )
from ...components.tt import passphrase, pin from ...components.tt import passphrase, pin
from ...components.tt.button import ButtonCancel, ButtonDefault from ...components.tt.button import ButtonCancel, ButtonDefault
from ...components.tt.confirm import Confirm, HoldToConfirm from ...components.tt.confirm import Confirm, HoldToConfirm, InfoConfirm
from ...components.tt.scroll import ( from ...components.tt.scroll import (
PAGEBREAK, PAGEBREAK,
AskPaginated, AskPaginated,
@ -39,7 +39,7 @@ from ...constants.tt import (
from ..common import button_request, interact from ..common import button_request, interact
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Awaitable, Iterable, Iterator, NoReturn, Sequence from typing import Any, Awaitable, Iterable, Iterator, NoReturn, Sequence
from ..common import PropertyType, ExceptionType from ..common import PropertyType, ExceptionType
from ...components.tt.button import ButtonContent from ...components.tt.button import ButtonContent
@ -62,6 +62,7 @@ __all__ = (
"show_xpub", "show_xpub",
"show_warning", "show_warning",
"confirm_output", "confirm_output",
"confirm_payment_request",
"confirm_blob", "confirm_blob",
"confirm_properties", "confirm_properties",
"confirm_total", "confirm_total",
@ -496,6 +497,7 @@ async def confirm_output(
width: int = MONO_ADDR_PER_LINE, width: int = MONO_ADDR_PER_LINE,
width_paginated: int = MONO_ADDR_PER_LINE - 1, width_paginated: int = MONO_ADDR_PER_LINE - 1,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
icon: str = ui.ICON_SEND,
) -> None: ) -> None:
header_lines = to_str.count("\n") + int(subtitle is not None) header_lines = to_str.count("\n") + int(subtitle is not None)
if len(address) > (TEXT_MAX_LINES - header_lines) * width: if len(address) > (TEXT_MAX_LINES - header_lines) * width:
@ -506,9 +508,9 @@ async def confirm_output(
if to_paginated: if to_paginated:
para.append((ui.NORMAL, "to")) para.append((ui.NORMAL, "to"))
para.extend((ui.MONO, line) for line in chunks(address, width_paginated)) para.extend((ui.MONO, line) for line in chunks(address, width_paginated))
content: ui.Layout = paginate_paragraphs(para, title, ui.ICON_SEND, ui.GREEN) content: ui.Layout = paginate_paragraphs(para, title, icon, ui.GREEN)
else: else:
text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False) text = Text(title, icon, ui.GREEN, new_lines=False)
if subtitle is not None: if subtitle is not None:
text.normal(subtitle, "\n") text.normal(subtitle, "\n")
text.content = [font_amount, amount, ui.NORMAL, color_to, to_str, ui.FG] text.content = [font_amount, amount, ui.NORMAL, color_to, to_str, ui.FG]
@ -518,6 +520,28 @@ async def confirm_output(
await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code)) await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code))
async def confirm_payment_request(
ctx: wire.GenericContext,
recipient_name: str,
amount: str,
memos: list[str],
) -> Any:
para = [(ui.NORMAL, f"{amount} to\n{recipient_name}")]
para.extend((ui.NORMAL, memo) for memo in memos)
content = paginate_paragraphs(
para,
"Confirm sending",
ui.ICON_SEND,
ui.GREEN,
confirm=lambda text: InfoConfirm(text, info="Details"),
)
return await raise_if_cancelled(
interact(
ctx, content, "confirm_payment_request", ButtonRequestType.ConfirmOutput
)
)
async def should_show_more( async def should_show_more(
ctx: wire.GenericContext, ctx: wire.GenericContext,
title: str, title: str,

@ -2,17 +2,22 @@ from common import unittest, await_result, H_
import storage.cache import storage.cache
from trezor import wire from trezor import wire
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.messages import AuthorizeCoinJoin from trezor.messages import AuthorizeCoinJoin
from trezor.messages import TxInput from trezor.messages import TxInput
from trezor.messages import TxOutput from trezor.messages import TxOutput
from trezor.messages import SignTx from trezor.messages import SignTx
from trezor.messages import TxAckPaymentRequest
from trezor.enums import InputScriptType, OutputScriptType from trezor.enums import InputScriptType, OutputScriptType
from trezor.utils import HashWriter
from apps.common import coins from apps.common import coins
from apps.bitcoin.authorization import CoinJoinAuthorization from apps.bitcoin.authorization import CoinJoinAuthorization
from apps.bitcoin.sign_tx.approvers import CoinJoinApprover from apps.bitcoin.sign_tx.approvers import CoinJoinApprover
from apps.bitcoin.sign_tx.bitcoin import Bitcoin from apps.bitcoin.sign_tx.bitcoin import Bitcoin
from apps.bitcoin.sign_tx.tx_info import TxInfo from apps.bitcoin.sign_tx.tx_info import TxInfo
from apps.bitcoin import writers
class TestApprover(unittest.TestCase): class TestApprover(unittest.TestCase):
@ -20,9 +25,10 @@ class TestApprover(unittest.TestCase):
def setUp(self): def setUp(self):
self.coin = coins.by_name('Bitcoin') self.coin = coins.by_name('Bitcoin')
self.fee_per_anonymity_percent = 0.003 self.fee_per_anonymity_percent = 0.003
self.coordinator_name = "www.example.com"
self.msg_auth = AuthorizeCoinJoin( self.msg_auth = AuthorizeCoinJoin(
coordinator="www.example.com", coordinator=self.coordinator_name,
max_total_fee=40000, max_total_fee=40000,
fee_per_anonymity=int(self.fee_per_anonymity_percent * 10**9), fee_per_anonymity=int(self.fee_per_anonymity_percent * 10**9),
address_n=[H_(84), H_(0), H_(0)], address_n=[H_(84), H_(0), H_(0)],
@ -62,8 +68,10 @@ class TestApprover(unittest.TestCase):
# Other's CoinJoined outputs. # Other's CoinJoined outputs.
outputs = [ outputs = [
TxOutput( TxOutput(
address="",
amount=denomination, amount=denomination,
script_type=OutputScriptType.PAYTOWITNESS, script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99) ) for i in range(99)
] ]
@ -71,9 +79,11 @@ class TestApprover(unittest.TestCase):
outputs.insert( outputs.insert(
40, 40,
TxOutput( TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 0, 2], address_n=[H_(84), H_(0), H_(0), 0, 2],
amount=denomination, amount=denomination,
script_type=OutputScriptType.PAYTOWITNESS, script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) )
) )
@ -84,25 +94,31 @@ class TestApprover(unittest.TestCase):
# Other's change-outputs. # Other's change-outputs.
outputs.extend( outputs.extend(
TxOutput( TxOutput(
address="",
amount=1000000 * (i + 1) - fees, amount=1000000 * (i + 1) - fees,
script_type=OutputScriptType.PAYTOWITNESS, script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99) ) for i in range(99)
) )
# Our change-output. # Our change-output.
outputs.append( outputs.append(
TxOutput( TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 1, 1], address_n=[H_(84), H_(0), H_(0), 1, 1],
amount=1000000 - fees, amount=1000000 - fees,
script_type=OutputScriptType.PAYTOWITNESS, script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) )
) )
# Coordinator's output. # Coordinator's output.
outputs.append( outputs.append(
TxOutput( TxOutput(
address="",
amount=total_coordinator_fee, amount=total_coordinator_fee,
script_type=OutputScriptType.PAYTOWITNESS, script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) )
) )
@ -111,12 +127,34 @@ class TestApprover(unittest.TestCase):
approver = CoinJoinApprover(tx, self.coin, authorization) approver = CoinJoinApprover(tx, self.coin, authorization)
signer = Bitcoin(tx, None, self.coin, approver) signer = Bitcoin(tx, None, self.coin, approver)
# Compute payment request signature.
# Private key of m/0h for "all all ... all" seed.
private_key = b'?S\ti\x8b\xc5o{,\xab\x03\x194\xea\xa8[_:\xeb\xdf\xce\xef\xe50\xf17D\x98`\xb9dj'
h_pr = HashWriter(sha256())
writers.write_bytes_fixed(h_pr, b"SL\x00\x24", 4)
writers.write_bytes_prefixed(h_pr, b"") # Empty nonce.
writers.write_bytes_prefixed(h_pr, self.coordinator_name.encode())
writers.write_bitcoin_varint(h_pr, 0) # No memos.
writers.write_uint32(h_pr, self.coin.slip44)
h_outputs = HashWriter(sha256())
for txo in outputs:
writers.write_uint64(h_outputs, txo.amount)
writers.write_bytes_prefixed(h_outputs, txo.address.encode())
writers.write_bytes_fixed(h_pr, h_outputs.get_digest(), 32)
signature = secp256k1.sign(private_key, h_pr.get_digest())
tx_ack_payment_req = TxAckPaymentRequest(
recipient_name=self.coordinator_name,
signature=signature,
)
for txi in inputs: for txi in inputs:
if txi.script_type == InputScriptType.EXTERNAL: if txi.script_type == InputScriptType.EXTERNAL:
approver.add_external_input(txi) approver.add_external_input(txi)
else: else:
await_result(approver.add_internal_input(txi)) await_result(approver.add_internal_input(txi))
await_result(approver.add_payment_request(tx_ack_payment_req, None))
for txo in outputs: for txo in outputs:
if txo.address_n: if txo.address_n:
approver.add_change_output(txo, script_pubkey=bytes(22)) approver.add_change_output(txo, script_pubkey=bytes(22))

@ -288,6 +288,14 @@ Host must respond with a `TxAckOutput` message. All relevant data must be set in
`tx.output`. The derivation path and script type are mandatory for all original `tx.output`. The derivation path and script type are mandatory for all original
change-outputs. change-outputs.
### Payment request
Trezor sets `request_type` to `TXPAYMENTREQ`, and `request_details.tx_hash` is unset.
`request_details.request_index` is the index of the payment request in the transaction:
0 is the first payment request, 1 is second, etc.
The host must respond with a `TxAckPaymentRequest` message.
## Replacement transactions ## Replacement transactions
A replacement transaction is a transaction that uses the same inputs as one or more A replacement transaction is a transaction that uses the same inputs as one or more
@ -334,6 +342,22 @@ So the replacement transaction is, for example, allowed to:
original external outputs or even to increase the user's change outputs so as to original external outputs or even to increase the user's change outputs so as to
decrease the amount that the user is spending. decrease the amount that the user is spending.
## Payment requests
In Trezor T a set of transaction outputs can be accompanied by a payment request.
Multiple payment requests per transaction are also possible. A payment request is a
message signed by a trusted party requesting payment of certain amounts to a set of
outputs as specified in [SLIP-24](https://github.com/satoshilabs/slips/blob/master/slip-0024.md).
The user then does not have to verify the output addresses, but only confirms the
payment of the requested amount to the recipient.
The host signals that an output belongs to a payment request by setting the
`payment_req_index` field in the `TxOutput` message. When Trezor encounters the first
output that has this field set to a particular index, it will ask for the payment request
that has the given index.
All outputs belonging to one payment request must be consecutive in the transaction.
## Implementation notes ## Implementation notes
### Pseudo-code ### Pseudo-code

Loading…
Cancel
Save