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
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
import apps.bitcoin.sign_tx.progress
apps.bitcoin.sign_tx.sig_hasher

@ -3,20 +3,24 @@ from typing import TYPE_CHECKING
from trezor import wire
from trezor.enums import OutputScriptType
from trezor.ui.components.common.confirm import INFO
from apps.common import safety_checks
from ..authorization import FEE_PER_ANONYMITY_DECIMALS
from ..keychain import validate_path_against_script_type
from . import helpers, tx_weight
from .payment_request import PaymentRequestVerifier
from .tx_info import OriginalTxInfo, TxInfo
if TYPE_CHECKING:
from trezor.messages import SignTx
from trezor.messages import TxInput
from trezor.messages import TxOutput
from trezor.messages import TxAckPaymentRequest
from apps.common.coininfo import CoinInfo
from apps.common.keychain import Keychain
from ..authorization import CoinJoinAuthorization
@ -29,6 +33,8 @@ class Approver:
def __init__(self, tx: SignTx, coin: CoinInfo) -> None:
self.coin = coin
self.weight = tx_weight.TxWeightCalculator()
self.payment_req_verifier: PaymentRequestVerifier | None = None
self.show_payment_req_details = False
# amounts in the current transaction
self.total_in = 0 # sum of input amounts
@ -67,10 +73,27 @@ class Approver:
self.orig_total_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.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
if self.payment_req_verifier:
self.payment_req_verifier.add_change_output(txo)
def add_orig_change_output(self, txo: TxOutput) -> None:
self.orig_total_out += txo.amount
@ -82,8 +105,9 @@ class Approver:
script_pubkey: bytes,
orig_txo: TxOutput | None = None,
) -> None:
self.weight.add_output(script_pubkey)
self.total_out += txo.amount
self._add_output(txo, script_pubkey)
if self.payment_req_verifier:
self.payment_req_verifier.add_external_output(txo)
def add_orig_external_output(self, txo: TxOutput) -> None:
self.orig_total_out += txo.amount
@ -94,7 +118,7 @@ class Approver:
raise NotImplementedError
async def approve_tx(self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]) -> None:
raise NotImplementedError
self.finish_payment_request()
class BasicApprover(Approver):
@ -164,9 +188,21 @@ class BasicApprover(Approver):
raise wire.ProcessError(
"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)
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(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
) -> None:
@ -188,6 +224,8 @@ class BasicApprover(Approver):
await helpers.confirm_replacement(description, orig.orig_hash)
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
# 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:
super().add_change_output(txo, script_pubkey)
self._add_output(txo, script_pubkey)
self.our_weight.add_output(script_pubkey)
self.group_our_count += 1
async def add_external_output(
self,
txo: TxOutput,
script_pubkey: bytes,
orig_txo: TxOutput | None = None,
async def add_payment_request(
self, msg: TxAckPaymentRequest, keychain: Keychain
) -> None:
await super().add_external_output(txo, script_pubkey, orig_txo)
self._add_output(txo, script_pubkey)
await super().add_payment_request(msg, keychain)
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(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
@ -334,6 +373,8 @@ class CoinJoinApprover(Approver):
pass
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.
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:
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
# not satisfied, then we will compute a lower coordinator fee, which may lead
# us to wrongfully decline the transaction.)

@ -126,6 +126,9 @@ class Bitcoin:
self.h_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)
def create_hash_writer(self) -> HashWriter:
@ -427,6 +430,19 @@ class Bitcoin:
script_pubkey: bytes,
orig_txo: TxOutput | 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):
# Output is change and does not need approval.
self.approver.add_change_output(txo, script_pubkey)

@ -9,6 +9,7 @@ from trezor.messages import (
SignTx,
TxAckInput,
TxAckOutput,
TxAckPaymentRequest,
TxAckPrevExtraData,
TxAckPrevInput,
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):
def __init__(self, description: str, txid: bytes):
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))
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
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
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
tx_req.request_type = RequestType.TXFINISHED
yield None, tx_req
@ -459,3 +492,13 @@ def sanitize_tx_output(txo: TxOutput, coin: CoinInfo) -> TxOutput:
raise wire.DataError("Missing orig_index field.")
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 ubinascii import hexlify
from trezor import utils
from trezor import ui, utils, wire
from trezor.enums import AmountUnit, ButtonRequestType, OutputScriptType
from trezor.strings import format_amount, format_timestamp
from trezor.ui import layouts
@ -15,8 +15,9 @@ if not utils.BITCOIN_ONLY:
if TYPE_CHECKING:
from trezor import wire
from trezor.messages import TxOutput
from typing import Any
from trezor.messages import TxAckPaymentRequest, TxOutput
from trezor.ui.layouts import LayoutType
from apps.common.coininfo import CoinInfo
@ -66,8 +67,19 @@ async def confirm_output(
else:
assert output.address is not None
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(
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
@ -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:
await layouts.confirm_replacement(
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"
if msg_type == MessageType.ChangeWipeCode:
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:
return "apps.management.sd_protect"

@ -20,7 +20,7 @@ from ...components.common.confirm import (
)
from ...components.tt import passphrase, pin
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 (
PAGEBREAK,
AskPaginated,
@ -39,7 +39,7 @@ from ...constants.tt import (
from ..common import button_request, interact
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 ...components.tt.button import ButtonContent
@ -62,6 +62,7 @@ __all__ = (
"show_xpub",
"show_warning",
"confirm_output",
"confirm_payment_request",
"confirm_blob",
"confirm_properties",
"confirm_total",
@ -496,6 +497,7 @@ async def confirm_output(
width: int = MONO_ADDR_PER_LINE,
width_paginated: int = MONO_ADDR_PER_LINE - 1,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
icon: str = ui.ICON_SEND,
) -> None:
header_lines = to_str.count("\n") + int(subtitle is not None)
if len(address) > (TEXT_MAX_LINES - header_lines) * width:
@ -506,9 +508,9 @@ async def confirm_output(
if to_paginated:
para.append((ui.NORMAL, "to"))
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:
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:
text.normal(subtitle, "\n")
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))
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(
ctx: wire.GenericContext,
title: str,

@ -2,17 +2,22 @@ from common import unittest, await_result, H_
import storage.cache
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 TxInput
from trezor.messages import TxOutput
from trezor.messages import SignTx
from trezor.messages import TxAckPaymentRequest
from trezor.enums import InputScriptType, OutputScriptType
from trezor.utils import HashWriter
from apps.common import coins
from apps.bitcoin.authorization import CoinJoinAuthorization
from apps.bitcoin.sign_tx.approvers import CoinJoinApprover
from apps.bitcoin.sign_tx.bitcoin import Bitcoin
from apps.bitcoin.sign_tx.tx_info import TxInfo
from apps.bitcoin import writers
class TestApprover(unittest.TestCase):
@ -20,9 +25,10 @@ class TestApprover(unittest.TestCase):
def setUp(self):
self.coin = coins.by_name('Bitcoin')
self.fee_per_anonymity_percent = 0.003
self.coordinator_name = "www.example.com"
self.msg_auth = AuthorizeCoinJoin(
coordinator="www.example.com",
coordinator=self.coordinator_name,
max_total_fee=40000,
fee_per_anonymity=int(self.fee_per_anonymity_percent * 10**9),
address_n=[H_(84), H_(0), H_(0)],
@ -62,8 +68,10 @@ class TestApprover(unittest.TestCase):
# Other's CoinJoined outputs.
outputs = [
TxOutput(
address="",
amount=denomination,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99)
]
@ -71,9 +79,11 @@ class TestApprover(unittest.TestCase):
outputs.insert(
40,
TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 0, 2],
amount=denomination,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)
)
@ -84,25 +94,31 @@ class TestApprover(unittest.TestCase):
# Other's change-outputs.
outputs.extend(
TxOutput(
address="",
amount=1000000 * (i + 1) - fees,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
) for i in range(99)
)
# Our change-output.
outputs.append(
TxOutput(
address="",
address_n=[H_(84), H_(0), H_(0), 1, 1],
amount=1000000 - fees,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)
)
# Coordinator's output.
outputs.append(
TxOutput(
address="",
amount=total_coordinator_fee,
script_type=OutputScriptType.PAYTOWITNESS,
payment_req_index=0,
)
)
@ -111,12 +127,34 @@ class TestApprover(unittest.TestCase):
approver = CoinJoinApprover(tx, self.coin, authorization)
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:
if txi.script_type == InputScriptType.EXTERNAL:
approver.add_external_input(txi)
else:
await_result(approver.add_internal_input(txi))
await_result(approver.add_payment_request(tx_ack_payment_req, None))
for txo in outputs:
if txo.address_n:
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
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
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
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
### Pseudo-code

Loading…
Cancel
Save