You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/apps/bitcoin/addresses.py

257 lines
9.1 KiB

from trezor import wire
from trezor.crypto import base58, cashaddr
from trezor.crypto.hashlib import sha256
from trezor.messages import InputScriptType
from trezor.messages.MultisigRedeemScriptType import MultisigRedeemScriptType
from apps.common import HARDENED, address_type, paths
from apps.common.coininfo import CoinInfo
from .common import ecdsa_hash_pubkey, encode_bech32_address
from .multisig import multisig_get_pubkeys, multisig_pubkey_index
from .scripts import output_script_multisig, output_script_native_p2wpkh_or_p2wsh
if False:
from typing import List
from trezor.crypto import bip32
from trezor.messages.TxInputType import EnumTypeInputScriptType
def get_address(
script_type: EnumTypeInputScriptType,
coin: CoinInfo,
node: bip32.HDNode,
multisig: MultisigRedeemScriptType = None,
) -> str:
if (
script_type == InputScriptType.SPENDADDRESS
or script_type == InputScriptType.SPENDMULTISIG
):
if multisig: # p2sh multisig
pubkey = node.public_key()
index = multisig_pubkey_index(multisig, pubkey)
if index is None:
raise wire.ProcessError("Public key not found")
if coin.address_type_p2sh is None:
raise wire.ProcessError("Multisig not enabled on this coin")
pubkeys = multisig_get_pubkeys(multisig)
address = address_multisig_p2sh(pubkeys, multisig.m, coin)
if coin.cashaddr_prefix is not None:
address = address_to_cashaddr(address, coin)
return address
if script_type == InputScriptType.SPENDMULTISIG:
raise wire.ProcessError("Multisig details required")
# p2pkh
address = node.address(coin.address_type)
if coin.cashaddr_prefix is not None:
address = address_to_cashaddr(address, coin)
return address
elif script_type == InputScriptType.SPENDWITNESS: # native p2wpkh or native p2wsh
if not coin.segwit or not coin.bech32_prefix:
raise wire.ProcessError("Segwit not enabled on this coin")
# native p2wsh multisig
if multisig is not None:
pubkeys = multisig_get_pubkeys(multisig)
return address_multisig_p2wsh(pubkeys, multisig.m, coin.bech32_prefix)
# native p2wpkh
return address_p2wpkh(node.public_key(), coin)
elif (
script_type == InputScriptType.SPENDP2SHWITNESS
): # p2wpkh or p2wsh nested in p2sh
if not coin.segwit or coin.address_type_p2sh is None:
raise wire.ProcessError("Segwit not enabled on this coin")
# p2wsh multisig nested in p2sh
if multisig is not None:
pubkeys = multisig_get_pubkeys(multisig)
return address_multisig_p2wsh_in_p2sh(pubkeys, multisig.m, coin)
# p2wpkh nested in p2sh
return address_p2wpkh_in_p2sh(node.public_key(), coin)
else:
raise wire.ProcessError("Invalid script type")
def address_multisig_p2sh(pubkeys: List[bytes], m: int, coin: CoinInfo) -> str:
if coin.address_type_p2sh is None:
raise wire.ProcessError("Multisig not enabled on this coin")
redeem_script = output_script_multisig(pubkeys, m)
redeem_script_hash = coin.script_hash(redeem_script)
return address_p2sh(redeem_script_hash, coin)
def address_multisig_p2wsh_in_p2sh(pubkeys: List[bytes], m: int, coin: CoinInfo) -> str:
if coin.address_type_p2sh is None:
raise wire.ProcessError("Multisig not enabled on this coin")
witness_script = output_script_multisig(pubkeys, m)
witness_script_hash = sha256(witness_script).digest()
return address_p2wsh_in_p2sh(witness_script_hash, coin)
def address_multisig_p2wsh(pubkeys: List[bytes], m: int, hrp: str) -> str:
if not hrp:
raise wire.ProcessError("Multisig not enabled on this coin")
witness_script = output_script_multisig(pubkeys, m)
witness_script_hash = sha256(witness_script).digest()
return address_p2wsh(witness_script_hash, hrp)
def address_pkh(pubkey: bytes, coin: CoinInfo) -> str:
s = address_type.tobytes(coin.address_type) + coin.script_hash(pubkey)
return base58.encode_check(bytes(s), coin.b58_hash)
def address_p2sh(redeem_script_hash: bytes, coin: CoinInfo) -> str:
s = address_type.tobytes(coin.address_type_p2sh) + redeem_script_hash
return base58.encode_check(bytes(s), coin.b58_hash)
def address_p2wpkh_in_p2sh(pubkey: bytes, coin: CoinInfo) -> str:
pubkey_hash = ecdsa_hash_pubkey(pubkey, coin)
redeem_script = output_script_native_p2wpkh_or_p2wsh(pubkey_hash)
redeem_script_hash = coin.script_hash(redeem_script)
return address_p2sh(redeem_script_hash, coin)
def address_p2wsh_in_p2sh(witness_script_hash: bytes, coin: CoinInfo) -> str:
redeem_script = output_script_native_p2wpkh_or_p2wsh(witness_script_hash)
redeem_script_hash = coin.script_hash(redeem_script)
return address_p2sh(redeem_script_hash, coin)
def address_p2wpkh(pubkey: bytes, coin: CoinInfo) -> str:
pubkeyhash = ecdsa_hash_pubkey(pubkey, coin)
return encode_bech32_address(coin.bech32_prefix, pubkeyhash)
def address_p2wsh(witness_script_hash: bytes, hrp: str) -> str:
return encode_bech32_address(hrp, witness_script_hash)
def address_to_cashaddr(address: str, coin: CoinInfo) -> str:
raw = base58.decode_check(address, coin.b58_hash)
version, data = raw[0], raw[1:]
if version == coin.address_type:
version = cashaddr.ADDRESS_TYPE_P2KH
elif version == coin.address_type_p2sh:
version = cashaddr.ADDRESS_TYPE_P2SH
else:
raise ValueError("Unknown cashaddr address type")
return cashaddr.encode(coin.cashaddr_prefix, version, data)
def address_short(coin: CoinInfo, address: str) -> str:
if coin.cashaddr_prefix is not None and address.startswith(
coin.cashaddr_prefix + ":"
):
return address[len(coin.cashaddr_prefix) + 1 :]
else:
return address
def validate_full_path(
path: list,
coin: CoinInfo,
script_type: EnumTypeInputScriptType,
validate_script_type: bool = True,
) -> bool:
"""
Validates derivation path to fit Bitcoin-like coins. We mostly use
44', but for segwit-enabled coins we use either 49' (P2WPKH-nested-in-P2SH)
or 84' (native P2WPKH). Electrum uses m/45' for legacy addresses and
m/48' for segwit, so those two are allowed as well.
See docs/coins for what paths are allowed. Please note that this is not
a comprehensive check, some nuances are omitted for simplification.
"""
if len(path) not in (4, 5, 6):
return False
if not validate_purpose(path[0], coin):
return False
if validate_script_type and not validate_purpose_against_script_type(
path[0], script_type
):
return False
if path[1] > 20 and path[1] != coin.slip44 | HARDENED:
return False
if (20 < path[2] < HARDENED) or path[2] > 20 | HARDENED:
return False
if path[3] not in (0, 1, 0 | HARDENED, 1 | HARDENED, 2 | HARDENED):
return False
if len(path) > 4 and path[4] > 1000000:
return False
if len(path) > 5 and path[5] > 1000000:
return False
return True
def validate_purpose(purpose: int, coin: CoinInfo) -> bool:
if purpose not in (
44 | HARDENED,
45 | HARDENED,
48 | HARDENED,
49 | HARDENED,
84 | HARDENED,
):
return False
if not coin.segwit and purpose not in (44 | HARDENED, 45 | HARDENED, 48 | HARDENED):
return False
return True
def validate_purpose_against_script_type(
purpose: int, script_type: EnumTypeInputScriptType
) -> bool:
"""
Validates purpose against provided input's script type:
- 44 for spending address (script_type == SPENDADDRESS)
- 45, 48 for multisig (script_type == SPENDMULTISIG)
- 49 for p2wsh-nested-in-p2sh spend (script_type == SPENDP2SHWITNESS)
- 84 for p2wsh native segwit spend (script_type == SPENDWITNESS)
"""
if purpose == 44 | HARDENED and script_type != InputScriptType.SPENDADDRESS:
return False
if purpose == 45 | HARDENED and script_type != InputScriptType.SPENDMULTISIG:
return False
if purpose == 48 | HARDENED and script_type not in (
InputScriptType.SPENDMULTISIG,
InputScriptType.SPENDP2SHWITNESS,
InputScriptType.SPENDWITNESS,
):
return False
if purpose == 49 | HARDENED and script_type != InputScriptType.SPENDP2SHWITNESS:
return False
if purpose == 84 | HARDENED and script_type != InputScriptType.SPENDWITNESS:
return False
return True
def validate_path_for_bitcoin_public_key(path: list, coin: CoinInfo) -> bool:
"""
Validates derivation path to fit Bitcoin-like coins for GetPublicKey.
"""
length = len(path)
if length < 3 or length > 5:
return False
if not validate_purpose(path[0], coin):
return False
if path[1] != coin.slip44 | HARDENED:
return False
if path[2] < HARDENED or path[2] > 20 | HARDENED:
return False
if length > 3 and paths.is_hardened(path[3]):
return False
if length > 4 and paths.is_hardened(path[4]):
return False
return True