mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-08-03 04:18:17 +00:00
274 lines
8.3 KiB
Python
274 lines
8.3 KiB
Python
from micropython import const
|
|
from typing import TYPE_CHECKING
|
|
|
|
from trezor import wire
|
|
from trezor.crypto import bech32
|
|
from trezor.crypto.curve import bip340
|
|
from trezor.enums import InputScriptType, OutputScriptType
|
|
|
|
if TYPE_CHECKING:
|
|
from enum import IntEnum
|
|
|
|
from trezor.crypto import bip32
|
|
from trezor.messages import TxInput
|
|
from trezor.utils import HashWriter
|
|
|
|
from apps.common.coininfo import CoinInfo
|
|
else:
|
|
IntEnum = object
|
|
|
|
|
|
BITCOIN_NAMES = ("Bitcoin", "Regtest", "Testnet")
|
|
|
|
|
|
class SigHashType(IntEnum):
|
|
"""Enumeration type listing the supported signature hash types.
|
|
|
|
Class constants defined below don't need to be used in the code.
|
|
They are a list of all allowed incoming sighash types.
|
|
"""
|
|
|
|
# Signature hash type with the same semantics as SIGHASH_ALL, but instead
|
|
# of having to include the byte in the signature, it is implied.
|
|
SIGHASH_ALL_TAPROOT = 0x00
|
|
|
|
# Default signature hash type in Bitcoin which signs all inputs and all
|
|
# outputs of the transaction.
|
|
SIGHASH_ALL = 0x01
|
|
|
|
# Signature hash flag used in some Bitcoin-like altcoins for replay
|
|
# protection.
|
|
SIGHASH_FORKID = 0x40
|
|
|
|
# Signature hash type with the same semantics as SIGHASH_ALL. Used in some
|
|
# Bitcoin-like altcoins for replay protection.
|
|
SIGHASH_ALL_FORKID = 0x41
|
|
|
|
@classmethod
|
|
def from_int(cls, sighash_type: int) -> "SigHashType":
|
|
val: SigHashType
|
|
for val in cls.__dict__.values():
|
|
if val == sighash_type:
|
|
return val
|
|
raise ValueError("Unsupported sighash type.")
|
|
|
|
|
|
# The number of bip32 levels used in a wallet (chain and address)
|
|
BIP32_WALLET_DEPTH = const(2)
|
|
|
|
# Bitcoin opcodes
|
|
OP_0 = const(0x00)
|
|
OP_1 = const(0x51)
|
|
|
|
# supported witness versions for bech32 addresses
|
|
_BECH32_WITVERS = (0, 1)
|
|
|
|
MULTISIG_INPUT_SCRIPT_TYPES = (
|
|
InputScriptType.SPENDMULTISIG,
|
|
InputScriptType.SPENDP2SHWITNESS,
|
|
InputScriptType.SPENDWITNESS,
|
|
)
|
|
MULTISIG_OUTPUT_SCRIPT_TYPES = (
|
|
OutputScriptType.PAYTOMULTISIG,
|
|
OutputScriptType.PAYTOP2SHWITNESS,
|
|
OutputScriptType.PAYTOWITNESS,
|
|
)
|
|
|
|
CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES: dict[OutputScriptType, InputScriptType] = {
|
|
OutputScriptType.PAYTOADDRESS: InputScriptType.SPENDADDRESS,
|
|
OutputScriptType.PAYTOMULTISIG: InputScriptType.SPENDMULTISIG,
|
|
OutputScriptType.PAYTOP2SHWITNESS: InputScriptType.SPENDP2SHWITNESS,
|
|
OutputScriptType.PAYTOWITNESS: InputScriptType.SPENDWITNESS,
|
|
OutputScriptType.PAYTOTAPROOT: InputScriptType.SPENDTAPROOT,
|
|
}
|
|
|
|
INTERNAL_INPUT_SCRIPT_TYPES = tuple(CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES.values())
|
|
CHANGE_OUTPUT_SCRIPT_TYPES = tuple(CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES.keys())
|
|
|
|
SEGWIT_INPUT_SCRIPT_TYPES = (
|
|
InputScriptType.SPENDP2SHWITNESS,
|
|
InputScriptType.SPENDWITNESS,
|
|
InputScriptType.SPENDTAPROOT,
|
|
)
|
|
|
|
SEGWIT_OUTPUT_SCRIPT_TYPES = (
|
|
OutputScriptType.PAYTOP2SHWITNESS,
|
|
OutputScriptType.PAYTOWITNESS,
|
|
OutputScriptType.PAYTOTAPROOT,
|
|
)
|
|
|
|
NONSEGWIT_INPUT_SCRIPT_TYPES = (
|
|
InputScriptType.SPENDADDRESS,
|
|
InputScriptType.SPENDMULTISIG,
|
|
)
|
|
|
|
|
|
def ecdsa_sign(node: bip32.HDNode, digest: bytes) -> bytes:
|
|
from trezor.crypto import der
|
|
from trezor.crypto.curve import secp256k1
|
|
|
|
sig = secp256k1.sign(node.private_key(), digest)
|
|
sigder = der.encode_seq((sig[1:33], sig[33:65]))
|
|
return sigder
|
|
|
|
|
|
def bip340_sign(node: bip32.HDNode, digest: bytes) -> bytes:
|
|
internal_private_key = node.private_key()
|
|
output_private_key = bip340.tweak_secret_key(internal_private_key)
|
|
return bip340.sign(output_private_key, digest)
|
|
|
|
|
|
def ecdsa_hash_pubkey(pubkey: bytes, coin: CoinInfo) -> bytes:
|
|
from trezor.utils import ensure
|
|
|
|
ensure(
|
|
coin.curve_name.startswith("secp256k1")
|
|
) # The following code makes sense only for Weiersrass curves
|
|
|
|
if pubkey[0] == 0x04:
|
|
ensure(len(pubkey) == 65) # uncompressed format
|
|
elif pubkey[0] == 0x00:
|
|
ensure(len(pubkey) == 1) # point at infinity
|
|
else:
|
|
ensure(len(pubkey) == 33) # compresssed format
|
|
|
|
return coin.script_hash(pubkey).digest()
|
|
|
|
|
|
def encode_bech32_address(prefix: str, witver: int, script: bytes) -> str:
|
|
assert witver in _BECH32_WITVERS
|
|
address = bech32.encode(prefix, witver, script)
|
|
if address is None:
|
|
raise wire.ProcessError("Invalid address")
|
|
return address
|
|
|
|
|
|
def decode_bech32_address(prefix: str, address: str) -> tuple[int, bytes]:
|
|
witver, raw = bech32.decode(prefix, address)
|
|
if witver not in _BECH32_WITVERS:
|
|
raise wire.DataError("Invalid address witness program")
|
|
assert witver is not None
|
|
assert raw is not None
|
|
# check that P2TR address encodes a valid BIP340 public key
|
|
if witver == 1 and not bip340.verify_publickey(raw):
|
|
raise wire.DataError("Invalid Taproot witness program")
|
|
return witver, raw
|
|
|
|
|
|
def input_is_segwit(txi: TxInput) -> bool:
|
|
# Note that we don't investigate whether external inputs that are not presigned
|
|
# are SegWit or not. For practical purposes we count them as SegWit, because
|
|
# they behave as such, i.e. they don't use get_legacy_tx_digest().
|
|
return txi.script_type in SEGWIT_INPUT_SCRIPT_TYPES or (
|
|
txi.script_type == InputScriptType.EXTERNAL
|
|
and bool(txi.witness or not txi.script_sig)
|
|
)
|
|
|
|
|
|
def input_is_taproot(txi: TxInput) -> bool:
|
|
if txi.script_type == InputScriptType.SPENDTAPROOT:
|
|
return True
|
|
|
|
if txi.script_type == InputScriptType.EXTERNAL:
|
|
assert txi.script_pubkey is not None
|
|
if txi.script_pubkey[0] == OP_1:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def input_is_external(txi: TxInput) -> bool:
|
|
return txi.script_type == InputScriptType.EXTERNAL
|
|
|
|
|
|
def input_is_external_unverified(txi: TxInput) -> bool:
|
|
return (
|
|
txi.script_type == InputScriptType.EXTERNAL
|
|
and txi.ownership_proof is None
|
|
and txi.witness is None
|
|
and txi.script_sig is None
|
|
)
|
|
|
|
|
|
def sanitize_input_script_type(coin: CoinInfo, script_type: InputScriptType) -> None:
|
|
if script_type in SEGWIT_INPUT_SCRIPT_TYPES and not coin.segwit:
|
|
raise wire.DataError("Segwit not enabled on this coin.")
|
|
|
|
if script_type == InputScriptType.SPENDTAPROOT and not coin.taproot:
|
|
raise wire.DataError("Taproot not enabled on this coin")
|
|
|
|
|
|
def tagged_hashwriter(tag: bytes) -> HashWriter:
|
|
from trezor.crypto.hashlib import sha256
|
|
from trezor.utils import HashWriter
|
|
|
|
tag_digest = sha256(tag).digest()
|
|
ctx = sha256(tag_digest)
|
|
ctx.update(tag_digest)
|
|
return HashWriter(ctx)
|
|
|
|
|
|
def format_fee_rate(
|
|
fee_rate: float, coin: CoinInfo, include_shortcut: bool = False
|
|
) -> str:
|
|
from trezor.strings import format_amount
|
|
|
|
# Use format_amount to get correct thousands separator -- micropython's built-in
|
|
# formatting doesn't add thousands sep to floating point numbers.
|
|
# We multiply by 100 to get a fixed-point integer with two decimal places,
|
|
# and add 0.5 to round to the nearest integer.
|
|
fee_rate_formatted = format_amount(int(fee_rate * 100 + 0.5), 2)
|
|
|
|
if include_shortcut and coin.coin_shortcut != "BTC":
|
|
shortcut = " " + coin.coin_shortcut
|
|
else:
|
|
shortcut = ""
|
|
|
|
return f"{fee_rate_formatted} sat{shortcut}/{'v' if coin.segwit else ''}B"
|
|
|
|
|
|
# function copied from python/src/trezorlib/tools.py
|
|
def descriptor_checksum(desc: str) -> str:
|
|
def _polymod(c: int, val: int) -> int:
|
|
c0 = c >> 35
|
|
c = ((c & 0x7FFFFFFFF) << 5) ^ val
|
|
if c0 & 1:
|
|
c ^= 0xF5DEE51989
|
|
if c0 & 2:
|
|
c ^= 0xA9FDCA3312
|
|
if c0 & 4:
|
|
c ^= 0x1BAB10E32D
|
|
if c0 & 8:
|
|
c ^= 0x3706B1677A
|
|
if c0 & 16:
|
|
c ^= 0x644D626FFD
|
|
return c
|
|
|
|
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
|
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
|
|
c = 1
|
|
cls = 0
|
|
clscount = 0
|
|
for ch in desc:
|
|
pos = INPUT_CHARSET.find(ch)
|
|
if pos == -1:
|
|
return ""
|
|
c = _polymod(c, pos & 31)
|
|
cls = cls * 3 + (pos >> 5)
|
|
clscount += 1
|
|
if clscount == 3:
|
|
c = _polymod(c, cls)
|
|
cls = 0
|
|
clscount = 0
|
|
if clscount > 0:
|
|
c = _polymod(c, cls)
|
|
for j in range(0, 8):
|
|
c = _polymod(c, 0)
|
|
c ^= 1
|
|
|
|
ret = [""] * 8
|
|
for j in range(0, 8):
|
|
ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31]
|
|
return "".join(ret)
|