1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-29 09:38:08 +00:00

refactor(change): factor out ChangeDetector

This commit is contained in:
Ondřej Vejpustek 2024-11-12 12:52:40 +01:00
parent 4d4ab93197
commit 5aa9e644be
4 changed files with 78 additions and 52 deletions

View File

@ -271,6 +271,8 @@ apps.bitcoin.sign_tx.bitcoin
import apps.bitcoin.sign_tx.bitcoin import apps.bitcoin.sign_tx.bitcoin
apps.bitcoin.sign_tx.bitcoinlike apps.bitcoin.sign_tx.bitcoinlike
import apps.bitcoin.sign_tx.bitcoinlike import apps.bitcoin.sign_tx.bitcoinlike
apps.bitcoin.sign_tx.change_detector
import apps.bitcoin.sign_tx.change_detector
apps.bitcoin.sign_tx.decred apps.bitcoin.sign_tx.decred
import apps.bitcoin.sign_tx.decred import apps.bitcoin.sign_tx.decred
apps.bitcoin.sign_tx.helpers apps.bitcoin.sign_tx.helpers

View File

@ -227,7 +227,9 @@ class BasicApprover(Approver):
"Adding new OP_RETURN outputs in replacement transactions is not supported." "Adding new OP_RETURN outputs in replacement transactions is not supported."
) )
elif txo.payment_req_index is None or self.show_payment_req_details: elif txo.payment_req_index is None or self.show_payment_req_details:
source_path = tx_info.wallet_path.get_path() if tx_info else None source_path = (
tx_info.change_detector.wallet_path.get_path() if tx_info else None
)
# Ask user to confirm output, unless it is part of a payment # Ask user to confirm output, unless it is part of a payment
# request, which gets confirmed separately. # request, which gets confirmed separately.
await helpers.confirm_output( await helpers.confirm_output(
@ -294,7 +296,7 @@ class BasicApprover(Approver):
if self.has_unverified_external_input: if self.has_unverified_external_input:
await helpers.confirm_unverified_external_input() await helpers.confirm_unverified_external_input()
if tx_info.wallet_path.get_path() is None: if tx_info.change_detector.wallet_path.get_path() is None:
await helpers.confirm_multiple_accounts() await helpers.confirm_multiple_accounts()
fee = self.total_in - self.total_out fee = self.total_in - self.total_out
@ -390,7 +392,7 @@ class BasicApprover(Approver):
fee_rate, fee_rate,
coin, coin,
amount_unit, amount_unit,
tx_info.wallet_path.get_path(), tx_info.change_detector.wallet_path.get_path(),
) )
else: else:
await helpers.confirm_joint_total(spending, total, coin, amount_unit) await helpers.confirm_joint_total(spending, total, coin, amount_unit)

View File

@ -0,0 +1,65 @@
from micropython import const
from typing import TYPE_CHECKING
from .. import common
if TYPE_CHECKING:
from trezor.messages import TxInput, TxOutput
# The chain id used for change.
_BIP32_CHANGE_CHAIN = const(1)
# The maximum allowed change address. This should be large enough for normal
# use and still allow to quickly brute-force the correct BIP32 path.
_BIP32_MAX_LAST_ELEMENT = const(1_000_000)
class ChangeDetector:
def __init__(self) -> None:
from .matchcheck import (
MultisigFingerprintChecker,
ScriptTypeChecker,
WalletPathChecker,
)
# Checksum of multisig inputs, used to validate change-output.
self.multisig_fingerprint = MultisigFingerprintChecker()
# Common prefix of input paths, used to validate change-output.
self.wallet_path = WalletPathChecker()
# Common script type, used to validate change-output.
self.script_type = ScriptTypeChecker()
def add_input(self, txi: TxInput) -> None:
if not common.input_is_external(txi):
self.wallet_path.add_input(txi)
self.script_type.add_input(txi)
self.multisig_fingerprint.add_input(txi)
def check_input(self, txi: TxInput) -> None:
self.wallet_path.check_input(txi)
self.script_type.check_input(txi)
self.multisig_fingerprint.check_input(txi)
def output_is_change(self, txo: TxOutput) -> bool:
if txo.script_type not in common.CHANGE_OUTPUT_SCRIPT_TYPES:
return False
# Check the multisig fingerprint only for multisig outputs. This means
# that a transfer from a multisig account to a singlesig account is
# treated as a change-output as long as all other change-output
# conditions are satisfied. This goes a bit against the concept of a
# multisig account but the other cosigners will notice that they are
# relinquishing control of the funds, so there is no security risk.
if txo.multisig and not self.multisig_fingerprint.output_matches(txo):
return False
return (
self.wallet_path.output_matches(txo)
and self.script_type.output_matches(txo)
and len(txo.address_n) >= common.BIP32_WALLET_DEPTH
and txo.address_n[-2] <= _BIP32_CHANGE_CHAIN
and txo.address_n[-1] <= _BIP32_MAX_LAST_ELEMENT
and txo.amount > 0
)

