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

Cardano shelley update 2/3 (#1112)

This commit is contained in:
Gabriel Kerekeš 2020-07-23 15:54:49 +02:00 committed by matejcik
parent e1615e60ec
commit d2c1624602
45 changed files with 2909 additions and 453 deletions

View File

@ -7,6 +7,46 @@ option java_outer_classname = "TrezorMessageCardano";
import "messages-common.proto";
/**
* Values correspond to address header values given by the spec.
*/
enum CardanoAddressType {
BASE = 0;
POINTER = 4;
ENTERPRISE = 6;
BYRON = 8;
REWARD = 14;
}
/**
* Structure representing cardano PointerAddress pointer,
* which points to a staking key registration certificate.
* @embed
*/
message CardanoBlockchainPointerType {
optional uint32 block_index = 1;
optional uint32 tx_index = 2;
optional uint32 certificate_index = 3;
}
/**
* Structure to represent address parameters so they can be
* reused in CardanoGetAddress and CardanoTxOutputType.
* NetworkId isn't a part of the parameters, because in a transaction
* this will be included separately in the transaction itself, so it
* shouldn't be duplicated here.
* @embed
*/
message CardanoAddressParametersType {
optional CardanoAddressType address_type = 1; // one of the CardanoAddressType-s
repeated uint32 address_n = 2; // BIP-32-style path to derive the spending key from master node
repeated uint32 address_n_staking = 3; // BIP-32-style path to derive staking key from master node
optional bytes staking_key_hash = 4; // staking key can be derived from address_n_staking, or
// can be sent directly e.g. if it doesn't belong to
// the same account as address_n
optional CardanoBlockchainPointerType certificate_pointer = 5; // a pointer to the staking key registration certificate
}
/**
* Request: Ask device for Cardano address
* @start
@ -14,9 +54,11 @@ import "messages-common.proto";
* @next Failure
*/
message CardanoGetAddress {
repeated uint32 address_n = 1; // BIP-32-style path to derive the key from master node
optional bool show_display = 2; // optionally prompt for confirmation on trezor display
optional uint32 protocol_magic = 3; // network's protocol magic - needed for Byron addresses on testnets
// repeated uint32 address_n = 1; // moved to address_parameters
optional bool show_display = 2; // optionally prompt for confirmation on trezor display
optional uint32 protocol_magic = 3; // network's protocol magic - needed for Byron addresses on testnets
optional uint32 network_id = 4; // network id - mainnet or testnet
optional CardanoAddressParametersType address_parameters = 5; // parameters used to derive the address
}
/**
@ -60,6 +102,7 @@ message CardanoSignTx {
optional uint32 protocol_magic = 5; // network's protocol magic
optional uint64 fee = 6; // transaction fee - added in shelley
optional uint64 ttl = 7; // transaction ttl - added in shelley
optional uint32 network_id = 8; // network id - mainnet or testnet
/**
* Structure representing cardano transaction input
*/
@ -74,9 +117,10 @@ message CardanoSignTx {
* Structure representing cardano transaction output
*/
message CardanoTxOutputType {
optional string address = 1; // target coin address in Base58 encoding
repeated uint32 address_n = 2; // BIP-32 path to derive the key from master node; has higher priority than "address"
optional uint64 amount = 3; // amount to spend
optional string address = 1; // target coin address in bech32 or base58
// repeated uint32 address_n = 2; // moved to address_parameters
optional uint64 amount = 3; // amount to spend
optional CardanoAddressParametersType address_parameters = 4; // parameters used to derive the address
}
}

View File

@ -1,10 +1,7 @@
from trezor import wire
from trezor.messages import MessageType
from apps.common import HARDENED
CURVE = "ed25519"
SEED_NAMESPACE = [HARDENED | 44, HARDENED | 1815]
def boot() -> None:

View File

@ -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 _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 derive_address_and_node(
keychain: seed.Keychain, path: list, protocol_magic: int
) -> Tuple[str, bip32.HDNode]:
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), node)
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)}
return address_attributes
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)
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)
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:
def validate_full_path(path: List[int]) -> bool:
"""
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
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 validate_full_path(path: list) -> bool:
"""
Validates derivation path to fit 44'/1815'/a'/{0,1}/i,
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`.
The derivation scheme v1 allowed a'/0/i only,
but in v2 it can be a'/1/i as well.
"""
if len(path) != 5:
return False
if path[0] != 44 | HARDENED:
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] != 0 and path[3] != 1:
if path[3] not in (0, 1, 2):
return False
if path[4] > 1000000:
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 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)
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
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 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 address_bytes
def _get_address_type(address: bytes) -> int:
return address[0] >> 4
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
_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 _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
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)
if valid_hrp != bech32_hrp:
raise INVALID_ADDRESS
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_network_id(address: bytes, network_id: int) -> None:
if _get_address_network_id(address) != network_id:
raise NETWORK_MISMATCH
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 staking_key_hash is None and not _is_staking_path(staking_path):
raise wire.DataError("Invalid staking path!")
def _is_staking_path(path: List[int]) -> bool:
"""
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 not validate_full_path(path):
return False
if path[0] != purposes.SHELLEY:
return False
if path[3] != 2:
return False
if path[4] != 0:
return False
return True
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 to_account_path(path: List[int]) -> List[int]:
ACCOUNT_PATH_LENGTH = const(3)
return path[:ACCOUNT_PATH_LENGTH]

View File

@ -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])

View File

@ -2,30 +2,88 @@ from trezor import log, wire
from trezor.messages.CardanoAddress import CardanoAddress
from apps.common import paths
from apps.common.layout import address_n_to_str, show_address, show_qr
from apps.common.layout import address_n_to_str, show_qr
from . import CURVE, seed
from .address import derive_address_and_node, validate_full_path
from .address import derive_human_readable_address, to_account_path, validate_full_path
from .helpers import protocol_magics, staking_use_cases
from .layout import (
show_address,
show_warning_address_foreign_staking_key,
show_warning_address_pointer,
)
if False:
from trezor.messages import CardanoAddressParametersType, CardanoGetAddress
@seed.with_keychain
async def get_address(ctx, msg, keychain: seed.Keychain):
await paths.validate_path(ctx, validate_full_path, keychain, msg.address_n, CURVE)
async def get_address(
ctx: wire.Context, msg: CardanoGetAddress, keychain: seed.Keychain
) -> CardanoAddress:
address_parameters = msg.address_parameters
await paths.validate_path(
ctx, validate_full_path, keychain, address_parameters.address_n, CURVE
)
try:
address, _ = derive_address_and_node(
keychain, msg.address_n, msg.protocol_magic
address = derive_human_readable_address(
keychain, address_parameters, msg.protocol_magic, msg.network_id
)
except ValueError as e:
if __debug__:
log.exception(__name__, e)
raise wire.ProcessError("Deriving address failed")
if msg.show_display:
desc = address_n_to_str(msg.address_n)
while True:
if await show_address(ctx, address, desc=desc):
break
if await show_qr(ctx, address, desc=desc):
break
await _display_address(
ctx, keychain, address_parameters, address, msg.protocol_magic
)
return CardanoAddress(address=address)
async def _display_address(
ctx: wire.Context,
keychain: seed.Keychain,
address_parameters: CardanoAddressParametersType,
address: str,
protocol_magic: int,
) -> None:
await _show_staking_warnings(ctx, keychain, address_parameters)
network = None
if not protocol_magics.is_mainnet(protocol_magic):
network = protocol_magic
while True:
if await show_address(
ctx,
address,
address_parameters.address_type,
address_parameters.address_n,
network=network,
):
break
if await show_qr(
ctx, address, desc=address_n_to_str(address_parameters.address_n)
):
break
async def _show_staking_warnings(
ctx: wire.Context,
keychain: seed.Keychain,
address_parameters: CardanoAddressParametersType,
) -> None:
staking_type = staking_use_cases.get(keychain, address_parameters)
if staking_type == staking_use_cases.MISMATCH:
await show_warning_address_foreign_staking_key(
ctx,
to_account_path(address_parameters.address_n),
to_account_path(address_parameters.address_n_staking),
address_parameters.staking_key_hash,
)
elif staking_type == staking_use_cases.POINTER_ADDRESS:
await show_warning_address_pointer(ctx, address_parameters.certificate_pointer)

View File

@ -0,0 +1,4 @@
from trezor import wire
INVALID_ADDRESS = wire.ProcessError("Invalid address")
NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!")

View File

@ -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)

View File

@ -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

View File

@ -7,5 +7,9 @@ NAMES = {
}
def is_mainnet(protocol_magic: int) -> bool:
return protocol_magic == MAINNET
def to_ui_string(value: int) -> str:
return NAMES.get(value, "Unknown")

View File

@ -0,0 +1,4 @@
from apps.common import HARDENED
BYRON = 44 | HARDENED
SHELLEY = 1852 | HARDENED

View File

@ -0,0 +1,6 @@
from apps.common import HARDENED
from . import purposes
BYRON = [purposes.BYRON, 1815 | HARDENED]
SHELLEY = [purposes.SHELLEY, 1815 | HARDENED]

View File

@ -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]

View File

@ -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)

View File

@ -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",
)

View File

@ -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]))

View File

@ -5,7 +5,7 @@ from trezor.crypto import bip32
from apps.common import mnemonic
from apps.common.passphrase import get as get_passphrase
from . import SEED_NAMESPACE
from .helpers import seed_namespaces
if False:
from apps.common.paths import Bip32Path
@ -13,19 +13,42 @@ if False:
class Keychain:
"""Cardano keychain hard-coded to SEED_NAMESPACE."""
"""Cardano keychain hard-coded to Byron and Shelley seed namespaces."""
def __init__(self, root: bip32.HDNode) -> None:
self.root = root
self.byron_root = self._create_namespace_root(seed_namespaces.BYRON)
self.shelley_root = self._create_namespace_root(seed_namespaces.SHELLEY)
def verify_path(self, path: Bip32Path) -> None:
if path[: len(SEED_NAMESPACE)] != SEED_NAMESPACE:
if not is_byron_path(path) and not is_shelley_path(path):
raise wire.DataError("Forbidden key path")
def _create_namespace_root(self, namespace: list):
new_root = self.root.clone()
for i in namespace:
new_root.derive_cardano(i)
return new_root
def _get_path_root(self, path: list):
if is_byron_path(path):
return self.byron_root
elif is_shelley_path(path):
return self.shelley_root
else:
raise wire.DataError("Forbidden key path")
def derive(self, node_path: Bip32Path) -> bip32.HDNode:
self.verify_path(node_path)
path_root = self._get_path_root(node_path)
# this is true now, so for simplicity we don't branch on path type
assert len(seed_namespaces.BYRON) == len(seed_namespaces.SHELLEY)
suffix = node_path[len(seed_namespaces.SHELLEY) :]
# derive child node from the root
node = self.root.clone()
suffix = node_path[len(SEED_NAMESPACE) :]
node = path_root.clone()
for i in suffix:
node.derive_cardano(i)
return node
@ -35,6 +58,14 @@ class Keychain:
# self.root.__del__()
def is_byron_path(path: Bip32Path):
return path[: len(seed_namespaces.BYRON)] == seed_namespaces.BYRON
def is_shelley_path(path: Bip32Path):
return path[: len(seed_namespaces.SHELLEY)] == seed_namespaces.SHELLEY
@cache.stored_async(cache.APP_CARDANO_ROOT)
async def get_keychain(ctx: wire.Context) -> Keychain:
if not device.is_initialized():
@ -49,10 +80,6 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
seed = mnemonic.get_seed(passphrase)
root = bip32.from_seed(seed, "ed25519 cardano seed")
# derive the namespaced root node
for i in SEED_NAMESPACE:
root.derive_cardano(i)
keychain = Keychain(root)
return keychain

View File

@ -1,8 +1,9 @@
from micropython import const
from trezor import log, wire
from trezor.crypto import base58, hashlib
from trezor.crypto import hashlib
from trezor.crypto.curve import ed25519
from trezor.messages import CardanoAddressParametersType
from trezor.messages.CardanoSignedTx import CardanoSignedTx
from apps.common import cbor
@ -11,12 +12,24 @@ from apps.common.seed import remove_ed25519_prefix
from . import CURVE, seed
from .address import (
derive_address_and_node,
get_address_attributes,
derive_address_bytes,
derive_human_readable_address,
get_address_bytes_unsafe,
to_account_path,
validate_full_path,
validate_output_address,
)
from .layout import confirm_sending, confirm_transaction
from .byron_address import get_address_attributes
from .helpers import network_ids, protocol_magics, staking_use_cases
from .layout import (
confirm_sending,
confirm_transaction,
show_warning_tx_different_staking_account,
show_warning_tx_no_staking_info,
show_warning_tx_pointer_address,
show_warning_tx_staking_key_hash,
)
from .seed import is_byron_path, is_shelley_path
if False:
from typing import Dict, List, Tuple
@ -41,10 +54,12 @@ async def sign_tx(
if msg.fee > LOVELACE_MAX_SUPPLY:
raise wire.ProcessError("Fee is out of range!")
_validate_network_info(msg.network_id, msg.protocol_magic)
for i in msg.inputs:
await validate_path(ctx, validate_full_path, keychain, i.address_n, CURVE)
_validate_outputs(keychain, msg.outputs, msg.protocol_magic)
_validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id)
# display the transaction in UI
await _show_tx(ctx, keychain, msg)
@ -61,18 +76,42 @@ async def sign_tx(
return tx
def _validate_network_info(network_id: int, protocol_magic: int) -> None:
"""
We are only concerned about checking that both network_id and protocol_magic
belong to the mainnet or that both belong to a testnet. We don't need to check for
consistency between various testnets (at least for now).
"""
is_mainnet_network_id = network_ids.is_mainnet(network_id)
is_mainnet_protocol_magic = protocol_magics.is_mainnet(protocol_magic)
if is_mainnet_network_id != is_mainnet_protocol_magic:
raise wire.ProcessError("Invalid network id/protocol magic combination!")
def _validate_outputs(
keychain: seed.Keychain, outputs: List[CardanoTxOutputType], protocol_magic: int
keychain: seed.Keychain,
outputs: List[CardanoTxOutputType],
protocol_magic: int,
network_id: int,
) -> None:
if not outputs:
raise wire.ProcessError("Transaction has no outputs!")
total_amount = 0
for output in outputs:
total_amount += output.amount
if output.address_n:
continue
if output.address_parameters:
# try to derive the address to validate it
derive_address_bytes(
keychain, output.address_parameters, protocol_magic, network_id
)
elif output.address is not None:
validate_output_address(output.address, protocol_magic)
validate_output_address(output.address, protocol_magic, network_id)
else:
raise wire.ProcessError("Each output must have address or address_n field!")
raise wire.ProcessError(
"Each output must have an address field or address_parameters!"
)
if total_amount > LOVELACE_MAX_SUPPLY:
raise wire.ProcessError("Total transaction amount is out of range!")
@ -82,11 +121,7 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b
tx_body = _build_tx_body(keychain, msg)
tx_hash = _hash_tx_body(tx_body)
witnesses_for_cbor = _build_witnesses(
keychain, msg.inputs, tx_hash, msg.protocol_magic
)
# byron witnesses have the key 2 in Shelley
witnesses = {2: witnesses_for_cbor}
witnesses = _build_witnesses(keychain, msg.inputs, tx_hash, msg.protocol_magic)
serialized_tx = cbor.encode([tx_body, witnesses, None])
@ -95,7 +130,9 @@ def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, b
def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict:
inputs_for_cbor = _build_inputs(msg.inputs)
outputs_for_cbor = _build_outputs(keychain, msg.outputs, msg.protocol_magic)
outputs_for_cbor = _build_outputs(
keychain, msg.outputs, msg.protocol_magic, msg.network_id
)
tx_body = {
0: inputs_for_cbor,
@ -112,19 +149,23 @@ def _build_inputs(inputs: List[CardanoTxInputType]) -> List[Tuple[bytes, int]]:
def _build_outputs(
keychain: seed.Keychain, outputs: List[CardanoTxOutputType], protocol_magic: int
keychain: seed.Keychain,
outputs: List[CardanoTxOutputType],
protocol_magic: int,
network_id: int,
) -> List[Tuple[bytes, int]]:
result = []
for output in outputs:
amount = output.amount
if output.address_n:
address, _ = derive_address_and_node(
keychain, output.address_n, protocol_magic
if output.address_parameters:
address = derive_address_bytes(
keychain, output.address_parameters, protocol_magic, network_id
)
else:
address = output.address
# output address is validated in _validate_outputs before this happens
address = get_address_bytes_unsafe(output.address)
result.append((base58.decode(address), amount))
result.append((address, amount))
return result
@ -139,9 +180,53 @@ def _build_witnesses(
inputs: List[CardanoTxInputType],
tx_body_hash: bytes,
protocol_magic: int,
) -> List[Tuple[bytes, bytes, bytes, bytes]]:
result = []
) -> Dict:
shelley_witnesses = _build_shelley_witnesses(keychain, inputs, tx_body_hash)
byron_witnesses = _build_byron_witnesses(
keychain, inputs, tx_body_hash, protocol_magic
)
# use key 0 for shelley witnesses and key 2 for byron witnesses
# according to the spec in shelley.cddl in cardano-ledger-specs
witnesses = {}
if len(shelley_witnesses) > 0:
witnesses[0] = shelley_witnesses
if len(byron_witnesses) > 0:
witnesses[2] = byron_witnesses
return witnesses
def _build_shelley_witnesses(
keychain: seed.Keychain, inputs: List[CardanoTxInputType], tx_body_hash: bytes,
) -> List[Tuple[bytes, bytes]]:
shelley_witnesses = []
for input in inputs:
if not is_shelley_path(input.address_n):
continue
node = keychain.derive(input.address_n)
public_key = remove_ed25519_prefix(node.public_key())
signature = ed25519.sign_ext(
node.private_key(), node.private_key_ext(), tx_body_hash
)
shelley_witnesses.append((public_key, signature))
return shelley_witnesses
def _build_byron_witnesses(
keychain: seed.Keychain,
inputs: List[CardanoTxInputType],
tx_body_hash: bytes,
protocol_magic: int,
) -> List[Tuple[bytes, bytes, bytes, bytes]]:
byron_witnesses = []
for input in inputs:
if not is_byron_path(input.address_n):
continue
node = keychain.derive(input.address_n)
public_key = remove_ed25519_prefix(node.public_key())
@ -151,31 +236,69 @@ def _build_witnesses(
chain_code = node.chain_code()
address_attributes = cbor.encode(get_address_attributes(protocol_magic))
result.append((public_key, signature, chain_code, address_attributes))
byron_witnesses.append((public_key, signature, chain_code, address_attributes))
return result
return byron_witnesses
async def _show_tx(
ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx
) -> None:
total_amount = await _show_outputs(ctx, keychain, msg)
await confirm_transaction(ctx, total_amount, msg.fee, msg.protocol_magic)
async def _show_outputs(
ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx
) -> int:
total_amount = 0
for output in msg.outputs:
if _should_hide_output(output.address_n, msg.inputs):
continue
total_amount += output.amount
if not output.address:
address, _ = derive_address_and_node(
keychain, output.address_n, msg.protocol_magic
if output.address_parameters:
address = derive_human_readable_address(
keychain, output.address_parameters, msg.protocol_magic, msg.network_id
)
await _show_change_output_staking_warnings(
ctx, keychain, output.address_parameters, address, output.amount
)
if _should_hide_output(output.address_parameters.address_n, msg.inputs):
continue
else:
address = output.address
total_amount += output.amount
await confirm_sending(ctx, output.amount, address)
await confirm_transaction(ctx, total_amount, msg.fee, msg.protocol_magic)
return total_amount
async def _show_change_output_staking_warnings(
ctx: wire.Context,
keychain: seed.Keychain,
address_parameters: CardanoAddressParametersType,
address: str,
amount: int,
):
address_type = address_parameters.address_type
staking_use_case = staking_use_cases.get(keychain, address_parameters)
if staking_use_case == staking_use_cases.NO_STAKING:
await show_warning_tx_no_staking_info(ctx, address_type, amount)
elif staking_use_case == staking_use_cases.POINTER_ADDRESS:
await show_warning_tx_pointer_address(
ctx, address_parameters.certificate_pointer, amount,
)
elif staking_use_case == staking_use_cases.MISMATCH:
if address_parameters.address_n_staking:
await show_warning_tx_different_staking_account(
ctx, to_account_path(address_parameters.address_n_staking), amount,
)
else:
await show_warning_tx_staking_key_hash(
ctx, address_parameters.staking_key_hash, amount,
)
# addresses from the same account as inputs should be hidden

View File

@ -61,7 +61,9 @@ def bech32_encode(hrp: str, data: List[int]) -> str:
return hrp + "1" + "".join([CHARSET[d] for d in combined])
def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]:
def bech32_decode(
bech: str, max_bech_len: int = 90
) -> Tuple[Optional[str], Optional[List[int]]]:
"""Validate a Bech32 string, and determine HRP and data."""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
@ -69,7 +71,7 @@ def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]:
return (None, None)
bech = bech.lower()
pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
if pos < 1 or pos + 7 > len(bech) or len(bech) > max_bech_len:
return (None, None)
if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None)

View File

@ -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),
}

View File

@ -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]

View File

@ -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),
}

View File

@ -2,6 +2,8 @@
# fmt: off
import protobuf as p
from .CardanoAddressParametersType import CardanoAddressParametersType
if __debug__:
try:
from typing import Dict, List # noqa: F401
@ -15,18 +17,21 @@ class CardanoGetAddress(p.MessageType):
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
protocol_magic: int = None,
network_id: int = None,
address_parameters: CardanoAddressParametersType = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
self.protocol_magic = protocol_magic
self.network_id = network_id
self.address_parameters = address_parameters
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
3: ('protocol_magic', p.UVarintType, 0),
4: ('network_id', p.UVarintType, 0),
5: ('address_parameters', CardanoAddressParametersType, 0),
}

View File

@ -23,12 +23,14 @@ class CardanoSignTx(p.MessageType):
protocol_magic: int = None,
fee: int = None,
ttl: int = None,
network_id: int = None,
) -> None:
self.inputs = inputs if inputs is not None else []
self.outputs = outputs if outputs is not None else []
self.protocol_magic = protocol_magic
self.fee = fee
self.ttl = ttl
self.network_id = network_id
@classmethod
def get_fields(cls) -> Dict:
@ -38,4 +40,5 @@ class CardanoSignTx(p.MessageType):
5: ('protocol_magic', p.UVarintType, 0),
6: ('fee', p.UVarintType, 0),
7: ('ttl', p.UVarintType, 0),
8: ('network_id', p.UVarintType, 0),
}

View File

@ -2,6 +2,8 @@
# fmt: off
import protobuf as p
from .CardanoAddressParametersType import CardanoAddressParametersType
if __debug__:
try:
from typing import Dict, List # noqa: F401
@ -15,17 +17,17 @@ class CardanoTxOutputType(p.MessageType):
def __init__(
self,
address: str = None,
address_n: List[int] = None,
amount: int = None,
address_parameters: CardanoAddressParametersType = None,
) -> None:
self.address = address
self.address_n = address_n if address_n is not None else []
self.amount = amount
self.address_parameters = address_parameters
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address', p.UnicodeType, 0),
2: ('address_n', p.UVarintType, p.FLAG_REPEATED),
3: ('amount', p.UVarintType, 0),
4: ('address_parameters', CardanoAddressParametersType, 0),
}

View File

@ -1,16 +1,22 @@
from common import *
from apps.common import seed
from apps.common import HARDENED
from trezor import wire
from trezor.crypto import bip32, slip39
from trezor.messages import CardanoAddressType
from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType
from trezor.messages.CardanoBlockchainPointerType import CardanoBlockchainPointerType
from apps.common import HARDENED, seed
if not utils.BITCOIN_ONLY:
from apps.cardano import protocol_magics
from apps.cardano.address import (
derive_human_readable_address,
validate_full_path,
)
from apps.cardano.byron_address import (
_get_address_root,
_address_hash,
validate_full_path,
derive_address_and_node
)
from apps.cardano.helpers import network_ids, protocol_magics
from apps.cardano.seed import Keychain
@ -20,8 +26,6 @@ class TestCardanoAddress(unittest.TestCase):
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
addresses = [
@ -32,7 +36,11 @@ class TestCardanoAddress(unittest.TestCase):
for i, expected in enumerate(addresses):
# 44'/1815'/0'/0/i'
address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i], protocol_magics.MAINNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i],
)
address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET)
self.assertEqual(expected, address)
nodes = [
@ -57,7 +65,7 @@ class TestCardanoAddress(unittest.TestCase):
]
for i, (priv, ext, pub, chain) in enumerate(nodes):
_, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i], protocol_magics.MAINNET)
n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0x80000000 + i])
self.assertEqual(hexlify(n.private_key()), priv)
self.assertEqual(hexlify(n.private_key_ext()), ext)
self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub)
@ -67,8 +75,6 @@ class TestCardanoAddress(unittest.TestCase):
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
addresses = [
@ -79,7 +85,11 @@ class TestCardanoAddress(unittest.TestCase):
for i, expected in enumerate(addresses):
# 44'/1815'/0'/0/i
address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i],
)
address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET)
self.assertEqual(address, expected)
nodes = [
@ -104,7 +114,7 @@ class TestCardanoAddress(unittest.TestCase):
]
for i, (priv, ext, pub, chain) in enumerate(nodes):
_, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET)
n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i])
self.assertEqual(hexlify(n.private_key()), priv)
self.assertEqual(hexlify(n.private_key_ext()), ext)
self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub)
@ -115,12 +125,14 @@ class TestCardanoAddress(unittest.TestCase):
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
# 44'/1815'
address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815], protocol_magics.MAINNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815],
)
address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET)
self.assertEqual(address, "Ae2tdPwUPEZ2FGHX3yCKPSbSgyuuTYgMxNq652zKopxT4TuWvEd8Utd92w3")
priv, ext, pub, chain = (
@ -130,7 +142,7 @@ class TestCardanoAddress(unittest.TestCase):
b"02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a"
)
_, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815], protocol_magics.MAINNET)
n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815])
self.assertEqual(hexlify(n.private_key()), priv)
self.assertEqual(hexlify(n.private_key_ext()), ext)
self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub)
@ -198,8 +210,6 @@ class TestCardanoAddress(unittest.TestCase):
self.assertEqual(hexlify(node.chain_code()), root_chain)
# Check derived nodes and addresses.
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
nodes = [
@ -228,7 +238,12 @@ class TestCardanoAddress(unittest.TestCase):
for i, (address, priv, ext, pub, chain) in enumerate(nodes):
# 44'/1815'/0'/0/i
a, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i],
)
a = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET)
n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i])
self.assertEqual(a, address)
self.assertEqual(hexlify(n.private_key()), priv)
self.assertEqual(hexlify(n.private_key_ext()), ext)
@ -262,8 +277,6 @@ class TestCardanoAddress(unittest.TestCase):
self.assertEqual(hexlify(node.chain_code()), root_chain)
# Check derived nodes and addresses.
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
nodes = [
@ -292,19 +305,274 @@ class TestCardanoAddress(unittest.TestCase):
for i, (address, priv, ext, pub, chain) in enumerate(nodes):
# 44'/1815'/0'/0/i
a, n = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.MAINNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i],
)
a = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_ids.MAINNET)
n = keychain.derive([0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i])
self.assertEqual(a, address)
self.assertEqual(hexlify(n.private_key()), priv)
self.assertEqual(hexlify(n.private_key_ext()), ext)
self.assertEqual(hexlify(seed.remove_ed25519_prefix(n.public_key())), pub)
self.assertEqual(hexlify(n.chain_code()), chain)
def test_testnet_address(self):
def test_base_address(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)
test_vectors = [
# network id, account, expected result
# data generated with code under test
(network_ids.MAINNET, 4, "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"),
(network_ids.TESTNET, 4, "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"),
]
for network_id, account, expected_address in test_vectors:
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BASE,
address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0],
address_n_staking=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 2, 0]
)
actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id)
self.assertEqual(actual_address, expected_address)
def test_base_address_with_staking_key_hash(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)
test_vectors = [
# network id, account, staking key hash, expected result
# own staking key hash
# data generated with code under test
(network_ids.MAINNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r"),
(network_ids.TESTNET, 4, unhexlify("1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlswzkqcu"),
# staking key hash not owned - derived with "all all..." mnenomnic, data generated with code under test
(network_ids.MAINNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsxrrvc2"),
(network_ids.MAINNET, 0, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzersj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms6xjnst"),
(network_ids.TESTNET, 4, unhexlify("122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277"), "addr_test1qr4sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms947v54"),
]
for network_id, account, staking_key_hash, expected_address in test_vectors:
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BASE,
address_n=[1852 | HARDENED, 1815 | HARDENED, account | HARDENED, 0, 0],
staking_key_hash=staking_key_hash,
)
actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id)
self.assertEqual(actual_address, expected_address)
def test_base_address_with_invalid_parameters(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)
# both address_n_staking and staking_key_hash are None
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BASE,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
address_n_staking=None,
staking_key_hash=None,
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
# address_n_staking is not a staking path
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BASE,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
address_n_staking=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
staking_key_hash=None,
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
def test_enterprise_address(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)
test_vectors = [
# network id, expected result
(network_ids.MAINNET, "addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8"),
(network_ids.TESTNET, "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz")
]
for network_id, expected_address in test_vectors:
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.ENTERPRISE,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
)
actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id)
self.assertEqual(actual_address, expected_address)
def test_pointer_address(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)
test_vectors = [
# network id, pointer, expected result
(network_ids.MAINNET, CardanoBlockchainPointerType(1, 2, 3), "addr1gx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpse33frd"),
(network_ids.TESTNET, CardanoBlockchainPointerType(24157, 177, 42), "addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5ph3wczvf2pfz4ly")
]
for network_id, pointer, expected_address in test_vectors:
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=pointer,
)
actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id)
self.assertEqual(actual_address, expected_address)
def test_pointer_address_invalid_pointers(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)
# pointer is None
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=None,
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
# block index is None
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=CardanoBlockchainPointerType(None, 2, 3),
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
# tx index is None
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=CardanoBlockchainPointerType(1, None, 3),
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
# certificate index is None
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=CardanoBlockchainPointerType(1, 2, None),
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
def test_reward_address(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)
test_vectors = [
# network id, expected result
(network_ids.MAINNET, "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz"),
(network_ids.TESTNET, "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl")
]
for network_id, expected_address in test_vectors:
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.REWARD,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 2, 0],
)
actual_address = derive_human_readable_address(keychain, address_parameters, protocol_magics.MAINNET, network_id)
self.assertEqual(actual_address, expected_address)
def test_reward_address_with_non_staking_path(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)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.REWARD,
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0]
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
def test_shelley_address_with_byron_namespace(self):
"""
It shouldn't be possible to derive Shelley addresses
(Base, Pointer, Enterprise, Reward) with a Byron namespace (44')
"""
mnemonic = "test walk nut penalty hip pave soap entry language right filter choice"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
keychain = Keychain(node)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BASE,
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0]
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.POINTER,
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
certificate_pointer=CardanoBlockchainPointerType(0, 0, 0)
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.ENTERPRISE,
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.REWARD,
address_n=[44 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
def test_byron_address_with_shelley_namespace(self):
"""
It shouldn't be possible to derive Byron addresses
with a Shelley namespace (1852')
"""
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
keychain = Keychain(node)
with self.assertRaises(wire.DataError):
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[1852 | HARDENED, 1815 | HARDENED, 0 | HARDENED, 0, 0],
)
derive_human_readable_address(keychain, address_parameters, 0, 0)
def test_testnet_byron_address(self):
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
addresses = [
@ -315,7 +583,11 @@ class TestCardanoAddress(unittest.TestCase):
for i, expected in enumerate(addresses):
# 44'/1815'/0'/0/i'
address, _ = derive_address_and_node(keychain, [0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i], protocol_magics.TESTNET)
address_parameters = CardanoAddressParametersType(
address_type=CardanoAddressType.BYRON,
address_n=[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, i],
)
address = derive_human_readable_address(keychain, address_parameters, protocol_magics.TESTNET, 0)
self.assertEqual(expected, address)
if __name__ == '__main__':

View File

@ -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()

View File

@ -11,8 +11,6 @@ class TestCardanoGetPublicKey(unittest.TestCase):
mnemonic = "all all all all all all all all all all all all"
passphrase = ""
node = bip32.from_mnemonic_cardano(mnemonic, passphrase)
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
derivation_paths = [
@ -20,6 +18,11 @@ class TestCardanoGetPublicKey(unittest.TestCase):
[0x80000000 | 44, 0x80000000 | 1815],
[0x80000000 | 44, 0x80000000 | 1815, 0, 0, 0],
[0x80000000 | 44, 0x80000000 | 1815, 0x80000000, 0, 0],
[0x80000000 | 1852, 0x80000000 | 1815, 0x80000000, 0, 0x80000000],
[0x80000000 | 1852, 0x80000000 | 1815],
[0x80000000 | 1852, 0x80000000 | 1815, 0, 0, 0],
[0x80000000 | 1852, 0x80000000 | 1815, 0x80000000, 0, 0],
]
public_keys = [
@ -27,6 +30,11 @@ class TestCardanoGetPublicKey(unittest.TestCase):
b'8c47ebce34234d04fd3dfbac33feaba6133e4e3d77c4b5ab18120ec6878ad4ce',
b'17cc0bf978756d0d5c76f931629036a810c61801b78beecb44555773d13e3791',
b'b90fb812a2268e9569ff1172e8daed1da3dc7e72c7bded7c5bcb7282039f90d5',
b'f698a764b23aa6667b1157fc4247c6a1b58c21a3865ac6a47a3590167a9e0211',
b'e9c46841be76e3be0289694fd5c7503c04f40e5b036abac200b98a9006cf6647',
b'6d225f078ca611f00d86cbfd8ba6c6ac7826721434eae6525686efb878b72370',
b'5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1',
]
chain_codes = [
@ -34,6 +42,11 @@ class TestCardanoGetPublicKey(unittest.TestCase):
b'02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a',
b'646ac4a6295326bae6831be05921edfbcb362de48dfd37b12e74c227dfad768d',
b'fd8e71c1543de2cdc7f7623130c5f2cceb53549055fa1f5bc88199989e08cce7',
b'13cfb6de37a568aae56cadac907e6469b121464fe1b70a10c213eaea2cbb6636',
b'58f3f46f4a93e7a4431e75b10af7497b747c3053cb7466ed53f4277e78a63c52',
b'f72b3c361381db2d88289440268c94c5e7467c9414375e6b63d03026750f3c66',
b'f123474e140a2c360b01f0fa66f2f22e2e965a5b07a80358cf75f77abbd66088',
]
xpub_keys = [
@ -41,6 +54,11 @@ class TestCardanoGetPublicKey(unittest.TestCase):
'8c47ebce34234d04fd3dfbac33feaba6133e4e3d77c4b5ab18120ec6878ad4ce02ac67c59a8b0264724a635774ca2c242afa10d7ab70e2bf0a8f7d4bb10f1f7a',
'17cc0bf978756d0d5c76f931629036a810c61801b78beecb44555773d13e3791646ac4a6295326bae6831be05921edfbcb362de48dfd37b12e74c227dfad768d',
'b90fb812a2268e9569ff1172e8daed1da3dc7e72c7bded7c5bcb7282039f90d5fd8e71c1543de2cdc7f7623130c5f2cceb53549055fa1f5bc88199989e08cce7',
'f698a764b23aa6667b1157fc4247c6a1b58c21a3865ac6a47a3590167a9e021113cfb6de37a568aae56cadac907e6469b121464fe1b70a10c213eaea2cbb6636',
'e9c46841be76e3be0289694fd5c7503c04f40e5b036abac200b98a9006cf664758f3f46f4a93e7a4431e75b10af7497b747c3053cb7466ed53f4277e78a63c52',
'6d225f078ca611f00d86cbfd8ba6c6ac7826721434eae6525686efb878b72370f72b3c361381db2d88289440268c94c5e7467c9414375e6b63d03026750f3c66',
'5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1f123474e140a2c360b01f0fa66f2f22e2e965a5b07a80358cf75f77abbd66088',
]
for index, derivation_path in enumerate(derivation_paths):
@ -65,8 +83,6 @@ class TestCardanoGetPublicKey(unittest.TestCase):
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
# 44'/1815'/0'/0/i
@ -116,8 +132,6 @@ class TestCardanoGetPublicKey(unittest.TestCase):
node = bip32.from_seed(master_secret, "ed25519 cardano seed")
node.derive_cardano(0x80000000 | 44)
node.derive_cardano(0x80000000 | 1815)
keychain = Keychain(node)
# 44'/1815'/0'/0/i

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -20,19 +20,130 @@ from . import messages, tools
from .tools import expect
PROTOCOL_MAGICS = {"mainnet": 764824073, "testnet": 42}
NETWORK_IDS = {"mainnet": 1, "testnet": 0}
REQUIRED_FIELDS_TRANSACTION = ("inputs", "outputs")
REQUIRED_FIELDS_INPUT = ("path", "prev_hash", "prev_index")
INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields"
ADDRESS_TYPES = (
messages.CardanoAddressType.BYRON,
messages.CardanoAddressType.BASE,
messages.CardanoAddressType.POINTER,
messages.CardanoAddressType.ENTERPRISE,
messages.CardanoAddressType.REWARD,
)
def create_address_parameters(
address_type: messages.CardanoAddressType,
address_n: List[int],
address_n_staking: List[int] = None,
staking_key_hash: bytes = None,
block_index: int = None,
tx_index: int = None,
certificate_index: int = None,
) -> messages.CardanoAddressParametersType:
certificate_pointer = None
if address_type not in ADDRESS_TYPES:
raise ValueError("Unknown address type")
if address_type == messages.CardanoAddressType.POINTER:
certificate_pointer = create_certificate_pointer(
block_index, tx_index, certificate_index
)
return messages.CardanoAddressParametersType(
address_type=address_type,
address_n=address_n,
address_n_staking=address_n_staking,
staking_key_hash=staking_key_hash,
certificate_pointer=certificate_pointer,
)
def create_certificate_pointer(
block_index: int, tx_index: int, certificate_index: int
) -> messages.CardanoBlockchainPointerType:
if block_index is None or tx_index is None or certificate_index is None:
raise ValueError("Invalid pointer parameters")
return messages.CardanoBlockchainPointerType(
block_index=block_index, tx_index=tx_index, certificate_index=certificate_index
)
def create_input(input) -> messages.CardanoTxInputType:
if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT):
raise ValueError("The input is missing some fields")
path = input["path"]
return messages.CardanoTxInputType(
address_n=tools.parse_path(path),
prev_hash=bytes.fromhex(input["prev_hash"]),
prev_index=input["prev_index"],
)
def create_output(output) -> messages.CardanoTxOutputType:
contains_address = output.get("address") is not None
contains_address_type = output.get("addressType") is not None
if output.get("amount") is None:
raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE)
if not (contains_address or contains_address_type):
raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE)
if contains_address:
return messages.CardanoTxOutputType(
address=output["address"], amount=int(output["amount"])
)
else:
return _create_change_output(output)
def _create_change_output(output) -> messages.CardanoTxOutputType:
if output.get("path") is None:
raise ValueError(INCOMPLETE_OUTPUT_ERROR_MESSAGE)
staking_key_hash_bytes = None
if output.get("stakingKeyHash"):
staking_key_hash_bytes = bytes.fromhex(output.get("stakingKeyHash"))
address_parameters = create_address_parameters(
int(output["addressType"]),
tools.parse_path(output["path"]),
tools.parse_path(output.get("stakingPath")),
staking_key_hash_bytes,
output.get("blockIndex"),
output.get("txIndex"),
output.get("certificateIndex"),
)
return messages.CardanoTxOutputType(
address_parameters=address_parameters, amount=int(output["amount"])
)
# ====== Client functions ====== #
@expect(messages.CardanoAddress, field="address")
def get_address(
client, address_n: List[int], protocol_magic: int, show_display=False
client,
address_parameters: messages.CardanoAddressParametersType,
protocol_magic: int,
network_id: int,
show_display=False,
) -> messages.CardanoAddress:
return client.call(
messages.CardanoGetAddress(
address_n=address_n,
address_parameters=address_parameters,
protocol_magic=protocol_magic,
network_id=network_id,
show_display=show_display,
)
)
@ -51,6 +162,7 @@ def sign_tx(
fee: int,
ttl: int,
protocol_magic: int,
network_id: int,
) -> messages.CardanoSignedTx:
response = client.call(
messages.CardanoSignTx(
@ -59,36 +171,8 @@ def sign_tx(
fee=fee,
ttl=ttl,
protocol_magic=protocol_magic,
network_id=network_id,
)
)
return response
def create_input(input) -> messages.CardanoTxInputType:
if not all(input.get(k) is not None for k in REQUIRED_FIELDS_INPUT):
raise ValueError("The input is missing some fields")
path = input["path"]
return messages.CardanoTxInputType(
address_n=tools.parse_path(path),
prev_hash=bytes.fromhex(input["prev_hash"]),
prev_index=input["prev_index"],
)
def create_output(output) -> messages.CardanoTxOutputType:
if not output.get("amount") or not (output.get("address") or output.get("path")):
raise ValueError("The output is missing some fields")
if output.get("path"):
path = output["path"]
return messages.CardanoTxOutputType(
address_n=tools.parse_path(path), amount=int(output["amount"])
)
return messages.CardanoTxOutputType(
address=output["address"], amount=int(output["amount"])
)

View File

@ -18,11 +18,19 @@ import json
import click
from .. import cardano, tools
from . import with_client
from .. import cardano, messages, tools
from . import ChoiceType, with_client
PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0"
ADDRESS_TYPES = {
"byron": messages.CardanoAddressType.BYRON,
"base": messages.CardanoAddressType.BASE,
"pointer": messages.CardanoAddressType.POINTER,
"enterprise": messages.CardanoAddressType.ENTERPRISE,
"reward": messages.CardanoAddressType.REWARD,
}
@click.group(name="cardano")
def cli():
@ -37,19 +45,27 @@ def cli():
required=True,
help="Transaction in JSON format",
)
@click.option("-p", "--protocol-magic", type=int, default=1)
@click.option(
"-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"]
)
@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"])
@click.option("-t", "--testnet", is_flag=True)
@with_client
def sign_tx(client, file, protocol_magic):
def sign_tx(client, file, protocol_magic, network_id, testnet):
"""Sign Cardano transaction."""
transaction = json.load(file)
if testnet:
protocol_magic = cardano.PROTOCOL_MAGICS["testnet"]
network_id = cardano.NETWORK_IDS["testnet"]
inputs = [cardano.create_input(input) for input in transaction["inputs"]]
outputs = [cardano.create_output(output) for output in transaction["outputs"]]
fee = transaction["fee"]
ttl = transaction["ttl"]
signed_transaction = cardano.sign_tx(
client, inputs, outputs, fee, ttl, protocol_magic
client, inputs, outputs, fee, ttl, protocol_magic, network_id
)
return {
@ -61,12 +77,67 @@ def sign_tx(client, file, protocol_magic):
@cli.command()
@click.option("-n", "--address", required=True, help=PATH_HELP)
@click.option("-d", "--show-display", is_flag=True)
@click.option("-p", "--protocol-magic", type=int, default=1)
@click.option("-t", "--address-type", type=ChoiceType(ADDRESS_TYPES), default="base")
@click.option("-s", "--staking-address", type=str, default=None)
@click.option("-h", "--staking-key-hash", type=str, default=None)
@click.option("-b", "--block_index", type=int, default=None)
@click.option("-x", "--tx_index", type=int, default=None)
@click.option("-c", "--certificate_index", type=int, default=None)
@click.option(
"-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"]
)
@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"])
@click.option("-e", "--testnet", is_flag=True)
@with_client
def get_address(client, address, show_display, protocol_magic):
"""Get Cardano address."""
address_n = tools.parse_path(address)
return cardano.get_address(client, address_n, protocol_magic, show_display)
def get_address(
client,
address,
address_type,
staking_address,
staking_key_hash,
block_index,
tx_index,
certificate_index,
protocol_magic,
network_id,
show_display,
testnet,
):
"""
Get Cardano address.
All address types require the address, address_type, protocol_magic and
network_id parameters.
When deriving a base address you can choose to include staking info as
staking_address or staking_key_hash - one has to be chosen.
When deriving a pointer address you need to specify the block_index,
tx_index and certificate_index parameters.
Byron, enterprise and reward addresses only require the general parameters.
"""
if testnet:
protocol_magic = cardano.PROTOCOL_MAGICS["testnet"]
network_id = cardano.NETWORK_IDS["testnet"]
staking_key_hash_bytes = None
if staking_key_hash:
staking_key_hash_bytes = bytes.fromhex(staking_key_hash)
address_parameters = cardano.create_address_parameters(
address_type,
tools.parse_path(address),
tools.parse_path(staking_address),
staking_key_hash_bytes,
block_index,
tx_index,
certificate_index,
)
return cardano.get_address(
client, address_parameters, protocol_magic, network_id, show_display
)
@cli.command()

View File

@ -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),
}

View File

@ -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]

View File

@ -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),
}

View File

@ -2,6 +2,8 @@
# fmt: off
from .. import protobuf as p
from .CardanoAddressParametersType import CardanoAddressParametersType
if __debug__:
try:
from typing import Dict, List # noqa: F401
@ -15,18 +17,21 @@ class CardanoGetAddress(p.MessageType):
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
protocol_magic: int = None,
network_id: int = None,
address_parameters: CardanoAddressParametersType = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
self.protocol_magic = protocol_magic
self.network_id = network_id
self.address_parameters = address_parameters
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
3: ('protocol_magic', p.UVarintType, 0),
4: ('network_id', p.UVarintType, 0),
5: ('address_parameters', CardanoAddressParametersType, 0),
}

View File

@ -23,12 +23,14 @@ class CardanoSignTx(p.MessageType):
protocol_magic: int = None,
fee: int = None,
ttl: int = None,
network_id: int = None,
) -> None:
self.inputs = inputs if inputs is not None else []
self.outputs = outputs if outputs is not None else []
self.protocol_magic = protocol_magic
self.fee = fee
self.ttl = ttl
self.network_id = network_id
@classmethod
def get_fields(cls) -> Dict:
@ -38,4 +40,5 @@ class CardanoSignTx(p.MessageType):
5: ('protocol_magic', p.UVarintType, 0),
6: ('fee', p.UVarintType, 0),
7: ('ttl', p.UVarintType, 0),
8: ('network_id', p.UVarintType, 0),
}

View File

@ -2,6 +2,8 @@
# fmt: off
from .. import protobuf as p
from .CardanoAddressParametersType import CardanoAddressParametersType
if __debug__:
try:
from typing import Dict, List # noqa: F401
@ -15,17 +17,17 @@ class CardanoTxOutputType(p.MessageType):
def __init__(
self,
address: str = None,
address_n: List[int] = None,
amount: int = None,
address_parameters: CardanoAddressParametersType = None,
) -> None:
self.address = address
self.address_n = address_n if address_n is not None else []
self.amount = amount
self.address_parameters = address_parameters
@classmethod
def get_fields(cls) -> Dict:
return {
1: ('address', p.UnicodeType, 0),
2: ('address_n', p.UVarintType, p.FLAG_REPEATED),
3: ('amount', p.UVarintType, 0),
4: ('address_parameters', CardanoAddressParametersType, 0),
}

View File

@ -21,6 +21,8 @@ from .ButtonAck import ButtonAck
from .ButtonRequest import ButtonRequest
from .Cancel import Cancel
from .CardanoAddress import CardanoAddress
from .CardanoAddressParametersType import CardanoAddressParametersType
from .CardanoBlockchainPointerType import CardanoBlockchainPointerType
from .CardanoGetAddress import CardanoGetAddress
from .CardanoGetPublicKey import CardanoGetPublicKey
from .CardanoPublicKey import CardanoPublicKey
@ -277,6 +279,7 @@ from . import BinanceOrderType
from . import BinanceTimeInForce
from . import ButtonRequestType
from . import Capability
from . import CardanoAddressType
from . import DebugLinkShowTextStyle
from . import DebugSwipeDirection
from . import FailureType

View File

@ -16,10 +16,14 @@
import pytest
from trezorlib.cardano import PROTOCOL_MAGICS, get_address
from trezorlib.tools import parse_path
from ..common import MNEMONIC12
from trezorlib import tools
from trezorlib.cardano import (
NETWORK_IDS,
PROTOCOL_MAGICS,
create_address_parameters,
get_address,
)
from trezorlib.messages import CardanoAddressType
@pytest.mark.altcoin
@ -32,38 +36,250 @@ from ..common import MNEMONIC12
(
"m/44'/1815'/0'/0/0",
PROTOCOL_MAGICS["mainnet"],
"Ae2tdPwUPEZLCq3sFv4wVYxwqjMH2nUzBVt1HFr4v87snYrtYq3d3bq2PUQ",
"Ae2tdPwUPEZ5YUb8sM3eS8JqKgrRLzhiu71crfuH2MFtqaYr5ACNRdsswsZ",
),
(
"m/44'/1815'/0'/0/1",
PROTOCOL_MAGICS["mainnet"],
"Ae2tdPwUPEZEY6pVJoyuNNdLp7VbMB7U7qfebeJ7XGunk5Z2eHarkcN1bHK",
"Ae2tdPwUPEZJb8r1VZxweSwHDTYtqeYqF39rZmVbrNK62JHd4Wd7Ytsc8eG",
),
(
"m/44'/1815'/0'/0/2",
PROTOCOL_MAGICS["mainnet"],
"Ae2tdPwUPEZ3gZD1QeUHvAqadAV59Zid6NP9VCR9BG5LLAja9YtBUgr6ttK",
"Ae2tdPwUPEZFm6Y7aPZGKMyMAK16yA5pWWKU9g73ncUQNZsAjzjhszenCsq",
),
# testnet
# data generated by code under test
(
"m/44'/1815'/0'/0/0",
PROTOCOL_MAGICS["testnet"],
"2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r",
"2657WMsDfac5F3zbgs9BwNWx3dhGAJERkAL93gPa68NJ2i8mbCHm2pLUHWSj8Mfea",
),
(
"m/44'/1815'/0'/0/1",
PROTOCOL_MAGICS["testnet"],
"2657WMsDfac61ebUDw53WUX49Dcfya8S8G7iYbhN4nP8JSFuh38T1LuFax1bUnhxA",
"2657WMsDfac6ezKWszxLFqJjSUgpg9NgxKc1koqi24sVpRaPhiwMaExk4useKn5HA",
),
(
"m/44'/1815'/0'/0/2",
PROTOCOL_MAGICS["testnet"],
"2657WMsDfac5PMpEsxc1md3pgZKUZRZ11MUK8tjkDHBQG9b3TMBsTQc4PmmumVrcn",
"2657WMsDfac7hr1ioJGr6g7r6JRx4r1My8Rj91tcPTeVjJDpfBYKURrPG2zVLx2Sq",
),
],
)
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
def test_cardano_get_address(client, path, protocol_magic, expected_address):
address = get_address(client, parse_path(path), protocol_magic)
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.BYRON, address_n=tools.parse_path(path),
),
protocol_magic=protocol_magic,
network_id=NETWORK_IDS["mainnet"],
)
assert address == expected_address
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"path, staking_path, network_id, expected_address",
[
# data generated with code under test
(
"m/1852'/1815'/4'/0/0",
"m/1852'/1815'/4'/2/0",
NETWORK_IDS["mainnet"],
"addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqsx9990",
),
(
"m/1852'/1815'/4'/0/0",
"m/1852'/1815'/4'/2/0",
NETWORK_IDS["testnet"],
"addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wnrqua2vw243tmjfjt0h5wsru6appuz8c0pfd75ur7myyeqnsc9fs",
),
],
)
def test_cardano_get_base_address(
client, path, staking_path, network_id, expected_address
):
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.BASE,
address_n=tools.parse_path(path),
address_n_staking=tools.parse_path(staking_path),
),
protocol_magic=PROTOCOL_MAGICS["mainnet"],
network_id=network_id,
)
assert address == expected_address
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"path, staking_key_hash, network_id, expected_address",
[
# data generated with code under test
(
"m/1852'/1815'/4'/0/0",
"1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff",
NETWORK_IDS["mainnet"],
"addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsydc62k",
),
(
"m/1852'/1815'/4'/0/0",
"1bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff",
NETWORK_IDS["testnet"],
"addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hls8m96xf",
),
# staking key hash not owned - derived with "all all..." mnenomnic, data generated with code under test
(
"m/1852'/1815'/4'/0/0",
"122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277",
NETWORK_IDS["mainnet"],
"addr1q8v42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfms06skxl",
),
(
"m/1852'/1815'/0'/0/0",
"122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277",
NETWORK_IDS["testnet"],
"addr_test1qzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsu8d9w5",
),
(
"m/1852'/1815'/4'/0/0",
"122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b4277",
NETWORK_IDS["testnet"],
"addr_test1qrv42wjda8r6mpfj40d36znlgfdcqp7jtj03ah8skh6u8wsj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsvvdk2q",
),
],
)
def test_cardano_get_base_address_with_staking_key_hash(
client, path, staking_key_hash, network_id, expected_address
):
# data form shelley test vectors
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.BASE,
address_n=tools.parse_path(path),
staking_key_hash=bytes.fromhex(staking_key_hash),
),
protocol_magic=PROTOCOL_MAGICS["mainnet"],
network_id=network_id,
)
assert address == expected_address
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"path, network_id, expected_address",
[
# data generated with code under test
(
"m/1852'/1815'/0'/0/0",
NETWORK_IDS["mainnet"],
"addr1vxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92su77c6m",
),
(
"m/1852'/1815'/0'/0/0",
NETWORK_IDS["testnet"],
"addr_test1vzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92s8k2y47",
),
],
)
def test_cardano_get_enterprise_address(client, path, network_id, expected_address):
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.ENTERPRISE,
address_n=tools.parse_path(path),
),
protocol_magic=PROTOCOL_MAGICS["mainnet"],
network_id=network_id,
)
assert address == expected_address
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"path, block_index, tx_index, certificate_index, network_id, expected_address",
[
# data generated with code under test
(
"m/1852'/1815'/0'/0/0",
1,
2,
3,
NETWORK_IDS["mainnet"],
"addr1gxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92spqgpsl97q83",
),
(
"m/1852'/1815'/0'/0/0",
24157,
177,
42,
NETWORK_IDS["testnet"],
"addr_test1gzq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z925ph3wczvf2ag2x9t",
),
],
)
def test_cardano_get_pointer_address(
client,
path,
block_index,
tx_index,
certificate_index,
network_id,
expected_address,
):
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.POINTER,
address_n=tools.parse_path(path),
block_index=block_index,
tx_index=tx_index,
certificate_index=certificate_index,
),
protocol_magic=PROTOCOL_MAGICS["mainnet"],
network_id=network_id,
)
assert address == expected_address
@pytest.mark.altcoin
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"path, network_id, expected_address",
[
# data generated with code under test
(
"m/1852'/1815'/0'/2/0",
NETWORK_IDS["mainnet"],
"stake1uyfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yacalmqha",
),
(
"m/1852'/1815'/0'/2/0",
NETWORK_IDS["testnet"],
"stake_test1uqfz49rtntfa9h0s98f6s28sg69weemgjhc4e8hm66d5yac643znq",
),
],
)
def test_cardano_get_reward_address(client, path, network_id, expected_address):
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.REWARD, address_n=tools.parse_path(path),
),
protocol_magic=PROTOCOL_MAGICS["mainnet"],
network_id=network_id,
)
assert address == expected_address

View File

@ -16,8 +16,9 @@
import pytest
from trezorlib.cardano import PROTOCOL_MAGICS, get_address
from trezorlib.tools import parse_path
from trezorlib import tools
from trezorlib.cardano import PROTOCOL_MAGICS, create_address_parameters, get_address
from trezorlib.messages import CardanoAddressType
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
@ -70,5 +71,13 @@ def test_cardano_get_address(client, path, protocol_magic, expected_address):
assert client.features.passphrase_protection is True
client.use_passphrase("TREZOR")
address = get_address(client, parse_path(path), protocol_magic)
address = get_address(
client,
address_parameters=create_address_parameters(
address_type=CardanoAddressType.BYRON, address_n=tools.parse_path(path),
),
protocol_magic=protocol_magic,
network_id=0,
)
assert address == expected_address
assert address == expected_address

View File

@ -37,14 +37,24 @@ from trezorlib.tools import parse_path
"70f131bb799fd659c997221ad8cae7dcce4e8da701f8101cf15307fd3a3712a1",
),
(
"m/44'/1815'/2'",
"076338cee5ab3dae19f06ccaa80e3d4428cf0e1bdc04243e41bba7be63a90da7",
"5dcdf129f6f2d108292e615c4b67a1fc41a64e6a96130f5c981e5e8e046a6cd7",
"m/1852'/1815'/0'",
"d507c8f866691bd96e131334c355188b1a1d0b2fa0ab11545075aab332d77d9e",
"b19657ad13ee581b56b0f8d744d66ca356b93d42fe176b3de007d53e9c4c4e7a",
),
(
"m/44'/1815'/3'",
"5f769380dc6fd17a4e0f2d23aa359442a712e5e96d7838ebb91eb020003cccc3",
"1197ea234f528987cbac9817ebc31344395b837a3bb7c2332f87e095e70550a5",
"m/1852'/1815'/1'",
"140791584001446365f169c82241c7c214475000180dab39fa0588fc9c3d6d80",
"7f9f812d49816844b52e319857aa75961724ad1a146701679d02d7168622233d",
),
(
"m/1852'/1815'/2'",
"ff6ccc3097ca79fc29fe92a9639c47644746780c63acae10a9e6f03bf5c919dd",
"27d985feabf40d83a30aa4645ff008c068187559dd224ba59e26d0d2dc3598ce",
),
(
"m/1852'/1815'/3'",
"be81ace1f63f4f0cae74dd274a72d7818f238bc764ab3e0dc0beb1945b756dca",
"29034f036a162ac4f9f9f397b2d1f289754bb6633915f26b199e156f81d05c88",
),
],
)

View File

@ -17,21 +17,73 @@
import pytest
from trezorlib import cardano, messages
from trezorlib.cardano import PROTOCOL_MAGICS
from trezorlib.cardano import NETWORK_IDS, PROTOCOL_MAGICS
from trezorlib.exceptions import TrezorFailure
SAMPLE_INPUT = {
"path": "m/44'/1815'/0'/0/1",
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"prev_index": 0,
class InputAction:
"""
Test cases don't use the same input flows. These constants are used to define
the expected input flows for each test case. Corresponding input actions
are then executed on the device to simulate user inputs.
"""
SWIPE = 0
YES = 1
SAMPLE_INPUTS = {
"byron_input": {
"path": "m/44'/1815'/0'/0/1",
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"prev_index": 0,
},
"shelley_input": {
"path": "m/1852'/1815'/0'/0/0",
"prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7",
"prev_index": 0,
},
}
SAMPLE_OUTPUTS = {
"simple_output": {
"simple_byron_output": {
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
"amount": "3003112",
},
"change_output": {"path": "m/44'/1815'/0'/0/1", "amount": "1000000"},
"byron_change_output": {
"addressType": 8,
"path": "m/44'/1815'/0'/0/1",
"amount": "1000000",
},
"simple_shelley_output": {
"address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r",
"amount": "1",
},
"base_address_change_output": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingPath": "m/1852'/1815'/0'/2/0",
"amount": "7120787",
},
"staking_key_hash_output": {
"addressType": 0,
"path": "m/1852'/1815'/0'/0/0",
"stakingKeyHash": "32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc",
"amount": "7120787",
},
"pointer_address_output": {
"addressType": 4,
"path": "m/1852'/1815'/0'/0/0",
"blockIndex": 1,
"txIndex": 2,
"certificateIndex": 3,
"amount": "7120787",
},
"enterprise_address_output": {
"addressType": 6,
"path": "m/1852'/1815'/0'/0/0",
"amount": "7120787",
},
"invalid_address": {
"address": "jsK75PTH2esX8k4Wvxenyz83LJJWToBbVmGrWUer2CHFHanLseh7r3sW5X5q",
"amount": "3003112",
@ -44,14 +96,42 @@ SAMPLE_OUTPUTS = {
"address": "Ae2tdPwUPEZ5YUb8sM3eS8JqKgrRLzhiu71crfuH2MFtqaYr5ACNRZR3Mbm",
"amount": "3003112",
},
"large_simple_output": {
"invalid_base_address_too_short": {
"address": "addr1q89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqj922xhxkn6twlq2wn4q50q352annk3903tj00h45mggqvpjcf",
"amount": "3003112",
},
"invalid_base_address_too_long": {
"address": "addr1q89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfm5zhnjqfc",
"amount": "3003112",
},
"invalid_pointer_address_too_short": {
"address": "addr1g89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcgrfjd3l",
"amount": "3003112",
},
"invalid_pointer_address_too_long": {
"address": "addr1g89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqysszqgqqysszqgqqysszqgqqzpqv0wa7",
"amount": "3003112",
},
"invalid_enterprise_address_too_short": {
"address": "addr1v89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcg0c7m2w",
"amount": "3003112",
},
"invalid_enterprise_address_too_long": {
"address": "addr1v89s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqzp9v4srv",
"amount": "3003112",
},
"large_simple_byron_output": {
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
"amount": "449999999199999999",
},
"testnet_output": {
"address": "2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r",
"address": "2657WMsDfac7BteXkJq5Jzdog4h47fPbkwUM49isuWbYAr2cFRHa3rURP236h9PBe",
"amount": "3003112",
},
"shelley_testnet_output": {
"address": "addr_test1vr9s8py7y68e3x66sscs0wkhlg5ssfrfs65084jrlrqcfqqtmut0e",
"amount": "1",
},
}
VALID_VECTORS = [
@ -59,14 +139,18 @@ VALID_VECTORS = [
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"]],
[SAMPLE_OUTPUTS["simple_byron_output"]],
# fee
42,
# ttl
10,
# input flow
[[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]],
# tx hash
"73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6",
# serialized tx
@ -74,37 +158,166 @@ VALID_VECTORS = [
),
# Mainnet transaction with change
(
# protocol magic (mainnet)
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"], SAMPLE_OUTPUTS["change_output"]],
[SAMPLE_OUTPUTS["simple_byron_output"], SAMPLE_OUTPUTS["byron_change_output"]],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"81b14b7e62972127eb33c0b1198de6430540ad3a98eec621a3194f2baac43a43",
# serialized tx
"83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582b82d818582183581c9e1c71de652ec8b85fec296f0685ca3988781c94a2e1a5d89d92f45fa0001a0d0c25611a002dd2e882582b82d818582183581cda4da43db3fca93695e71dab839e72271204d28b9d964d306b8800a8a0001a7a6916a51a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840d909b16038c4fd772a177038242e6793be39c735430b03ee924ed18026bd28d06920b5846247945f1204276e4b759aa5ac05a4a73b49ce705ab0e5e54a3a170e582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63541a0f6",
),
# Testnet transaction
# simple transaction with base address change output
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["change_output"]],
[
SAMPLE_OUTPUTS["simple_shelley_output"],
SAMPLE_OUTPUTS["base_address_change_output"],
],
# fee
42,
# ttl
10,
# input flow
[[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]],
# tx hash
"5dd03fb44cb88061b2a1c246981bb31adfe4f57be69b58badb5ae8f448450932",
"16fe72bb198be423677577e6326f1f648ec5fc11263b072006382d8125a6edda",
# tx body
"83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa122a946b9ad3d2ddf029d3a828f0468aece76895f15c9efbd69b42771a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c158406a78f07836dcf4a303448d2b16b217265a9226be3984a69a04dba5d04f4dbb2a47b5e1cbb345f474c0b9634a2f37b921ab26e6a65d5dfd015dacb4455fb8430af6",
),
# simple transaction with base address change output with staking key hash
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[
SAMPLE_OUTPUTS["simple_shelley_output"],
SAMPLE_OUTPUTS["staking_key_hash_output"],
],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"d1610bb89bece22ed3158738bc1fbb31c6af0685053e2993361e3380f49afad9",
# tx body
"83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258390180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc1a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840622f22d03bc9651ddc5eb2f5dc709ac4240a64d2b78c70355dd62106543c407d56e8134c4df7884ba67c8a1b5c706fc021df5c4d0ff37385c30572e73c727d00f6",
),
# simple transaction with pointer address change output
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[
SAMPLE_OUTPUTS["simple_shelley_output"],
SAMPLE_OUTPUTS["pointer_address_output"],
],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"40535fa8f88515f1da008d3cdf544cf9dbf1675c3cb0adb13b74b9293f1b7096",
# tx body
"83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff018258204180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa0102031a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840dbbf050cc13d0696b1884113613318a275e6f0f8c7cb3e7828c4f2f3c158b2622a5d65ea247f1eed758a0f6242a52060c319d6f37c8460f5d14be24456cd0b08f6",
),
# simple transaction with enterprise address change output
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[
SAMPLE_OUTPUTS["simple_shelley_output"],
SAMPLE_OUTPUTS["enterprise_address_output"],
],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"d3570557b197604109481a80aeb66cd2cfabc57f802ad593bacc12eb658e5d72",
# tx body
"83a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018282583901eb0baa5e570cffbe2934db29df0b6a3d7c0430ee65d4c3a7ab2fefb91bc428e4720702ebd5dab4fb175324c192dc9bb76cc5da956e3c8dff0182581d6180f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa1a006ca79302182a030aa100818258205d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c15840c5996650c438c4493b2c8a94229621bb9b151b8d61d75fb868c305e917031e9a1654f35023f7dbf5d1839ab9d57b153c7f79c2666af51ecf363780397956e00af6",
),
# Testnet transaction
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
# network id
NETWORK_IDS["testnet"],
# inputs
[SAMPLE_INPUTS["byron_input"]],
# outputs
[
SAMPLE_OUTPUTS["testnet_output"],
SAMPLE_OUTPUTS["shelley_testnet_output"],
SAMPLE_OUTPUTS["byron_change_output"],
],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
[InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"47cf79f20c6c62edb4162b3b232a57afc1bd0b57c7fd8389555276408a004776",
# serialized tx
"83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581c586b90cf80c021db288ce1c18ecfd3610acf64f8748768b0eb7335b1a10242182a001aae3129311a002dd2e882582f82d818582583581c98c3a558f39d1d993cc8770e8825c70a6d0f5a9eb243501c4526c29da10242182a001aa8566c011a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840fc30afdd0d4a6d8581e0f6abe895994d208fd382f2b23ff1553d711477a4fedbd1f68a76e7465c4816d5477f4287f7360acf71fca3b3d5902e4448e48c447106582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63545a10242182af6",
"83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018382582f82d818582583581cc817d85b524e3d073795819a25cdbb84cff6aa2bbb3a081980d248cba10242182a001a0fb6fc611a002dd2e882581d60cb03849e268f989b5a843107bad7fa2908246986a8f3d643f8c184800182582f82d818582583581c98c3a558f39d1d993cc8770e8825c70a6d0f5a9eb243501c4526c29da10242182a001aa8566c011a000f424002182a030aa1028184582089053545a6c254b0d9b1464e48d2b5fcf91d4e25c128afb1fcfc61d0843338ea5840cc11adf81cb3c3b75a438325f8577666f5cbb4d5d6b73fa6dbbcf5ab36897df34eecacdb54c3bc3ce7fc594ebb2c7aa4db4700f4290facad9b611a035af8710a582026308151516f3b0e02bb1638142747863c520273ce9bd3e5cd91e1d46fe2a63545a10242182af6",
),
]
@ -113,8 +326,10 @@ INVALID_VECTORS = [
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_address"]],
# fee
@ -126,10 +341,12 @@ INVALID_VECTORS = [
),
# Output address is invalid CBOR
(
# protocol magic (mainnet)
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_cbor"]],
# fee
@ -143,8 +360,10 @@ INVALID_VECTORS = [
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_crc"]],
# fee
@ -154,14 +373,118 @@ INVALID_VECTORS = [
# error message
"Invalid address",
),
# Fee is too high
# Output base address is too short
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"]],
[SAMPLE_OUTPUTS["invalid_base_address_too_short"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Output base address is too long
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_base_address_too_long"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Output pointer address is too short
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_pointer_address_too_short"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Output pointer address is too long
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_pointer_address_too_long"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Output enterprise address is too short
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_enterprise_address_too_short"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Output enterprise address is too long
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["invalid_enterprise_address_too_long"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Fee is too high
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_byron_output"]],
# fee
45000000000000001,
# ttl
@ -171,12 +494,17 @@ INVALID_VECTORS = [
),
# Output total is too high
(
# protocol magic (mainnet)
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["large_simple_output"], SAMPLE_OUTPUTS["change_output"]],
[
SAMPLE_OUTPUTS["large_simple_byron_output"],
SAMPLE_OUTPUTS["byron_change_output"],
],
# fee
42,
# ttl
@ -188,8 +516,10 @@ INVALID_VECTORS = [
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["testnet_output"]],
# fee
@ -203,10 +533,12 @@ INVALID_VECTORS = [
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
# network id
NETWORK_IDS["testnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"]],
[SAMPLE_OUTPUTS["simple_byron_output"]],
# fee
42,
# ttl
@ -214,6 +546,74 @@ INVALID_VECTORS = [
# error message
"Output address network mismatch!",
),
# Shelley mainnet transaction with testnet output
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["shelley_testnet_output"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Shelley testnet transaction with mainnet output
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
# network id
NETWORK_IDS["testnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_shelley_output"]],
# fee
42,
# ttl
10,
# error message
"Invalid address",
),
# Testnet protocol magic with mainnet network id
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_shelley_output"]],
# fee
42,
# ttl
10,
# error message
"Invalid network id/protocol magic combination!",
),
# Mainnet protocol magic with testnet network id
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["testnet"],
# inputs
[SAMPLE_INPUTS["shelley_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_byron_output"]],
# fee
42,
# ttl
10,
# error message
"Invalid network id/protocol magic combination!",
),
]
@ -221,32 +621,47 @@ INVALID_VECTORS = [
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"protocol_magic,inputs,outputs,fee,ttl,tx_hash,serialized_tx", VALID_VECTORS
"protocol_magic,network_id,inputs,outputs,fee,ttl,input_flow_sequences,tx_hash,serialized_tx",
VALID_VECTORS,
)
def test_cardano_sign_tx(
client, protocol_magic, inputs, outputs, fee, ttl, tx_hash, serialized_tx
client,
protocol_magic,
network_id,
inputs,
outputs,
fee,
ttl,
input_flow_sequences,
tx_hash,
serialized_tx,
):
inputs = [cardano.create_input(i) for i in inputs]
outputs = [cardano.create_output(o) for o in outputs]
expected_responses = [
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
messages.CardanoSignedTx(),
messages.ButtonRequest(code=messages.ButtonRequestType.Other)
for i in range(len(input_flow_sequences))
]
expected_responses.append(messages.CardanoSignedTx())
def input_flow():
yield
client.debug.swipe_up()
client.debug.press_yes()
yield
client.debug.swipe_up()
client.debug.press_yes()
for sequence in input_flow_sequences:
yield
for action in sequence:
if action == InputAction.SWIPE:
client.debug.swipe_up()
elif action == InputAction.YES:
client.debug.press_yes()
else:
raise ValueError("Invalid input action")
with client:
client.set_expected_responses(expected_responses)
client.set_input_flow(input_flow)
response = cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic)
response = cardano.sign_tx(
client, inputs, outputs, fee, ttl, protocol_magic, network_id
)
assert response.tx_hash.hex() == tx_hash
assert response.serialized_tx.hex() == serialized_tx
@ -255,10 +670,18 @@ def test_cardano_sign_tx(
@pytest.mark.cardano
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.parametrize(
"protocol_magic,inputs,outputs,fee,ttl,expected_error_message", INVALID_VECTORS
"protocol_magic,network_id,inputs,outputs,fee,ttl,expected_error_message",
INVALID_VECTORS,
)
def test_cardano_sign_tx_validation(
client, protocol_magic, inputs, outputs, fee, ttl, expected_error_message
client,
protocol_magic,
network_id,
inputs,
outputs,
fee,
ttl,
expected_error_message,
):
inputs = [cardano.create_input(i) for i in inputs]
outputs = [cardano.create_output(o) for o in outputs]
@ -269,4 +692,6 @@ def test_cardano_sign_tx_validation(
client.set_expected_responses(expected_responses)
with pytest.raises(TrezorFailure, match=expected_error_message):
cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic)
cardano.sign_tx(
client, inputs, outputs, fee, ttl, protocol_magic, network_id
)

View File

@ -17,41 +17,32 @@
import pytest
from trezorlib import cardano, messages
from trezorlib.cardano import PROTOCOL_MAGICS
from trezorlib.cardano import NETWORK_IDS, PROTOCOL_MAGICS
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6
SAMPLE_INPUT = {
"path": "m/44'/1815'/0'/0/1",
"prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc",
"prev_index": 0,
}
SAMPLE_OUTPUTS = {
"simple_output": {
"address": "Ae2tdPwUPEZCanmBz5g2GEwFqKTKpNJcGYPKfDxoNeKZ8bRHr8366kseiK2",
"amount": "3003112",
},
"change_output": {"path": "m/44'/1815'/0'/0/1", "amount": "1000000"},
"testnet_output": {
"address": "2657WMsDfac5vydkak9a7BqGrsLqBzB7K3vT55rucZKYDmVnUCf6hXAFkZSTcUx7r",
"amount": "3003112",
},
}
from .test_msg_cardano_sign_transaction import (
SAMPLE_INPUTS,
SAMPLE_OUTPUTS,
InputAction,
)
VALID_VECTORS = [
# Mainnet transaction without change
(
# protocol magic
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"]],
[SAMPLE_OUTPUTS["simple_byron_output"]],
# fee
42,
# ttl
10,
# input flow
[[InputAction.SWIPE, InputAction.YES], [InputAction.SWIPE, InputAction.YES]],
# tx hash
"73e09bdebf98a9e0f17f86a2d11e0f14f4f8dae77cdf26ff1678e821f20c8db6",
# serialized tx
@ -61,14 +52,22 @@ VALID_VECTORS = [
(
# protocol magic (mainnet)
PROTOCOL_MAGICS["mainnet"],
# network id
NETWORK_IDS["mainnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["simple_output"], SAMPLE_OUTPUTS["change_output"]],
[SAMPLE_OUTPUTS["simple_byron_output"], SAMPLE_OUTPUTS["byron_change_output"]],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"4c43ce4c72f145b145ae7add414722735e250d048f61c4585a5becafcbffa6ae",
# serialized tx
@ -78,18 +77,26 @@ VALID_VECTORS = [
(
# protocol magic
PROTOCOL_MAGICS["testnet"],
# network id
NETWORK_IDS["testnet"],
# inputs
[SAMPLE_INPUT],
[SAMPLE_INPUTS["byron_input"]],
# outputs
[SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["change_output"]],
[SAMPLE_OUTPUTS["testnet_output"], SAMPLE_OUTPUTS["byron_change_output"]],
# fee
42,
# ttl
10,
# input flow
[
[InputAction.SWIPE, InputAction.YES],
[InputAction.YES],
[InputAction.SWIPE, InputAction.YES],
],
# tx hash
"ac7ef9e4f51ed4d6b791cee111b240dae2f00c39c5cc1a150631eba8aa955528",
"93a2c3cfb67ef1e4bae167b0f443c3370664bdb9171bc9cd41bad98e5cc049b2",
# serialized tx
"83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581c586b90cf80c021db288ce1c18ecfd3610acf64f8748768b0eb7335b1a10242182a001aae3129311a002dd2e882582f82d818582583581c709bfb5d9733cbdd72f520cd2c8b9f8f942da5e6cd0b6994e1803b0aa10242182a001aef14e76d1a000f424002182a030aa1028184582024c4fe188a39103db88818bc191fd8571eae7b284ebcbdf2462bde97b058a95c5840cfd68676454ad8bed8575dcb8ee91824c0f836da4f07a54112088b12c6b89be0c8f729d4e3fb1df0de10f049a66dea372f3e2888cabb6110d538a0e9a06fbb0758206f7a744035f4b3ddb8f861c18446169643cc3ae85e271b4b4f0eda05cf84c65b45a10242182af6",
"83a400818258201af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc00018282582f82d818582583581cc817d85b524e3d073795819a25cdbb84cff6aa2bbb3a081980d248cba10242182a001a0fb6fc611a002dd2e882582f82d818582583581c709bfb5d9733cbdd72f520cd2c8b9f8f942da5e6cd0b6994e1803b0aa10242182a001aef14e76d1a000f424002182a030aa1028184582024c4fe188a39103db88818bc191fd8571eae7b284ebcbdf2462bde97b058a95c5840552d1d66972598532fa539faa98cdc7889c8dce00577626a62fb22d0e244d9f49732b6ab65593352a7486123077b7e36308c5048cc8ee6dc465e576f065cb70558206f7a744035f4b3ddb8f861c18446169643cc3ae85e271b4b4f0eda05cf84c65b45a10242182af6",
),
]
@ -99,33 +106,48 @@ VALID_VECTORS = [
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.setup_client(mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6, passphrase=True)
@pytest.mark.parametrize(
"protocol_magic,inputs,outputs,fee,ttl,tx_hash,serialized_tx", VALID_VECTORS
"protocol_magic,network_id,inputs,outputs,fee,ttl,input_flow_sequences,tx_hash,serialized_tx",
VALID_VECTORS,
)
def test_cardano_sign_tx(
client, protocol_magic, inputs, outputs, fee, ttl, tx_hash, serialized_tx
client,
protocol_magic,
network_id,
inputs,
outputs,
fee,
ttl,
input_flow_sequences,
tx_hash,
serialized_tx,
):
inputs = [cardano.create_input(i) for i in inputs]
outputs = [cardano.create_output(o) for o in outputs]
expected_responses = [
messages.PassphraseRequest(),
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
messages.ButtonRequest(code=messages.ButtonRequestType.Other),
messages.CardanoSignedTx(),
expected_responses = [messages.PassphraseRequest()]
expected_responses += [
messages.ButtonRequest(code=messages.ButtonRequestType.Other)
for i in range(len(input_flow_sequences))
]
expected_responses.append(messages.CardanoSignedTx())
def input_flow():
yield
client.debug.swipe_up()
client.debug.press_yes()
yield
client.debug.swipe_up()
client.debug.press_yes()
for sequence in input_flow_sequences:
yield
for action in sequence:
if action == InputAction.SWIPE:
client.debug.swipe_up()
elif action == InputAction.YES:
client.debug.press_yes()
else:
raise ValueError("Invalid input action")
client.use_passphrase("TREZOR")
with client:
client.set_expected_responses(expected_responses)
client.set_input_flow(input_flow)
response = cardano.sign_tx(client, inputs, outputs, fee, ttl, protocol_magic)
response = cardano.sign_tx(
client, inputs, outputs, fee, ttl, protocol_magic, network_id
)
assert response.tx_hash.hex() == tx_hash
assert response.serialized_tx.hex() == serialized_tx

View File

@ -27,29 +27,54 @@
"test_msg_binance_sign_tx.py::test_binance_sign_message[message0-expected_response0]": "d41ee5e01a50f0f96fd7881db1750fab31cfe62c25b4eabbc092cc3daa039c7f",
"test_msg_binance_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "7b8bbe5ba7d7b07c95065608fb1cf9aeafcb3f9671835a6e5e5a6997ff4ff99b",
"test_msg_binance_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "813ad1b802dee1ace4dfa378edd840dbcea57c1a1b8eed67134def024c40a6e9",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-42-2657WMsDfac5vydkak9a": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-764824073-Ae2tdPwUPEZLC": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-42-2657WMsDfac61ebUDw53": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-764824073-Ae2tdPwUPEZEY": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-42-2657WMsDfac5PMpEsxc1": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-764824073-Ae2tdPwUPEZ3g": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-42-2657WMsDfac5F3zbgs9B": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-0-764824073-Ae2tdPwUPEZ5Y": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-42-2657WMsDfac6ezKWszxL": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-1-764824073-Ae2tdPwUPEZJb": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-42-2657WMsDfac7hr1ioJGr": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_address[m-44'-1815'-0'-0-2-764824073-Ae2tdPwUPEZFm": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_base_address[m-1852'-1815'-4'-0-0-m-1852'-1815'-4'": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_base_address_with_staking_key_hash[m-1852'-1815'-0": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_base_address_with_staking_key_hash[m-1852'-1815'-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_enterprise_address[m-1852'-1815'-0'-0-0-0-addr_tes": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_enterprise_address[m-1852'-1815'-0'-0-0-1-addr1vxq": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_pointer_address[m-1852'-1815'-0'-0-0-1-2-3-1-addr1": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_pointer_address[m-1852'-1815'-0'-0-0-24157-177-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_reward_address[m-1852'-1815'-0'-2-0-0-stake_test1u": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_address.py::test_cardano_get_reward_address[m-1852'-1815'-0'-2-0-1-stake1uyfz49": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-0'-d507c8f866691bd96e1": "ea2bf594e6a6cabd44dbc8c994dc455ea1f81eae844fa9df8833afdf46cda517",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-1'-140791584001446365f": "231bd3a5ccc02a4c27dd36c181c1338f1ef4c0ce19a558528fa53efae453ac25",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-2'-ff6ccc3097ca79fc29f": "f26abf1a1a855bda4a05816f12acf485ff1109f5b97d29e505ede199cf6e2e35",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-1852'-1815'-3'-be81ace1f63f4f0cae7": "ec69415277b55f4d53d2fa3c91ece0bef6d766474dd2f13ade3683e6e88f27e5",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-0'-c0fce1839f1a84c4e7702": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-1'-ea5dde31b9f551e08a5b6": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-2'-076338cee5ab3dae19f06": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_get_public_key.py::test_cardano_get_public_key[m-44'-1815'-3'-5f769380dc6fd17a4e0f2": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[42-inputs2-outputs2-42-10-5dd03fb44cb8806": "418f782a5d37c227f2d82f144bcee661d65187e07a421f64e6a5465daf544e7d",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs0-outputs0-42-10-73e09bde": "63dc0880175285dc1b3dcadb3ae66439f28b1c18fdaf50245cd84b6c8cb3d0e0",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-inputs1-outputs1-42-10-81b14b7e": "63dc0880175285dc1b3dcadb3ae66439f28b1c18fdaf50245cd84b6c8cb3d0e0",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-inputs6-outputs6-42-10-Outp": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs0-outputs0-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs1-outputs1-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs2-outputs2-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs3-outputs3-450": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs4-outputs4-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-inputs5-outputs5-42-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[42-inputs2-outputs2-42-10-ac7ef9e4f51": "ba354313a87bff3e5079da58c74cdc265a9be8e9a80006e76ea68d0cdb869665",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs0-outputs0-42-10-73e0": "37b670a523fca62a6d6d997541722ed070e1acf58fe618bc040e907272aa0f16",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-inputs1-outputs1-42-10-4c43": "37b670a523fca62a6d6d997541722ed070e1acf58fe618bc040e907272aa0f16",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[42-0-inputs6-outputs6-42-10-input_flow_se": "ae26b018ccceacd0c78de3389a673eca60753a6714f3146296ee17072417ec57",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs0-outputs0-42-10-input_": "fe89b315aeb25a4a449f0cadccdafcf58669d247101b038823aad25e4e7ed670",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs1-outputs1-42-10-input_": "900bd30689093a89966b56fa2663f260b0b2d4c0ba74dc4fb06cb0f45f5b3b4d",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs2-outputs2-42-10-input_": "7a35fb89f41b8730c1df37ab1fc63ee8aa4ba3b14282c68b1b86baf81665ff9c",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs3-outputs3-42-10-input_": "09faf9d0ad559154f74ac9cb3962411be07f81c5f310b23249211af1a6d4e1b2",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs4-outputs4-42-10-input_": "3141c25aef3c98eed72a91833b5b55b2a4e3593bb8e79562765eb82321e99685",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx[764824073-1-inputs5-outputs5-42-10-input_": "5a2a0a944774c4cbe6721fb06d41117da09394a9198c465db74dd81f037c701d",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-0-inputs12-outputs12-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-0-inputs14-outputs14-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[42-1-inputs15-outputs15-42-10-": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-0-inputs16-outputs16": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs0-outputs0-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs1-outputs1-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs10-outputs10": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs11-outputs11": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs13-outputs13": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs2-outputs2-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs3-outputs3-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs4-outputs4-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs5-outputs5-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs6-outputs6-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs7-outputs7-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs8-outputs8-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_transaction.py::test_cardano_sign_tx_validation[764824073-1-inputs9-outputs9-4": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[42-0-inputs2-outputs2-42-10-input_flo": "d488a0f2c127a675a1c2e2d410b6c4f402cdb610e19886a8998aa5ad786a779e",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-1-inputs0-outputs0-42-10-in": "6aa71de5007b0faf1eea4b1cfda1da6a739f852c0d875a1e59d83c03178c2f98",
"test_msg_cardano_sign_tx_slip39_basic.py::test_cardano_sign_tx[764824073-1-inputs1-outputs1-42-10-in": "7abf2e87a9b1e50afdf3502ba9480b07a59d59ccccf24915b46fb81285ae3fa8",
"test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "9e11b251c03ef09127da79d92f8483c4db438c7303328774790d45e3f6fb8c96",
"test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "d280ed129a2ea4781af9e35542aa31ecf63da75fc6812ed3bd05107809f836a4",
"test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "32c808f00bada2059f933f3515337e494c837bdf65e4ea918b457d1c9f4cb42a",