1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-08-03 04:18:17 +00:00
trezor-firmware/core/src/apps/bitcoin/common.py
2024-11-04 19:29:04 +01:00

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)