View File

@ -1,7 +1,7 @@
from micropython import const from micropython import const
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .. import common, writers from .. import writers
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Protocol from typing import Protocol
@ -32,13 +32,6 @@ if TYPE_CHECKING:
) -> None: ... ) -> None: ...
# The chain id used for change.
_BIP32_CHANGE_CHAIN = const(1)
# The maximum allowed change address. This should be large enough for normal
# use and still allow to quickly brute-force the correct BIP32 path.
_BIP32_MAX_LAST_ELEMENT = const(1_000_000)
# Setting nSequence to this value for every input in a transaction disables nLockTime. # Setting nSequence to this value for every input in a transaction disables nLockTime.
_SEQUENCE_FINAL = const(0xFFFF_FFFF) _SEQUENCE_FINAL = const(0xFFFF_FFFF)
@ -52,20 +45,9 @@ class TxInfoBase:
from trezor.crypto.hashlib import sha256 from trezor.crypto.hashlib import sha256
from trezor.utils import HashWriter from trezor.utils import HashWriter
from .matchcheck import ( from .change_detector import ChangeDetector
MultisigFingerprintChecker,
ScriptTypeChecker,
WalletPathChecker,
)
# Checksum of multisig inputs, used to validate change-output. self.change_detector = ChangeDetector()
self.multisig_fingerprint = MultisigFingerprintChecker()
# Common prefix of input paths, used to validate change-output.
self.wallet_path = WalletPathChecker()
# Common script type, used to validate change-output.
self.script_type = ScriptTypeChecker()
# h_tx_check is used to make sure that the inputs and outputs streamed in # h_tx_check is used to make sure that the inputs and outputs streamed in
# different steps are the same every time, e.g. the ones streamed for approval # different steps are the same every time, e.g. the ones streamed for approval
@ -88,42 +70,17 @@ class TxInfoBase:
self.sig_hasher.add_input(txi, script_pubkey) self.sig_hasher.add_input(txi, script_pubkey)
writers.write_tx_input_check(self.h_tx_check, txi) writers.write_tx_input_check(self.h_tx_check, txi)
self.min_sequence = min(self.min_sequence, txi.sequence) self.min_sequence = min(self.min_sequence, txi.sequence)
self.change_detector.add_input(txi)
if not common.input_is_external(txi):
self.wallet_path.add_input(txi)
self.script_type.add_input(txi)
self.multisig_fingerprint.add_input(txi)
def add_output(self, txo: TxOutput, script_pubkey: bytes) -> None: def add_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
self.sig_hasher.add_output(txo, script_pubkey) self.sig_hasher.add_output(txo, script_pubkey)
writers.write_tx_output(self.h_tx_check, txo, script_pubkey) writers.write_tx_output(self.h_tx_check, txo, script_pubkey)
def check_input(self, txi: TxInput) -> None: def check_input(self, txi: TxInput) -> None:
self.wallet_path.check_input(txi) self.change_detector.check_input(txi)
self.script_type.check_input(txi)
self.multisig_fingerprint.check_input(txi)
def output_is_change(self, txo: TxOutput) -> bool: def output_is_change(self, txo: TxOutput) -> bool:
if txo.script_type not in common.CHANGE_OUTPUT_SCRIPT_TYPES: return self.change_detector.output_is_change(txo)
return False
# Check the multisig fingerprint only for multisig outputs. This means
# that a transfer from a multisig account to a singlesig account is
# treated as a change-output as long as all other change-output
# conditions are satisfied. This goes a bit against the concept of a
# multisig account but the other cosigners will notice that they are
# relinquishing control of the funds, so there is no security risk.
if txo.multisig and not self.multisig_fingerprint.output_matches(txo):
return False
return (
self.wallet_path.output_matches(txo)
and self.script_type.output_matches(txo)
and len(txo.address_n) >= common.BIP32_WALLET_DEPTH
and txo.address_n[-2] <= _BIP32_CHANGE_CHAIN
and txo.address_n[-1] <= _BIP32_MAX_LAST_ELEMENT
and txo.amount > 0
)
def lock_time_disabled(self) -> bool: def lock_time_disabled(self) -> bool:
return self.min_sequence == _SEQUENCE_FINAL return self.min_sequence == _SEQUENCE_FINAL