Cardano shelley update 2/3 (#1112)
parent
e1615e60ec
commit
d2c1624602
@ -1,145 +1,352 @@
|
||||
from trezor import log, wire
|
||||
from trezor.crypto import base58, crc, hashlib
|
||||
from micropython import const
|
||||
|
||||
from apps.common import HARDENED, cbor
|
||||
from trezor import wire
|
||||
from trezor.crypto import base58, hashlib
|
||||
from trezor.messages import CardanoAddressParametersType, CardanoAddressType
|
||||
|
||||
from apps.common import HARDENED
|
||||
from apps.common.seed import remove_ed25519_prefix
|
||||
|
||||
from . import protocol_magics
|
||||
from .byron_address import derive_byron_address, validate_output_byron_address
|
||||
from .helpers import INVALID_ADDRESS, NETWORK_MISMATCH, bech32, network_ids, purposes
|
||||
from .helpers.utils import variable_length_encode
|
||||
from .seed import is_byron_path, is_shelley_path
|
||||
|
||||
if False:
|
||||
from typing import Tuple
|
||||
from trezor.crypto import bip32
|
||||
from typing import List
|
||||
from trezor.messages import CardanoBlockchainPointerType
|
||||
from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType
|
||||
from . import seed
|
||||
|
||||
PROTOCOL_MAGIC_KEY = 2
|
||||
INVALID_ADDRESS = wire.ProcessError("Invalid address")
|
||||
NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!")
|
||||
ADDRESS_TYPES_SHELLEY = (
|
||||
CardanoAddressType.BASE,
|
||||
CardanoAddressType.POINTER,
|
||||
CardanoAddressType.ENTERPRISE,
|
||||
CardanoAddressType.REWARD,
|
||||
)
|
||||
|
||||
HEADER_LENGTH = 1
|
||||
HASH_LENGTH = 28
|
||||
MIN_POINTER_SIZE = 0
|
||||
MAX_POINTER_SIZE = 12
|
||||
|
||||
ADDRESS_BYTES_MIN_LENGTHS = {
|
||||
CardanoAddressType.BASE: HEADER_LENGTH + HASH_LENGTH + HASH_LENGTH,
|
||||
CardanoAddressType.POINTER: HEADER_LENGTH + HASH_LENGTH + MIN_POINTER_SIZE,
|
||||
CardanoAddressType.ENTERPRISE: HEADER_LENGTH + HASH_LENGTH,
|
||||
CardanoAddressType.REWARD: HEADER_LENGTH + HASH_LENGTH,
|
||||
}
|
||||
|
||||
ADDRESS_BYTES_MAX_LENGTHS = {
|
||||
CardanoAddressType.BASE: ADDRESS_BYTES_MIN_LENGTHS[CardanoAddressType.BASE],
|
||||
CardanoAddressType.POINTER: HEADER_LENGTH + HASH_LENGTH + MAX_POINTER_SIZE,
|
||||
CardanoAddressType.ENTERPRISE: ADDRESS_BYTES_MIN_LENGTHS[
|
||||
CardanoAddressType.ENTERPRISE
|
||||
],
|
||||
CardanoAddressType.REWARD: ADDRESS_BYTES_MIN_LENGTHS[CardanoAddressType.REWARD],
|
||||
}
|
||||
|
||||
|
||||
def validate_full_path(path: List[int]) -> bool:
|
||||
"""
|
||||
Validates derivation path to fit {44', 1852'}/1815'/a'/{0,1,2}/i,
|
||||
where `a` is an account number and i an address index.
|
||||
The max value for `a` is 20, 1 000 000 for `i`.
|
||||
"""
|
||||
if len(path) != 5:
|
||||
return False
|
||||
if path[0] not in (purposes.BYRON, purposes.SHELLEY):
|
||||
return False
|
||||
if path[1] != 1815 | HARDENED:
|
||||
return False
|
||||
if path[2] < HARDENED or path[2] > 20 | HARDENED:
|
||||
return False
|
||||
if path[3] not in (0, 1, 2):
|
||||
return False
|
||||
if path[4] > 1000000:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _encode_address_raw(address_data_encoded: bytes) -> str:
|
||||
return base58.encode(
|
||||
cbor.encode(
|
||||
[cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)]
|
||||
)
|
||||
)
|
||||
def validate_output_address(address: str, protocol_magic: int, network_id: int) -> None:
|
||||
if address is None or len(address) == 0:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
address_bytes = get_address_bytes_unsafe(address)
|
||||
address_type = _get_address_type(address_bytes)
|
||||
|
||||
def derive_address_and_node(
|
||||
keychain: seed.Keychain, path: list, protocol_magic: int
|
||||
) -> Tuple[str, bip32.HDNode]:
|
||||
node = keychain.derive(path)
|
||||
if address_type == CardanoAddressType.BYRON:
|
||||
validate_output_byron_address(address_bytes, protocol_magic)
|
||||
elif address_type in ADDRESS_TYPES_SHELLEY:
|
||||
_validate_output_shelley_address(address, address_bytes, network_id)
|
||||
else:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
address_attributes = get_address_attributes(protocol_magic)
|
||||
|
||||
address_root = _get_address_root(node, address_attributes)
|
||||
address_type = 0
|
||||
address_data = [address_root, address_attributes, address_type]
|
||||
address_data_encoded = cbor.encode(address_data)
|
||||
def get_address_bytes_unsafe(address: str) -> bytes:
|
||||
try:
|
||||
address_bytes = bech32.decode_unsafe(address)
|
||||
except ValueError:
|
||||
try:
|
||||
address_bytes = base58.decode(address)
|
||||
except ValueError:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
return (_encode_address_raw(address_data_encoded), node)
|
||||
return address_bytes
|
||||
|
||||
|
||||
def get_address_attributes(protocol_magic: int) -> dict:
|
||||
# protocol magic is included in Byron addresses only on testnets
|
||||
if protocol_magic == protocol_magics.MAINNET:
|
||||
address_attributes = {}
|
||||
else:
|
||||
address_attributes = {PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)}
|
||||
def _get_address_type(address: bytes) -> int:
|
||||
return address[0] >> 4
|
||||
|
||||
return address_attributes
|
||||
|
||||
def _validate_output_shelley_address(
|
||||
address_str: str, address_bytes: bytes, network_id: int
|
||||
) -> None:
|
||||
address_type = _get_address_type(address_bytes)
|
||||
# reward address cannot be an output address
|
||||
if address_type == CardanoAddressType.REWARD:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
def validate_output_address(address: str, protocol_magic: int) -> None:
|
||||
address_data_encoded = _decode_address_raw(address)
|
||||
_validate_address_data_protocol_magic(address_data_encoded, protocol_magic)
|
||||
_validate_address_size(address_bytes, address_type)
|
||||
_validate_output_address_bech32_hrp(address_str, address_type, network_id)
|
||||
_validate_address_network_id(address_bytes, network_id)
|
||||
|
||||
|
||||
def _decode_address_raw(address: str) -> bytes:
|
||||
try:
|
||||
address_hex = base58.decode(address)
|
||||
address_unpacked = cbor.decode(address_hex)
|
||||
except ValueError as e:
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
def _validate_address_size(
|
||||
address_bytes: bytes, address_type: EnumTypeCardanoAddressType
|
||||
) -> None:
|
||||
if not (
|
||||
ADDRESS_BYTES_MIN_LENGTHS[address_type]
|
||||
<= len(address_bytes)
|
||||
<= ADDRESS_BYTES_MAX_LENGTHS[address_type]
|
||||
):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
if not isinstance(address_unpacked, list) or len(address_unpacked) != 2:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
address_data_encoded = address_unpacked[0]
|
||||
if not isinstance(address_data_encoded, bytes):
|
||||
raise INVALID_ADDRESS
|
||||
def _validate_output_address_bech32_hrp(
|
||||
address_str: str, address_type: EnumTypeCardanoAddressType, network_id: int
|
||||
) -> None:
|
||||
valid_hrp = _get_bech32_hrp_for_address(address_type, network_id)
|
||||
bech32_hrp = bech32.get_hrp(address_str)
|
||||
|
||||
address_crc = address_unpacked[1]
|
||||
if not isinstance(address_crc, int):
|
||||
if valid_hrp != bech32_hrp:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
if address_crc != crc.crc32(address_data_encoded):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
return address_data_encoded
|
||||
def _get_bech32_hrp_for_address(
|
||||
address_type: EnumTypeCardanoAddressType, network_id: int
|
||||
) -> str:
|
||||
if address_type == CardanoAddressType.BYRON:
|
||||
# Byron address uses base58 encoding
|
||||
raise ValueError
|
||||
|
||||
if address_type == CardanoAddressType.REWARD:
|
||||
if network_ids.is_mainnet(network_id):
|
||||
return bech32.HRP_REWARD_ADDRESS
|
||||
else:
|
||||
return bech32.HRP_TESTNET_REWARD_ADDRESS
|
||||
else:
|
||||
if network_ids.is_mainnet(network_id):
|
||||
return bech32.HRP_ADDRESS
|
||||
else:
|
||||
return bech32.HRP_TESTNET_ADDRESS
|
||||
|
||||
def _validate_address_data_protocol_magic(
|
||||
address_data_encoded: bytes, protocol_magic: int
|
||||
) -> None:
|
||||
"""
|
||||
Determines whether the correct protocol magic (or none)
|
||||
is included in the address. Addresses on mainnet don't
|
||||
contain protocol magic, but addresses on the testnet do.
|
||||
"""
|
||||
address_data = cbor.decode(address_data_encoded)
|
||||
if not isinstance(address_data, list) or len(address_data) < 2:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
attributes = address_data[1]
|
||||
if protocol_magic == protocol_magics.MAINNET:
|
||||
if PROTOCOL_MAGIC_KEY in attributes:
|
||||
raise NETWORK_MISMATCH
|
||||
else: # testnet
|
||||
if len(attributes) == 0 or PROTOCOL_MAGIC_KEY not in attributes:
|
||||
raise NETWORK_MISMATCH
|
||||
def _validate_address_network_id(address: bytes, network_id: int) -> None:
|
||||
if _get_address_network_id(address) != network_id:
|
||||
raise NETWORK_MISMATCH
|
||||
|
||||
protocol_magic_cbor = attributes[PROTOCOL_MAGIC_KEY]
|
||||
address_protocol_magic = cbor.decode(protocol_magic_cbor)
|
||||
|
||||
if not isinstance(address_protocol_magic, int):
|
||||
raise INVALID_ADDRESS
|
||||
def _get_address_network_id(address: bytes) -> int:
|
||||
return address[0] & 0x0F
|
||||
|
||||
|
||||
def get_public_key_hash(keychain: seed.Keychain, path: List[int]) -> bytes:
|
||||
node = keychain.derive(path)
|
||||
public_key = remove_ed25519_prefix(node.public_key())
|
||||
return hashlib.blake2b(data=public_key, outlen=28).digest()
|
||||
|
||||
|
||||
def derive_human_readable_address(
|
||||
keychain: seed.Keychain,
|
||||
parameters: CardanoAddressParametersType,
|
||||
protocol_magic: int,
|
||||
network_id: int,
|
||||
) -> str:
|
||||
address = derive_address_bytes(keychain, parameters, protocol_magic, network_id)
|
||||
|
||||
address_type = _get_address_type(address)
|
||||
if address_type == CardanoAddressType.BYRON:
|
||||
return base58.encode(address)
|
||||
elif address_type in ADDRESS_TYPES_SHELLEY:
|
||||
hrp = _get_bech32_hrp_for_address(_get_address_type(address), network_id)
|
||||
return bech32.encode(hrp, address)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
|
||||
def derive_address_bytes(
|
||||
keychain: seed.Keychain,
|
||||
parameters: CardanoAddressParametersType,
|
||||
protocol_magic: int,
|
||||
network_id: int,
|
||||
) -> bytes:
|
||||
is_byron_address = parameters.address_type == CardanoAddressType.BYRON
|
||||
|
||||
if is_byron_address:
|
||||
address = _derive_byron_address(keychain, parameters.address_n, protocol_magic)
|
||||
else:
|
||||
address = _derive_shelley_address(keychain, parameters, network_id)
|
||||
|
||||
return address
|
||||
|
||||
|
||||
def _derive_byron_address(
|
||||
keychain: seed.Keychain, path: List[int], protocol_magic: int
|
||||
) -> bytes:
|
||||
if not is_byron_path(path):
|
||||
raise wire.DataError("Invalid path for byron address!")
|
||||
|
||||
address = derive_byron_address(keychain, path, protocol_magic)
|
||||
return address
|
||||
|
||||
|
||||
def _derive_shelley_address(
|
||||
keychain: seed.Keychain, parameters: CardanoAddressParametersType, network_id: int,
|
||||
) -> bytes:
|
||||
if not is_shelley_path(parameters.address_n):
|
||||
raise wire.DataError("Invalid path for shelley address!")
|
||||
|
||||
if parameters.address_type == CardanoAddressType.BASE:
|
||||
address = _derive_base_address(
|
||||
keychain,
|
||||
parameters.address_n,
|
||||
parameters.address_n_staking,
|
||||
parameters.staking_key_hash,
|
||||
network_id,
|
||||
)
|
||||
elif parameters.address_type == CardanoAddressType.ENTERPRISE:
|
||||
address = _derive_enterprise_address(keychain, parameters.address_n, network_id)
|
||||
elif parameters.address_type == CardanoAddressType.POINTER:
|
||||
address = _derive_pointer_address(
|
||||
keychain, parameters.address_n, parameters.certificate_pointer, network_id,
|
||||
)
|
||||
elif parameters.address_type == CardanoAddressType.REWARD:
|
||||
address = _derive_reward_address(keychain, parameters.address_n, network_id)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
return address
|
||||
|
||||
|
||||
def _create_address_header(
|
||||
address_type: EnumTypeCardanoAddressType, network_id: int
|
||||
) -> bytes:
|
||||
header = address_type << 4 | network_id
|
||||
return header.to_bytes(1, "little")
|
||||
|
||||
|
||||
def _derive_base_address(
|
||||
keychain: seed.Keychain,
|
||||
path: List[int],
|
||||
staking_path: List[int],
|
||||
staking_key_hash: bytes,
|
||||
network_id: int,
|
||||
) -> bytes:
|
||||
header = _create_address_header(CardanoAddressType.BASE, network_id)
|
||||
spending_key_hash = get_public_key_hash(keychain, path)
|
||||
|
||||
_validate_base_address_staking_info(staking_path, staking_key_hash)
|
||||
|
||||
if staking_key_hash is None:
|
||||
staking_key_hash = get_public_key_hash(keychain, staking_path)
|
||||
|
||||
return header + spending_key_hash + staking_key_hash
|
||||
|
||||
|
||||
def _validate_base_address_staking_info(
|
||||
staking_path: List[int], staking_key_hash: bytes,
|
||||
) -> None:
|
||||
if (staking_key_hash is None) == (not staking_path):
|
||||
raise wire.DataError(
|
||||
"Base address needs either a staking path or a staking key hash!"
|
||||
)
|
||||
|
||||
if address_protocol_magic != protocol_magic:
|
||||
raise NETWORK_MISMATCH
|
||||
if staking_key_hash is None and not _is_staking_path(staking_path):
|
||||
raise wire.DataError("Invalid staking path!")
|
||||
|
||||
|
||||
def validate_full_path(path: list) -> bool:
|
||||
def _is_staking_path(path: List[int]) -> bool:
|
||||
"""
|
||||
Validates derivation path to fit 44'/1815'/a'/{0,1}/i,
|
||||
where `a` is an account number and i an address index.
|
||||
The max value for `a` is 20, 1 000 000 for `i`.
|
||||
The derivation scheme v1 allowed a'/0/i only,
|
||||
but in v2 it can be a'/1/i as well.
|
||||
Validates path to match 1852'/1815'/a'/2/0. Path must
|
||||
be a valid Cardano path. It must have a Shelley purpose
|
||||
(Byron paths are not valid staking paths), it must have
|
||||
2 as chain type and currently there is only one staking
|
||||
path for each account so a 0 is required for address index.
|
||||
"""
|
||||
if len(path) != 5:
|
||||
return False
|
||||
if path[0] != 44 | HARDENED:
|
||||
return False
|
||||
if path[1] != 1815 | HARDENED:
|
||||
if not validate_full_path(path):
|
||||
return False
|
||||
if path[2] < HARDENED or path[2] > 20 | HARDENED:
|
||||
|
||||
if path[0] != purposes.SHELLEY:
|
||||
return False
|
||||
if path[3] != 0 and path[3] != 1:
|
||||
if path[3] != 2:
|
||||
return False
|
||||
if path[4] > 1000000:
|
||||
if path[4] != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _address_hash(data: list) -> bytes:
|
||||
cbor_data = cbor.encode(data)
|
||||
sha_data_hash = hashlib.sha3_256(cbor_data).digest()
|
||||
res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest()
|
||||
return res
|
||||
def _derive_pointer_address(
|
||||
keychain: seed.Keychain,
|
||||
path: List[int],
|
||||
pointer: CardanoBlockchainPointerType,
|
||||
network_id: int,
|
||||
) -> bytes:
|
||||
header = _create_address_header(CardanoAddressType.POINTER, network_id)
|
||||
spending_key_hash = get_public_key_hash(keychain, path)
|
||||
encoded_pointer = _encode_certificate_pointer(pointer)
|
||||
|
||||
return header + spending_key_hash + encoded_pointer
|
||||
|
||||
|
||||
def _encode_certificate_pointer(pointer: CardanoBlockchainPointerType) -> bytes:
|
||||
if (
|
||||
pointer is None
|
||||
or pointer.block_index is None
|
||||
or pointer.tx_index is None
|
||||
or pointer.certificate_index is None
|
||||
):
|
||||
raise wire.DataError("Invalid pointer!")
|
||||
|
||||
block_index_encoded = variable_length_encode(pointer.block_index)
|
||||
tx_index_encoded = variable_length_encode(pointer.tx_index)
|
||||
certificate_index_encoded = variable_length_encode(pointer.certificate_index)
|
||||
|
||||
return bytes(block_index_encoded + tx_index_encoded + certificate_index_encoded)
|
||||
|
||||
|
||||
def _derive_enterprise_address(
|
||||
keychain: seed.Keychain, path: List[int], network_id: int,
|
||||
) -> bytes:
|
||||
header = _create_address_header(CardanoAddressType.ENTERPRISE, network_id)
|
||||
spending_key_hash = get_public_key_hash(keychain, path)
|
||||
|
||||
return header + spending_key_hash
|
||||
|
||||
|
||||
def _derive_reward_address(
|
||||
keychain: seed.Keychain, path: List[int], network_id: int,
|
||||
) -> bytes:
|
||||
if not _is_staking_path(path):
|
||||
raise wire.DataError("Invalid path for reward address!")
|
||||
|
||||
header = _create_address_header(CardanoAddressType.REWARD, network_id)
|
||||
staking_key_hash = get_public_key_hash(keychain, path)
|
||||
|
||||
return header + staking_key_hash
|
||||
|
||||
|
||||
def _get_address_root(node: bip32.HDNode, address_attributes: dict) -> bytes:
|
||||
extpubkey = remove_ed25519_prefix(node.public_key()) + node.chain_code()
|
||||
return _address_hash([0, [0, extpubkey], address_attributes])
|
||||
def to_account_path(path: List[int]) -> List[int]:
|
||||
ACCOUNT_PATH_LENGTH = const(3)
|
||||
return path[:ACCOUNT_PATH_LENGTH]
|
||||
|
@ -0,0 +1,124 @@
|
||||
from trezor import log
|
||||
from trezor.crypto import crc, hashlib
|
||||
|
||||
from apps.common import cbor
|
||||
from apps.common.seed import remove_ed25519_prefix
|
||||
|
||||
from .helpers import INVALID_ADDRESS, NETWORK_MISMATCH, protocol_magics
|
||||
|
||||
if False:
|
||||
from trezor.crypto import bip32
|
||||
from . import seed
|
||||
|
||||
PROTOCOL_MAGIC_KEY = 2
|
||||
|
||||
|
||||
"""
|
||||
This is the legacy implementation of Byron addresses. Byron
|
||||
addresses should however remain supported in Shelley with
|
||||
exactly the same implementation, thus it is kept here
|
||||
with base58 encoding and all the nuances of Byron addresses.
|
||||
"""
|
||||
|
||||
|
||||
def _encode_address_raw(address_data_encoded) -> bytes:
|
||||
return cbor.encode(
|
||||
[cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)]
|
||||
)
|
||||
|
||||
|
||||
def derive_byron_address(
|
||||
keychain: seed.Keychain, path: list, protocol_magic: int
|
||||
) -> bytes:
|
||||
node = keychain.derive(path)
|
||||
|
||||
address_attributes = get_address_attributes(protocol_magic)
|
||||
|
||||
address_root = _get_address_root(node, address_attributes)
|
||||
address_type = 0
|
||||
address_data = [address_root, address_attributes, address_type]
|
||||
address_data_encoded = cbor.encode(address_data)
|
||||
|
||||
return _encode_address_raw(address_data_encoded)
|
||||
|
||||
|
||||
def get_address_attributes(protocol_magic: int) -> dict:
|
||||
# protocol magic is included in Byron addresses only on testnets
|
||||
if protocol_magics.is_mainnet(protocol_magic):
|
||||
address_attributes = {}
|
||||
else:
|
||||
address_attributes = {PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)}
|
||||
|
||||
return address_attributes
|
||||
|
||||
|
||||
def validate_output_byron_address(address: bytes, protocol_magic: int) -> None:
|
||||
address_data_encoded = _decode_address_raw(address)
|
||||
_validate_address_data_protocol_magic(address_data_encoded, protocol_magic)
|
||||
|
||||
|
||||
def _decode_address_raw(address: bytes) -> bytes:
|
||||
try:
|
||||
address_unpacked = cbor.decode(address)
|
||||
except ValueError as e:
|
||||
if __debug__:
|
||||
log.exception(__name__, e)
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
if not isinstance(address_unpacked, list) or len(address_unpacked) != 2:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
address_data_encoded = address_unpacked[0]
|
||||
if not isinstance(address_data_encoded, bytes):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
address_crc = address_unpacked[1]
|
||||
if not isinstance(address_crc, int):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
if address_crc != crc.crc32(address_data_encoded):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
return address_data_encoded
|
||||
|
||||
|
||||
def _validate_address_data_protocol_magic(
|
||||
address_data_encoded: bytes, protocol_magic: int
|
||||
) -> None:
|
||||
"""
|
||||
Determines whether the correct protocol magic (or none)
|
||||
is included in the address. Addresses on mainnet don't
|
||||
contain protocol magic, but addresses on the testnet do.
|
||||
"""
|
||||
address_data = cbor.decode(address_data_encoded)
|
||||
if not isinstance(address_data, list) or len(address_data) < 2:
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
attributes = address_data[1]
|
||||
if protocol_magics.is_mainnet(protocol_magic):
|
||||
if PROTOCOL_MAGIC_KEY in attributes:
|
||||
raise NETWORK_MISMATCH
|
||||
else: # testnet
|
||||
if len(attributes) == 0 or PROTOCOL_MAGIC_KEY not in attributes:
|
||||
raise NETWORK_MISMATCH
|
||||
|
||||
protocol_magic_cbor = attributes[PROTOCOL_MAGIC_KEY]
|
||||
address_protocol_magic = cbor.decode(protocol_magic_cbor)
|
||||
|
||||
if not isinstance(address_protocol_magic, int):
|
||||
raise INVALID_ADDRESS
|
||||
|
||||
if address_protocol_magic != protocol_magic:
|
||||
raise NETWORK_MISMATCH
|
||||
|
||||
|
||||
def _address_hash(data: list) -> bytes:
|
||||
cbor_data = cbor.encode(data)
|
||||
sha_data_hash = hashlib.sha3_256(cbor_data).digest()
|
||||
res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest()
|
||||
return res
|
||||
|
||||
|
||||
def _get_address_root(node: bip32.HDNode, address_attributes: dict) -> bytes:
|
||||
extpubkey = remove_ed25519_prefix(node.public_key()) + node.chain_code()
|
||||
return _address_hash([0, [0, extpubkey], address_attributes])
|
@ -0,0 +1,4 @@
|
||||
from trezor import wire
|
||||
|
||||
INVALID_ADDRESS = wire.ProcessError("Invalid address")
|
||||
NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!")
|
@ -0,0 +1,34 @@
|
||||
from trezor.crypto import bech32
|
||||
|
||||
HRP_SEPARATOR = "1"
|
||||
|
||||
HRP_ADDRESS = "addr"
|
||||
HRP_TESTNET_ADDRESS = "addr_test"
|
||||
HRP_REWARD_ADDRESS = "stake"
|
||||
HRP_TESTNET_REWARD_ADDRESS = "stake_test"
|
||||
|
||||
|
||||
def encode(hrp: str, data: bytes) -> str:
|
||||
converted_bits = bech32.convertbits(data, 8, 5)
|
||||
return bech32.bech32_encode(hrp, converted_bits)
|
||||
|
||||
|
||||
def decode_unsafe(bech: str) -> bytes:
|
||||
hrp = get_hrp(bech)
|
||||
return decode(hrp, bech)
|
||||
|
||||
|
||||
def get_hrp(bech: str):
|
||||
return bech.rsplit(HRP_SEPARATOR, 1)[0]
|
||||
|
||||
|
||||
def decode(hrp: str, bech: str) -> bytes:
|
||||
decoded_hrp, data = bech32.bech32_decode(bech, 130)
|
||||
if decoded_hrp != hrp:
|
||||
raise ValueError
|
||||
|
||||
decoded = bech32.convertbits(data, 5, 8, False)
|
||||
if decoded is None:
|
||||
raise ValueError
|
||||
|
||||
return bytes(decoded)
|
@ -0,0 +1,12 @@
|
||||
MAINNET = 1
|
||||
TESTNET = 0
|
||||
|
||||
|
||||
def is_mainnet(network_id: int) -> bool:
|
||||
"""
|
||||
In the future there might be 15 mainnet IDs and
|
||||
still only one testnet ID. Therefore it is safer
|
||||
to check that it is not a testnet id. Also, if
|
||||
the mainnet id was to change, this would still work.
|
||||
"""
|
||||
return network_id != TESTNET
|
@ -0,0 +1,4 @@
|
||||
from apps.common import HARDENED
|
||||
|
||||
BYRON = 44 | HARDENED
|
||||
SHELLEY = 1852 | HARDENED
|
@ -0,0 +1,6 @@
|
||||
from apps.common import HARDENED
|
||||
|
||||
from . import purposes
|
||||
|
||||
BYRON = [purposes.BYRON, 1815 | HARDENED]
|
||||
SHELLEY = [purposes.SHELLEY, 1815 | HARDENED]
|
@ -0,0 +1,58 @@
|
||||
from trezor.messages import CardanoAddressType
|
||||
|
||||
from ..address import get_public_key_hash, to_account_path, validate_full_path
|
||||
from ..seed import is_shelley_path
|
||||
|
||||
if False:
|
||||
from typing import List
|
||||
from trezor.messages import CardanoAddressParametersType
|
||||
from . import seed
|
||||
|
||||
|
||||
"""
|
||||
Used as a helper when deciding what warnings we should
|
||||
display to the user during get_address and sign_tx depending
|
||||
on the type of address and its parameters.
|
||||
"""
|
||||
|
||||
|
||||
NO_STAKING = 0
|
||||
MATCH = 1
|
||||
MISMATCH = 2
|
||||
POINTER_ADDRESS = 3
|
||||
|
||||
|
||||
def get(
|
||||
keychain: seed.Keychain, address_parameters: CardanoAddressParametersType
|
||||
) -> int:
|
||||
address_type = address_parameters.address_type
|
||||
if address_type == CardanoAddressType.BASE:
|
||||
if not validate_full_path(address_parameters.address_n):
|
||||
return MISMATCH
|
||||
if not is_shelley_path(address_parameters.address_n):
|
||||
return MISMATCH
|
||||
|
||||
spending_account_staking_path = _path_to_staking_path(
|
||||
address_parameters.address_n
|
||||
)
|
||||
if address_parameters.address_n_staking:
|
||||
if address_parameters.address_n_staking != spending_account_staking_path:
|
||||
return MISMATCH
|
||||
else:
|
||||
staking_key_hash = get_public_key_hash(
|
||||
keychain, spending_account_staking_path
|
||||
)
|
||||
if address_parameters.staking_key_hash != staking_key_hash:
|
||||
return MISMATCH
|
||||
|
||||
return MATCH
|
||||
elif address_type == CardanoAddressType.POINTER:
|
||||
return POINTER_ADDRESS
|
||||
elif address_type == CardanoAddressType.REWARD:
|
||||
return MATCH
|
||||
else:
|
||||
return NO_STAKING
|
||||
|
||||
|
||||
def _path_to_staking_path(path: List[int]) -> List[int]:
|
||||
return to_account_path(path) + [2, 0]
|
@ -0,0 +1,20 @@
|
||||
def variable_length_encode(number: int) -> bytes:
|
||||
"""
|
||||
Used for pointer encoding in pointer address.
|
||||
Encoding description can be found here:
|
||||
https://en.wikipedia.org/wiki/Variable-length_quantity
|
||||
"""
|
||||
if number < 0:
|
||||
raise ValueError("Negative numbers not supported. Number supplied: %s" % number)
|
||||
|
||||
encoded = []
|
||||
|
||||
bit_length = len(bin(number)[2:])
|
||||
encoded.append(number & 127)
|
||||
|
||||
while bit_length > 7:
|
||||
number >>= 7
|
||||
bit_length -= 7
|
||||
encoded.insert(0, (number & 127) + 128)
|
||||
|
||||
return bytes(encoded)
|
@ -0,0 +1,224 @@
|
||||
from ubinascii import hexlify
|
||||
|
||||
from trezor import ui
|
||||
from trezor.messages import ButtonRequestType, CardanoAddressType
|
||||
from trezor.strings import format_amount
|
||||
from trezor.ui.button import ButtonDefault
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks
|
||||
|
||||
from apps.common.confirm import confirm, require_confirm, require_hold_to_confirm
|
||||
from apps.common.layout import address_n_to_str, show_warning
|
||||
|
||||
from .helpers import protocol_magics
|
||||
|
||||
if False:
|
||||
from typing import List
|
||||
from trezor import wire
|
||||
from trezor.messages import CardanoBlockchainPointerType
|
||||
from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType
|
||||
|
||||
|
||||
ADDRESS_TYPE_NAMES = {
|
||||
CardanoAddressType.BYRON: "Legacy",
|
||||
CardanoAddressType.BASE: "Base",
|
||||
CardanoAddressType.POINTER: "Pointer",
|
||||
CardanoAddressType.ENTERPRISE: "Enterprise",
|
||||
CardanoAddressType.REWARD: "Reward",
|
||||
}
|
||||
|
||||
|
||||
def format_coin_amount(amount: int) -> str:
|
||||
return "%s %s" % (format_amount(amount, 6), "ADA")
|
||||
|
||||
|
||||
async def confirm_sending(ctx: wire.Context, amount: int, to: str):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Confirm sending:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("to")
|
||||
|
||||
to_lines = list(chunks(to, 17))
|
||||
t1.bold(to_lines[0])
|
||||
|
||||
pages = [t1] + _paginate_lines(to_lines, 1, "Confirm transaction", ui.ICON_SEND)
|
||||
|
||||
await require_confirm(ctx, Paginated(pages))
|
||||
|
||||
|
||||
async def show_warning_tx_no_staking_info(
|
||||
ctx: wire.Context, address_type: EnumTypeCardanoAddressType, amount: int
|
||||
):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Change " + ADDRESS_TYPE_NAMES[address_type].lower())
|
||||
t1.normal("address has no stake")
|
||||
t1.normal("rights.")
|
||||
t1.normal("Change amount:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
|
||||
await require_confirm(ctx, t1)
|
||||
|
||||
|
||||
async def show_warning_tx_pointer_address(
|
||||
ctx: wire.Context, pointer: CardanoBlockchainPointerType, amount: int,
|
||||
):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Change address has a")
|
||||
t1.normal("pointer with staking")
|
||||
t1.normal("rights.")
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Pointer:")
|
||||
t2.bold(
|
||||
"%s, %s, %s"
|
||||
% (pointer.block_index, pointer.tx_index, pointer.certificate_index)
|
||||
)
|
||||
t2.normal("Change amount:")
|
||||
t2.bold(format_coin_amount(amount))
|
||||
|
||||
await require_confirm(ctx, Paginated([t1, t2]))
|
||||
|
||||
|
||||
async def show_warning_tx_different_staking_account(
|
||||
ctx: wire.Context, staking_account_path: List[int], amount: int,
|
||||
):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Change address staking")
|
||||
t1.normal("rights do not match")
|
||||
t1.normal("the current account.")
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Staking account:")
|
||||
t2.bold(address_n_to_str(staking_account_path))
|
||||
t2.normal("Change amount:")
|
||||
t2.bold(format_coin_amount(amount))
|
||||
|
||||
await require_confirm(ctx, Paginated([t1, t2]))
|
||||
|
||||
|
||||
async def show_warning_tx_staking_key_hash(
|
||||
ctx: wire.Context, staking_key_hash: bytes, amount: int,
|
||||
):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Change address staking")
|
||||
t1.normal("rights do not match")
|
||||
t1.normal("the current account.")
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Staking key hash:")
|
||||
t2.mono(*chunks(hexlify(staking_key_hash), 17))
|
||||
|
||||
t3 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t3.normal("Change amount:")
|
||||
t3.bold(format_coin_amount(amount))
|
||||
|
||||
await require_confirm(ctx, Paginated([t1, t2, t3]))
|
||||
|
||||
|
||||
async def confirm_transaction(ctx, amount: int, fee: int, protocol_magic: int):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Transaction amount:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("Transaction fee:")
|
||||
t1.bold(format_coin_amount(fee))
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Network:")
|
||||
t2.bold(protocol_magics.to_ui_string(protocol_magic))
|
||||
|
||||
await require_hold_to_confirm(ctx, Paginated([t1, t2]))
|
||||
|
||||
|
||||
async def show_address(
|
||||
ctx: wire.Context,
|
||||
address: str,
|
||||
address_type: EnumTypeCardanoAddressType,
|
||||
path: List[int],
|
||||
network: int = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Custom show_address function is needed because cardano addresses don't
|
||||
fit on a single screen.
|
||||
"""
|
||||
path_str = address_n_to_str(path)
|
||||
t1 = Text(path_str, ui.ICON_RECEIVE, ui.GREEN)
|
||||
if network is not None:
|
||||
t1.normal("%s network" % protocol_magics.to_ui_string(network))
|
||||
t1.normal("%s address" % ADDRESS_TYPE_NAMES[address_type])
|
||||
|
||||
address_lines = list(chunks(address, 17))
|
||||
t1.bold(address_lines[0])
|
||||
t1.bold(address_lines[1])
|
||||
t1.bold(address_lines[2])
|
||||
|
||||
pages = [t1] + _paginate_lines(address_lines, 3, path_str, ui.ICON_RECEIVE)
|
||||
|
||||
return await confirm(
|
||||
ctx,
|
||||
Paginated(pages),
|
||||
code=ButtonRequestType.Address,
|
||||
cancel="QR",
|
||||
cancel_style=ButtonDefault,
|
||||
)
|
||||
|
||||
|
||||
def _paginate_lines(
|
||||
lines: List[str], offset: int, desc: str, icon: str, per_page: int = 4
|
||||
) -> List[ui.Component]:
|
||||
pages = []
|
||||
if len(lines) > offset:
|
||||
to_pages = list(chunks(lines[offset:], per_page))
|
||||
for page in to_pages:
|
||||
t = Text(desc, icon, ui.GREEN)
|
||||
for line in page:
|
||||
t.bold(line)
|
||||
pages.append(t)
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
async def show_warning_address_foreign_staking_key(
|
||||
ctx: wire.Context,
|
||||
account_path: List[int],
|
||||
staking_account_path: List[int],
|
||||
staking_key_hash: bytes,
|
||||
) -> None:
|
||||
await show_warning(
|
||||
ctx,
|
||||
(
|
||||
"Stake rights associated",
|
||||
"with this address do",
|
||||
"not match your",
|
||||
"account",
|
||||
address_n_to_str(account_path),
|
||||
),
|
||||
button="Ok",
|
||||
)
|
||||
|
||||
if staking_account_path:
|
||||
staking_key_message = (
|
||||
"Stake account path:",
|
||||
address_n_to_str(staking_account_path),
|
||||
)
|
||||
else:
|
||||
staking_key_message = ("Staking key:", hexlify(staking_key_hash))
|
||||
|
||||
await show_warning(
|
||||
ctx, staking_key_message, button="Ok",
|
||||
)
|
||||
|
||||
|
||||
async def show_warning_address_pointer(
|
||||
ctx: wire.Context, pointer: CardanoBlockchainPointerType
|
||||
) -> None:
|
||||
await show_warning(
|
||||
ctx,
|
||||
(
|
||||
"Pointer address:",
|
||||
"Block: %s" % pointer.block_index,
|
||||
"Transaction: %s" % pointer.tx_index,
|
||||
"Certificate: %s" % pointer.certificate_index,
|
||||
),
|
||||
button="Ok",
|
||||
)
|
@ -1,51 +0,0 @@
|
||||
from micropython import const
|
||||
|
||||
from trezor import ui
|
||||
from trezor.strings import format_amount
|
||||
from trezor.ui.scroll import Paginated
|
||||
from trezor.ui.text import Text
|
||||
from trezor.utils import chunks
|
||||
|
||||
from apps.common.confirm import require_confirm, require_hold_to_confirm
|
||||
|
||||
from .. import protocol_magics
|
||||
|
||||
|
||||
def format_coin_amount(amount):
|
||||
return "%s %s" % (format_amount(amount, 6), "ADA")
|
||||
|
||||
|
||||
async def confirm_sending(ctx, amount, to):
|
||||
to_lines = list(chunks(to, 17))
|
||||
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Confirm sending:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("to:")
|
||||
t1.bold(to_lines[0])
|
||||
|
||||
PER_PAGE = const(4)
|
||||
pages = [t1]
|
||||
if len(to_lines) > 1:
|
||||
to_pages = list(chunks(to_lines[1:], PER_PAGE))
|
||||
for page in to_pages:
|
||||
t = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
for line in page:
|
||||
t.bold(line)
|
||||
pages.append(t)
|
||||
|
||||
await require_confirm(ctx, Paginated(pages))
|
||||
|
||||
|
||||
async def confirm_transaction(ctx, amount, fee, protocol_magic):
|
||||
t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t1.normal("Total amount:")
|
||||
t1.bold(format_coin_amount(amount))
|
||||
t1.normal("including fee:")
|
||||
t1.bold(format_coin_amount(fee))
|
||||
|
||||
t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN)
|
||||
t2.normal("Network:")
|
||||
t2.bold(protocol_magics.to_ui_string(protocol_magic))
|
||||
|
||||
await require_hold_to_confirm(ctx, Paginated([t1, t2]))
|
@ -0,0 +1,40 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
import protobuf as p
|
||||
|
||||
from .CardanoBlockchainPointerType import CardanoBlockchainPointerType
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCardanoAddressType = Literal[0, 4, 6, 8, 14]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoAddressParametersType(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
address_type: EnumTypeCardanoAddressType = None,
|
||||
address_n: List[int] = None,
|
||||
address_n_staking: List[int] = None,
|
||||
staking_key_hash: bytes = None,
|
||||
certificate_pointer: CardanoBlockchainPointerType = None,
|
||||
) -> None:
|
||||
self.address_type = address_type
|
||||
self.address_n = address_n if address_n is not None else []
|
||||
self.address_n_staking = address_n_staking if address_n_staking is not None else []
|
||||
self.staking_key_hash = staking_key_hash
|
||||
self.certificate_pointer = certificate_pointer
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('address_type', p.EnumType("CardanoAddressType", (0, 4, 6, 8, 14)), 0),
|
||||
2: ('address_n', p.UVarintType, p.FLAG_REPEATED),
|
||||
3: ('address_n_staking', p.UVarintType, p.FLAG_REPEATED),
|
||||
4: ('staking_key_hash', p.BytesType, 0),
|
||||
5: ('certificate_pointer', CardanoBlockchainPointerType, 0),
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
BASE = 0 # type: Literal[0]
|
||||
POINTER = 4 # type: Literal[4]
|
||||
ENTERPRISE = 6 # type: Literal[6]
|
||||
BYRON = 8 # type: Literal[8]
|
||||
REWARD = 14 # type: Literal[14]
|
@ -0,0 +1,31 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoBlockchainPointerType(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
block_index: int = None,
|
||||
tx_index: int = None,
|
||||
certificate_index: int = None,
|
||||
) -> None:
|
||||
self.block_index = block_index
|
||||
self.tx_index = tx_index
|
||||
self.certificate_index = certificate_index
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('block_index', p.UVarintType, 0),
|
||||
2: ('tx_index', p.UVarintType, 0),
|
||||
3: ('certificate_index', p.UVarintType, 0),
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
from common import *
|
||||
|
||||
from apps.cardano.helpers import bech32
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestCardanoBech32(unittest.TestCase):
|
||||
def test_decode_and_encode(self):
|
||||
expected_bechs = [
|
||||
# human readable part, bech32
|
||||
("a", "a12uel5l"),
|
||||
("an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio",
|
||||
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs"),
|
||||
("abcdef", "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"),
|
||||
("1", "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j"),
|
||||
("split", "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w"),
|
||||
("addr", "addr1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsw8ezsk"),
|
||||
("addr_test", "addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5")
|
||||
]
|
||||
|
||||
for expected_human_readable_part, expected_bech in expected_bechs:
|
||||
decoded = bech32.decode(expected_human_readable_part, expected_bech)
|
||||
actual_bech = bech32.encode(expected_human_readable_part, decoded)
|
||||
|
||||
self.assertEqual(actual_bech, expected_bech)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -0,0 +1,65 @@
|
||||
from common import *
|
||||
from trezor import wire
|
||||
from trezor.crypto import bip32, slip39
|
||||
|
||||
from apps.common import HARDENED, seed
|
||||
|
||||
if not utils.BITCOIN_ONLY:
|
||||
from apps.cardano.seed import Keychain
|
||||
from apps.cardano.get_public_key import _get_public_key
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestCardanoKeychain(unittest.TestCase):
|
||||
def test_various_paths_at_once(self):
|
||||
mnemonic = "test walk nut penalty hip pave soap entry language right filter choice"
|
||||
passphrase = ""
|
||||
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
|
||||
keychain = Keychain(node)
|
||||
|
||||
derivation_paths = [
|
||||
[44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 0],
|
||||
[44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 1],
|
||||
[1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 0],
|
||||
[1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 1],
|
||||
[44 | HARDENED, 1815 | HARDENED, HARDENED, 0, 2],
|
||||
[1852 | HARDENED, 1815 | HARDENED, HARDENED, 0, 2]
|
||||
]
|
||||
|
||||
public_keys = [
|
||||
b'badd2852ccda7492364be0f88f2ba0b78c5f2d7179a941f1d19f756112b66afa',
|
||||
b'34377409140c061d76778626d43456880d5471c1cbade8c372cb6a3be9678072',
|
||||
b'73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d',
|
||||
b'f626ab887eb5f40b502463ccf2ec5a7311676ee9e5d55c492059a366c0b4d4a1',
|
||||
b'408ee7b2d1c84d7899dba07150fae88c5411974f1762cb659dd928db8aac206b',
|
||||
b'86e8a3880767e1ed521a47de1e031d47f33d5a8095be467bffbbd3295e27258e'
|
||||
]
|
||||
|
||||
chain_codes = [
|
||||
b"e1c5d15875d3ed68667978af38fe3fe586511d87a784c0962a333c21e63a865d",
|
||||
b"15c987276326a82defa4cb6762d43442f09e5dcbcc37fa0c58f24ae2dba3d3eb",
|
||||
b"dd75e154da417becec55cdd249327454138f082110297d5e87ab25e15fad150f",
|
||||
b"f7ab126f2884db9059fa09ca83be6b8bd0250426aeb62191bdd9861457b8bc91",
|
||||
b"18d5c9d20c8d23bed068c9ff3a1126b940f0e537f9d94891828a999dda6fafd1",
|
||||
b"580bba4bb0b9c56974e16a6998322a91e857e2fac28674404da993f6197fd29f"
|
||||
]
|
||||
|
||||
xpub_keys = [
|
||||
"badd2852ccda7492364be0f88f2ba0b78c5f2d7179a941f1d19f756112b66afae1c5d15875d3ed68667978af38fe3fe586511d87a784c0962a333c21e63a865d",
|
||||
"34377409140c061d76778626d43456880d5471c1cbade8c372cb6a3be967807215c987276326a82defa4cb6762d43442f09e5dcbcc37fa0c58f24ae2dba3d3eb",
|
||||
"73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7ddd75e154da417becec55cdd249327454138f082110297d5e87ab25e15fad150f",
|
||||
"f626ab887eb5f40b502463ccf2ec5a7311676ee9e5d55c492059a366c0b4d4a1f7ab126f2884db9059fa09ca83be6b8bd0250426aeb62191bdd9861457b8bc91",
|
||||
"408ee7b2d1c84d7899dba07150fae88c5411974f1762cb659dd928db8aac206b18d5c9d20c8d23bed068c9ff3a1126b940f0e537f9d94891828a999dda6fafd1",
|
||||
"86e8a3880767e1ed521a47de1e031d47f33d5a8095be467bffbbd3295e27258e580bba4bb0b9c56974e16a6998322a91e857e2fac28674404da993f6197fd29f"
|
||||
]
|
||||
|
||||
for index, derivation_path in enumerate(derivation_paths):
|
||||
key = _get_public_key(keychain, derivation_path)
|
||||
|
||||
self.assertEqual(hexlify(key.node.public_key), public_keys[index])
|
||||
self.assertEqual(hexlify(key.node.chain_code), chain_codes[index])
|
||||
self.assertEqual(key.xpub, xpub_keys[index])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -0,0 +1,100 @@
|
||||
from ubinascii import unhexlify
|
||||
|
||||
from common import *
|
||||
|
||||
from apps.common import HARDENED
|
||||
from trezor.crypto import bip32
|
||||
from trezor.messages import CardanoAddressType
|
||||
from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType
|
||||
from trezor.messages.CardanoBlockchainPointerType import CardanoBlockchainPointerType
|
||||
|
||||
|
||||
if not utils.BITCOIN_ONLY:
|
||||
from apps.cardano.helpers import staking_use_cases
|
||||
from apps.cardano.seed import Keychain
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestCardanoStakingUseCases(unittest.TestCase):
|
||||
def test_get(self):
|
||||
mnemonic = (
|
||||
"test walk nut penalty hip pave soap entry language right filter choice"
|
||||
)
|
||||
passphrase = ""
|
||||
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
|
||||
keychain = Keychain(node)
|
||||
|
||||
expected_staking_use_cases = [
|
||||
# address parameters, expected staking use case
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.BASE,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0],
|
||||
),
|
||||
staking_use_cases.MATCH,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.BASE,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 2 | HARDENED, 2, 0],
|
||||
),
|
||||
staking_use_cases.MISMATCH,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.BASE,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
staking_key_hash=unhexlify("32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc"),
|
||||
),
|
||||
staking_use_cases.MATCH,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.BASE,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
staking_key_hash=unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"),
|
||||
),
|
||||
staking_use_cases.MISMATCH,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.POINTER,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
certificate_pointer=CardanoBlockchainPointerType(
|
||||
block_index=1, tx_index=2, certificate_index=3
|
||||
),
|
||||
),
|
||||
staking_use_cases.POINTER_ADDRESS,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.REWARD,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0],
|
||||
),
|
||||
staking_use_cases.MATCH,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.ENTERPRISE,
|
||||
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
),
|
||||
staking_use_cases.NO_STAKING,
|
||||
),
|
||||
(
|
||||
CardanoAddressParametersType(
|
||||
address_type=CardanoAddressType.BYRON,
|
||||
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
|
||||
),
|
||||
staking_use_cases.NO_STAKING,
|
||||
),
|
||||
]
|
||||
|
||||
for address_parameters, expected_staking_use_case in expected_staking_use_cases:
|
||||
actual_staking_use_case = staking_use_cases.get(keychain, address_parameters)
|
||||
self.assertEqual(actual_staking_use_case, expected_staking_use_case)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -0,0 +1,33 @@
|
||||
from common import *
|
||||
|
||||
if not utils.BITCOIN_ONLY:
|
||||
from apps.cardano.helpers.utils import variable_length_encode
|
||||
|
||||
|
||||
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||
class TestCardanoUtils(unittest.TestCase):
|
||||
def test_variable_length_encode(self):
|
||||
test_vectors = [
|
||||
(0, bytes([0x00])),
|
||||
(42, bytes([0x2A])),
|
||||
(127, bytes([0x7F])),
|
||||
(128, bytes([0x81, 0x00])),
|
||||
(129, bytes([0x81, 0x01])),
|
||||
(255, bytes([0x81, 0x7F])),
|
||||
(256, bytes([0x82, 0x00])),
|
||||
(16383, bytes([0xFF, 0x7F])),
|
||||
(16384, bytes([0x81, 0x80, 0x00])),
|
||||
]
|
||||
|
||||
for number, expected in test_vectors:
|
||||
actual = variable_length_encode(number)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
|
||||
def test_variable_length_encode_negative_number(self):
|
||||
with self.assertRaises(ValueError):
|
||||
variable_length_encode(-1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -0,0 +1,40 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
from .. import protobuf as p
|
||||
|
||||
from .CardanoBlockchainPointerType import CardanoBlockchainPointerType
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
EnumTypeCardanoAddressType = Literal[0, 4, 6, 8, 14]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoAddressParametersType(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
address_type: EnumTypeCardanoAddressType = None,
|
||||
address_n: List[int] = None,
|
||||
address_n_staking: List[int] = None,
|
||||
staking_key_hash: bytes = None,
|
||||
certificate_pointer: CardanoBlockchainPointerType = None,
|
||||
) -> None:
|
||||
self.address_type = address_type
|
||||
self.address_n = address_n if address_n is not None else []
|
||||
self.address_n_staking = address_n_staking if address_n_staking is not None else []
|
||||
self.staking_key_hash = staking_key_hash
|
||||
self.certificate_pointer = certificate_pointer
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('address_type', p.EnumType("CardanoAddressType", (0, 4, 6, 8, 14)), 0),
|
||||
2: ('address_n', p.UVarintType, p.FLAG_REPEATED),
|
||||
3: ('address_n_staking', p.UVarintType, p.FLAG_REPEATED),
|
||||
4: ('staking_key_hash', p.BytesType, 0),
|
||||
5: ('certificate_pointer', CardanoBlockchainPointerType, 0),
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
if False:
|
||||
from typing_extensions import Literal
|
||||
|
||||
BASE = 0 # type: Literal[0]
|
||||
POINTER = 4 # type: Literal[4]
|
||||
ENTERPRISE = 6 # type: Literal[6]
|
||||
BYRON = 8 # type: Literal[8]
|
||||
REWARD = 14 # type: Literal[14]
|
@ -0,0 +1,31 @@
|
||||
# Automatically generated by pb2py
|
||||
# fmt: off
|
||||
from .. import protobuf as p
|
||||
|
||||
if __debug__:
|
||||
try:
|
||||
from typing import Dict, List # noqa: F401
|
||||
from typing_extensions import Literal # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CardanoBlockchainPointerType(p.MessageType):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
block_index: int = None,
|
||||
tx_index: int = None,
|
||||
certificate_index: int = None,
|
||||
) -> None:
|
||||
self.block_index = block_index
|
||||
self.tx_index = tx_index
|
||||
self.certificate_index = certificate_index
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> Dict:
|
||||
return {
|
||||
1: ('block_index', p.UVarintType, 0),
|
||||
2: ('tx_index', p.UVarintType, 0),
|
||||
3: ('certificate_index', p.UVarintType, 0),
|
||||
}
|
Loading…
Reference in new issue