mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-05-06 08:59:15 +00:00
feat(core): Allow decreasing output amount in RBF transactions.
This commit is contained in:
parent
5593862dc1
commit
b10acbe153
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Decred staking. [#1249]
|
- Decred staking. [#1249]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Allow decreasing the output value in RBF transactions. [#1491]
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
@ -376,3 +377,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[#1415]: https://github.com/trezor/trezor-firmware/pull/1415
|
[#1415]: https://github.com/trezor/trezor-firmware/pull/1415
|
||||||
[#1467]: https://github.com/trezor/trezor-firmware/issues/1467
|
[#1467]: https://github.com/trezor/trezor-firmware/issues/1467
|
||||||
[#1518]: https://github.com/trezor/trezor-firmware/pull/1518
|
[#1518]: https://github.com/trezor/trezor-firmware/pull/1518
|
||||||
|
[#1491]: https://github.com/trezor/trezor-firmware/issues/1491
|
||||||
|
@ -44,6 +44,12 @@ class Approver:
|
|||||||
|
|
||||||
self.amount_unit = tx.amount_unit
|
self.amount_unit = tx.amount_unit
|
||||||
|
|
||||||
|
def is_payjoin(self) -> bool:
|
||||||
|
# A PayJoin is a replacement transaction which manipulates the external inputs of the
|
||||||
|
# original transaction. A replacement transaction is not allowed to remove any inputs from
|
||||||
|
# the original, so the condition below is equivalent to external_in > orig_external_in.
|
||||||
|
return self.external_in != self.orig_external_in
|
||||||
|
|
||||||
async def add_internal_input(self, txi: TxInput) -> None:
|
async def add_internal_input(self, txi: TxInput) -> None:
|
||||||
self.weight.add_input(txi)
|
self.weight.add_input(txi)
|
||||||
self.total_in += txi.amount
|
self.total_in += txi.amount
|
||||||
@ -79,6 +85,11 @@ class Approver:
|
|||||||
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
|
||||||
|
|
||||||
|
async def approve_orig_txids(
|
||||||
|
self, tx_info: TxInfo, orig_txs: List[OriginalTxInfo]
|
||||||
|
) -> None:
|
||||||
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -109,11 +120,28 @@ class BasicApprover(Approver):
|
|||||||
) -> None:
|
) -> None:
|
||||||
await super().add_external_output(txo, script_pubkey, orig_txo)
|
await super().add_external_output(txo, script_pubkey, orig_txo)
|
||||||
|
|
||||||
# Replacement transactions must not decrease the value of any external outputs.
|
if orig_txo:
|
||||||
if orig_txo and txo.amount < orig_txo.amount:
|
if txo.amount < orig_txo.amount:
|
||||||
raise wire.ProcessError(
|
# Replacement transactions may need to decrease the value of external outputs to
|
||||||
"Reducing original output amounts is not supported."
|
# bump the fee. This is needed if the original transaction transfers the entire
|
||||||
)
|
# account balance ("Send Max").
|
||||||
|
if self.is_payjoin():
|
||||||
|
# In case of PayJoin the above could be used to increase other external
|
||||||
|
# outputs, which would create too much UI complexity.
|
||||||
|
raise wire.ProcessError(
|
||||||
|
"Reducing original output amounts is not supported."
|
||||||
|
)
|
||||||
|
await helpers.confirm_modify_output(
|
||||||
|
txo, orig_txo, self.coin, self.amount_unit
|
||||||
|
)
|
||||||
|
elif txo.amount > orig_txo.amount:
|
||||||
|
# PayJoin transactions may increase the value of external outputs without
|
||||||
|
# confirmation, because approve_tx() together with the branch above ensures that
|
||||||
|
# the increase is paid by external inputs.
|
||||||
|
if not self.is_payjoin():
|
||||||
|
raise wire.ProcessError(
|
||||||
|
"Increasing original output amounts is not supported."
|
||||||
|
)
|
||||||
|
|
||||||
if self.orig_total_in:
|
if self.orig_total_in:
|
||||||
# Skip output confirmation for replacement transactions,
|
# Skip output confirmation for replacement transactions,
|
||||||
@ -125,6 +153,26 @@ class BasicApprover(Approver):
|
|||||||
else:
|
else:
|
||||||
await helpers.confirm_output(txo, self.coin, self.amount_unit)
|
await helpers.confirm_output(txo, self.coin, self.amount_unit)
|
||||||
|
|
||||||
|
async def approve_orig_txids(
|
||||||
|
self, tx_info: TxInfo, orig_txs: List[OriginalTxInfo]
|
||||||
|
) -> None:
|
||||||
|
if not orig_txs:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_payjoin():
|
||||||
|
description = "PayJoin"
|
||||||
|
elif tx_info.rbf_disabled() and any(
|
||||||
|
not orig.rbf_disabled() for orig in orig_txs
|
||||||
|
):
|
||||||
|
description = "Finalize transaction"
|
||||||
|
elif len(orig_txs) > 1:
|
||||||
|
description = "Meld transactions"
|
||||||
|
else:
|
||||||
|
description = "Update transaction"
|
||||||
|
|
||||||
|
for orig in orig_txs:
|
||||||
|
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:
|
||||||
fee = self.total_in - self.total_out
|
fee = self.total_in - self.total_out
|
||||||
|
|
||||||
@ -175,30 +223,25 @@ class BasicApprover(Approver):
|
|||||||
"Original transactions must have same effective nLockTime as replacement transaction."
|
"Original transactions must have same effective nLockTime as replacement transaction."
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.external_in > self.orig_external_in:
|
if not self.is_payjoin():
|
||||||
description = "PayJoin"
|
# Not a PayJoin: Show the actual fee difference, since any difference in the fee is
|
||||||
elif tx_info.rbf_disabled() and any(
|
# coming entirely from the user's own funds and from decreases of external outputs.
|
||||||
not orig.rbf_disabled() for orig in orig_txs
|
# We consider the decreases as belonging to the user.
|
||||||
):
|
await helpers.confirm_modify_fee(
|
||||||
description = "Finalize transaction"
|
fee - orig_fee, fee, self.coin, self.amount_unit
|
||||||
elif len(orig_txs) > 1:
|
)
|
||||||
description = "Transaction meld"
|
elif spending > orig_spending:
|
||||||
else:
|
# PayJoin and user is spending more: Show the increase in the user's contribution
|
||||||
description = "Fee modification"
|
# to the fee, ignoring any contribution from external inputs. Decreasing of
|
||||||
|
# external outputs is not allowed in PayJoin, so there is no need to handle those.
|
||||||
for orig in orig_txs:
|
|
||||||
await helpers.confirm_replacement(description, orig.orig_hash)
|
|
||||||
|
|
||||||
# Always ask the user to confirm when they are paying more towards the fee.
|
|
||||||
# If they are not spending more, then ask for confirmation only if it's not
|
|
||||||
# a PayJoin. In complex scenarios where the user is not spending more and
|
|
||||||
# there are new external inputs the scenario can be open to multiple
|
|
||||||
# interpretations and the dialog would likely cause more confusion than
|
|
||||||
# what it's worth, see PR #1292.
|
|
||||||
if spending > orig_spending or self.external_in == self.orig_external_in:
|
|
||||||
await helpers.confirm_modify_fee(
|
await helpers.confirm_modify_fee(
|
||||||
spending - orig_spending, fee, self.coin, self.amount_unit
|
spending - orig_spending, fee, self.coin, self.amount_unit
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PayJoin and user is not spending more: When new external inputs are involved and
|
||||||
|
# the user is paying less, the scenario can be open to multiple interpretations and
|
||||||
|
# the dialog would likely cause more confusion than what it's worth, see PR #1292.
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
# Standard transaction.
|
# Standard transaction.
|
||||||
if tx_info.tx.lock_time > 0:
|
if tx_info.tx.lock_time > 0:
|
||||||
@ -263,6 +306,11 @@ class CoinJoinApprover(Approver):
|
|||||||
await super().add_external_output(txo, script_pubkey, orig_txo)
|
await super().add_external_output(txo, script_pubkey, orig_txo)
|
||||||
self._add_output(txo, script_pubkey)
|
self._add_output(txo, script_pubkey)
|
||||||
|
|
||||||
|
async def approve_orig_txids(
|
||||||
|
self, tx_info: TxInfo, orig_txs: List[OriginalTxInfo]
|
||||||
|
) -> None:
|
||||||
|
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:
|
||||||
# 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
|
||||||
|
@ -44,6 +44,9 @@ class Bitcoin:
|
|||||||
# Add inputs to hash143 and h_tx_check and compute the sum of input amounts.
|
# Add inputs to hash143 and h_tx_check and compute the sum of input amounts.
|
||||||
await self.step1_process_inputs()
|
await self.step1_process_inputs()
|
||||||
|
|
||||||
|
# Approve the original TXIDs in case of a replacement transaction.
|
||||||
|
await self.approver.approve_orig_txids(self.tx_info, self.orig_txs)
|
||||||
|
|
||||||
# Add outputs to hash143 and h_tx_check, approve outputs and compute
|
# Add outputs to hash143 and h_tx_check, approve outputs and compute
|
||||||
# sum of output amounts.
|
# sum of output amounts.
|
||||||
await self.step2_approve_outputs()
|
await self.step2_approve_outputs()
|
||||||
|
@ -85,6 +85,27 @@ class UiConfirmReplacement(UiConfirm):
|
|||||||
__eq__ = utils.obj_eq
|
__eq__ = utils.obj_eq
|
||||||
|
|
||||||
|
|
||||||
|
class UiConfirmModifyOutput(UiConfirm):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
txo: TxOutput,
|
||||||
|
orig_txo: TxOutput,
|
||||||
|
coin: CoinInfo,
|
||||||
|
amount_unit: EnumTypeAmountUnit,
|
||||||
|
):
|
||||||
|
self.txo = txo
|
||||||
|
self.orig_txo = orig_txo
|
||||||
|
self.coin = coin
|
||||||
|
self.amount_unit = amount_unit
|
||||||
|
|
||||||
|
def confirm_dialog(self, ctx: wire.Context) -> Awaitable[Any]:
|
||||||
|
return layout.confirm_modify_output(
|
||||||
|
ctx, self.txo, self.orig_txo, self.coin, self.amount_unit
|
||||||
|
)
|
||||||
|
|
||||||
|
__eq__ = utils.obj_eq
|
||||||
|
|
||||||
|
|
||||||
class UiConfirmModifyFee(UiConfirm):
|
class UiConfirmModifyFee(UiConfirm):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -199,6 +220,10 @@ def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # typ
|
|||||||
return (yield UiConfirmReplacement(description, txid))
|
return (yield UiConfirmReplacement(description, txid))
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_modify_output(txo: TxOutput, orig_txo: TxOutput, coin: CoinInfo, amount_unit: EnumTypeAmountUnit) -> Awaitable[Any]: # type: ignore
|
||||||
|
return (yield UiConfirmModifyOutput(txo, orig_txo, coin, amount_unit))
|
||||||
|
|
||||||
|
|
||||||
def confirm_modify_fee(user_fee_change: int, total_fee_new: int, coin: CoinInfo, amount_unit: EnumTypeAmountUnit) -> Awaitable[Any]: # type: ignore
|
def confirm_modify_fee(user_fee_change: int, total_fee_new: int, coin: CoinInfo, amount_unit: EnumTypeAmountUnit) -> Awaitable[Any]: # type: ignore
|
||||||
return (yield UiConfirmModifyFee(user_fee_change, total_fee_new, coin, amount_unit))
|
return (yield UiConfirmModifyFee(user_fee_change, total_fee_new, coin, amount_unit))
|
||||||
|
|
||||||
|
@ -96,6 +96,27 @@ async def confirm_replacement(ctx: wire.Context, description: str, txid: bytes)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_modify_output(
|
||||||
|
ctx: wire.Context,
|
||||||
|
txo: TxOutput,
|
||||||
|
orig_txo: TxOutput,
|
||||||
|
coin: CoinInfo,
|
||||||
|
amount_unit: EnumTypeAmountUnit,
|
||||||
|
) -> None:
|
||||||
|
assert txo.address is not None
|
||||||
|
address_short = addresses.address_short(coin, txo.address)
|
||||||
|
amount_change = txo.amount - orig_txo.amount
|
||||||
|
await require(
|
||||||
|
layouts.confirm_modify_output(
|
||||||
|
ctx,
|
||||||
|
address_short,
|
||||||
|
amount_change,
|
||||||
|
format_coin_amount(abs(amount_change), coin, amount_unit),
|
||||||
|
format_coin_amount(txo.amount, coin, amount_unit),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def confirm_modify_fee(
|
async def confirm_modify_fee(
|
||||||
ctx: wire.Context,
|
ctx: wire.Context,
|
||||||
user_fee_change: int,
|
user_fee_change: int,
|
||||||
|
@ -50,6 +50,7 @@ __all__ = (
|
|||||||
"confirm_joint_total",
|
"confirm_joint_total",
|
||||||
"confirm_metadata",
|
"confirm_metadata",
|
||||||
"confirm_replacement",
|
"confirm_replacement",
|
||||||
|
"confirm_modify_output",
|
||||||
"confirm_modify_fee",
|
"confirm_modify_fee",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -407,13 +408,43 @@ def confirm_replacement(
|
|||||||
return interact(ctx, Confirm(text), "confirm_replacement", ButtonRequestType.SignTx)
|
return interact(ctx, Confirm(text), "confirm_replacement", ButtonRequestType.SignTx)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_modify_output(
|
||||||
|
ctx: wire.GenericContext,
|
||||||
|
address: str,
|
||||||
|
sign: int,
|
||||||
|
amount_change: str,
|
||||||
|
amount_new: str,
|
||||||
|
) -> LayoutType:
|
||||||
|
page1 = Text("Modify amount", ui.ICON_SEND, ui.GREEN)
|
||||||
|
page1.normal("Address:")
|
||||||
|
page1.br_half()
|
||||||
|
page1.mono(*_split_address(address))
|
||||||
|
|
||||||
|
page2 = Text("Modify amount", ui.ICON_SEND, ui.GREEN)
|
||||||
|
if sign < 0:
|
||||||
|
page2.normal("Decrease amount by:")
|
||||||
|
else:
|
||||||
|
page2.normal("Increase amount by:")
|
||||||
|
page2.bold(amount_change)
|
||||||
|
page2.br_half()
|
||||||
|
page2.normal("New amount:")
|
||||||
|
page2.bold(amount_new)
|
||||||
|
|
||||||
|
return interact(
|
||||||
|
ctx,
|
||||||
|
Paginated([page1, Confirm(page2)]),
|
||||||
|
"modify_output",
|
||||||
|
ButtonRequestType.ConfirmOutput,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def confirm_modify_fee(
|
def confirm_modify_fee(
|
||||||
ctx: wire.GenericContext,
|
ctx: wire.GenericContext,
|
||||||
sign: int,
|
sign: int,
|
||||||
user_fee_change: str,
|
user_fee_change: str,
|
||||||
total_fee_new: str,
|
total_fee_new: str,
|
||||||
) -> LayoutType:
|
) -> LayoutType:
|
||||||
text = Text("Fee modification", ui.ICON_SEND, ui.GREEN)
|
text = Text("Modify fee", ui.ICON_SEND, ui.GREEN)
|
||||||
if sign == 0:
|
if sign == 0:
|
||||||
text.normal("Your fee did not change.")
|
text.normal("Your fee did not change.")
|
||||||
else:
|
else:
|
||||||
|
Loading…
Reference in New Issue
Block a user