From 7bf5cab840422965d72ed9a38daa7bb04ac2b51d Mon Sep 17 00:00:00 2001 From: gabrielkerekes Date: Mon, 27 Jul 2020 12:48:31 +0200 Subject: [PATCH] Update sign_tx Add certificates, withdrawals and metadata hash --- core/src/apps/cardano/address.py | 6 +- core/src/apps/cardano/helpers/__init__.py | 2 + core/src/apps/cardano/helpers/utils.py | 3 + core/src/apps/cardano/layout.py | 61 +++++++- core/src/apps/cardano/sign_tx.py | 178 ++++++++++++++++++++-- 5 files changed, 231 insertions(+), 19 deletions(-) diff --git a/core/src/apps/cardano/address.py b/core/src/apps/cardano/address.py index dcb1e9fe4..fa43a7a4a 100644 --- a/core/src/apps/cardano/address.py +++ b/core/src/apps/cardano/address.py @@ -256,11 +256,11 @@ def _validate_base_address_staking_info( "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): + 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: +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 @@ -322,7 +322,7 @@ def _derive_enterprise_address( def _derive_reward_address( keychain: seed.Keychain, path: List[int], network_id: int, ) -> bytes: - if not _is_staking_path(path): + if not is_staking_path(path): raise wire.DataError("Invalid path for reward address!") header = _create_address_header(CardanoAddressType.REWARD, network_id) diff --git a/core/src/apps/cardano/helpers/__init__.py b/core/src/apps/cardano/helpers/__init__.py index 093e6e58f..91d0e1d48 100644 --- a/core/src/apps/cardano/helpers/__init__.py +++ b/core/src/apps/cardano/helpers/__init__.py @@ -2,3 +2,5 @@ from trezor import wire INVALID_ADDRESS = wire.ProcessError("Invalid address") NETWORK_MISMATCH = wire.ProcessError("Output address network mismatch!") +INVALID_CERTIFICATE = wire.ProcessError("Invalid certificate") +INVALID_WITHDRAWAL = wire.ProcessError("Invalid withdrawal") diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index dd4b1378d..04e84f776 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -1,5 +1,8 @@ from micropython import const +if False: + from typing import List + def variable_length_encode(number: int) -> bytes: """ diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index cc1160aff..aac88a79d 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -1,7 +1,11 @@ from ubinascii import hexlify from trezor import ui -from trezor.messages import ButtonRequestType, CardanoAddressType +from trezor.messages import ( + ButtonRequestType, + CardanoAddressType, + CardanoCertificateType, +) from trezor.strings import format_amount from trezor.ui.button import ButtonDefault from trezor.ui.scroll import Paginated @@ -12,11 +16,16 @@ from apps.common.confirm import confirm, require_confirm, require_hold_to_confir from apps.common.layout import address_n_to_str, show_warning from .helpers import protocol_magics +from .helpers.utils import to_account_path if False: from typing import List from trezor import wire - from trezor.messages import CardanoBlockchainPointerType + from trezor.messages import ( + CardanoBlockchainPointerType, + CardanoTxCertificateType, + CardanoTxWithdrawalType, + ) from trezor.messages.CardanoAddressParametersType import EnumTypeCardanoAddressType @@ -28,6 +37,12 @@ ADDRESS_TYPE_NAMES = { CardanoAddressType.REWARD: "Reward", } +CERTIFICATE_TYPE_NAMES = { + CardanoCertificateType.STAKE_REGISTRATION: "Stake key registration", + CardanoCertificateType.STAKE_DEREGISTRATION: "Stake key deregistration", + CardanoCertificateType.STAKE_DELEGATION: "Stake delegation", +} + def format_coin_amount(amount: int) -> str: return "%s %s" % (format_amount(amount, 6), "ADA") @@ -130,6 +145,48 @@ async def confirm_transaction(ctx, amount: int, fee: int, protocol_magic: int): await require_hold_to_confirm(ctx, Paginated([t1, t2])) +async def confirm_certificate( + ctx: wire.Context, certificate: CardanoTxCertificateType +) -> bool: + pages = [] + + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Confirm:") + t1.bold(CERTIFICATE_TYPE_NAMES[certificate.type]) + t1.normal("for account:") + t1.bold(address_n_to_str(to_account_path(certificate.path))) + pages.append(t1) + + if certificate.type == CardanoCertificateType.STAKE_DELEGATION: + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t2.normal("to pool:") + t2.bold(hexlify(certificate.pool).decode()) + pages.append(t2) + + await require_confirm(ctx, Paginated(pages)) + + +async def confirm_withdrawal( + ctx: wire.Context, withdrawal: CardanoTxWithdrawalType +) -> bool: + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Confirm withdrawal") + t1.normal("for account:") + t1.bold(address_n_to_str(to_account_path(withdrawal.path))) + t1.normal("Amount:") + t1.bold(format_coin_amount(withdrawal.amount)) + + await require_confirm(ctx, t1) + + +async def confirm_metadata_hash(ctx: wire.Context, metadata_hash: bytes) -> bool: + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) + t1.normal("Confirm metadata hash:") + t1.bold(hexlify(metadata_hash).decode()) + + await require_confirm(ctx, t1) + + async def show_address( ctx: wire.Context, address: str, diff --git a/core/src/apps/cardano/sign_tx.py b/core/src/apps/cardano/sign_tx.py index 820e1d36e..b097683f0 100644 --- a/core/src/apps/cardano/sign_tx.py +++ b/core/src/apps/cardano/sign_tx.py @@ -3,7 +3,8 @@ from micropython import const from trezor import log, wire from trezor.crypto import hashlib from trezor.crypto.curve import ed25519 -from trezor.messages import CardanoAddressParametersType +from trezor.messages import CardanoAddressType, CardanoCertificateType +from trezor.messages.CardanoAddressParametersType import CardanoAddressParametersType from trezor.messages.CardanoSignedTx import CardanoSignedTx from apps.common import cbor @@ -15,15 +16,26 @@ from .address import ( derive_address_bytes, derive_human_readable_address, get_address_bytes_unsafe, + get_public_key_hash, + is_staking_path, validate_full_path, validate_output_address, ) from .byron_address import get_address_attributes -from .helpers import network_ids, protocol_magics, staking_use_cases +from .helpers import ( + INVALID_CERTIFICATE, + INVALID_WITHDRAWAL, + network_ids, + protocol_magics, + staking_use_cases, +) from .helpers.utils import to_account_path from .layout import ( + confirm_certificate, + confirm_metadata_hash, confirm_sending, confirm_transaction, + confirm_withdrawal, show_warning_tx_different_staking_account, show_warning_tx_no_staking_info, show_warning_tx_pointer_address, @@ -36,6 +48,8 @@ if False: from trezor.messages.CardanoSignTx import CardanoSignTx from trezor.messages.CardanoTxInputType import CardanoTxInputType from trezor.messages.CardanoTxOutputType import CardanoTxOutputType + from trezor.messages.CardanoTxCertificateType import CardanoTxCertificateType + from trezor.messages.CardanoTxWithdrawalType import CardanoTxWithdrawalType # the maximum allowed change address. this should be large enough for normal # use and still allow to quickly brute-force the correct bip32 path @@ -45,6 +59,9 @@ BIP_PATH_LENGTH = const(5) LOVELACE_MAX_SUPPLY = 45_000_000_000 * 1_000_000 +POOL_HASH_SIZE = 28 +METADATA_HASH_SIZE = 32 + @seed.with_keychain async def sign_tx( @@ -60,6 +77,11 @@ async def sign_tx( await validate_path(ctx, validate_full_path, keychain, i.address_n, CURVE) _validate_outputs(keychain, msg.outputs, msg.protocol_magic, msg.network_id) + _validate_certificates(msg.certificates) + _validate_withdrawals(msg.withdrawals) + + if msg.metadata_hash and len(msg.metadata_hash) != METADATA_HASH_SIZE: + raise wire.ProcessError("Invalid metadata hash") # display the transaction in UI await _show_tx(ctx, keychain, msg) @@ -117,12 +139,41 @@ def _validate_outputs( raise wire.ProcessError("Total transaction amount is out of range!") +def _validate_certificates(certificates: List[CardanoTxCertificateType]) -> None: + for certificate in certificates: + if not is_staking_path(certificate.path): + raise INVALID_CERTIFICATE + + if certificate.type == CardanoCertificateType.STAKE_DELEGATION: + if certificate.pool is None or len(certificate.pool) != POOL_HASH_SIZE: + raise INVALID_CERTIFICATE + + +def _validate_withdrawals(withdrawals: List[CardanoTxWithdrawalType]) -> None: + for withdrawal in withdrawals: + if not is_staking_path(withdrawal.path): + raise INVALID_WITHDRAWAL + + if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: + raise INVALID_WITHDRAWAL + + def _serialize_tx(keychain: seed.Keychain, msg: CardanoSignTx) -> Tuple[bytes, bytes]: tx_body = _build_tx_body(keychain, msg) tx_hash = _hash_tx_body(tx_body) - witnesses = _build_witnesses(keychain, msg.inputs, tx_hash, msg.protocol_magic) + witnesses = _build_witnesses( + keychain, + msg.inputs, + msg.certificates, + msg.withdrawals, + tx_hash, + msg.protocol_magic, + ) + # We always set transaction metadata to None, even if metadata + # hash is set. Metadata aren't sent to Trezor and the None + # should be replaced by the SW wallet if metadata exist. serialized_tx = cbor.encode([tx_body, witnesses, None]) return serialized_tx, tx_hash @@ -141,6 +192,21 @@ def _build_tx_body(keychain: seed.Keychain, msg: CardanoSignTx) -> Dict: 3: msg.ttl, } + if msg.certificates: + certificates_for_cbor = _build_certificates(keychain, msg.certificates) + tx_body[4] = certificates_for_cbor + + if msg.withdrawals: + withdrawals_for_cbor = _build_withdrawals( + keychain, msg.withdrawals, msg.protocol_magic, msg.network_id + ) + tx_body[5] = withdrawals_for_cbor + + # tx_body[6] is for protocol updates, which we don't support + + if msg.metadata_hash: + tx_body[7] = msg.metadata_hash + return tx_body @@ -170,6 +236,50 @@ def _build_outputs( return result +def _build_certificates( + keychain: seed.Keychain, certificates: List[CardanoTxCertificateType] +) -> List[Tuple]: + result = [] + for certificate in certificates: + public_key_hash = get_public_key_hash(keychain, certificate.path) + + stake_credential = [0, public_key_hash] + if certificate.type == CardanoCertificateType.STAKE_DELEGATION: + certificate_for_cbor = ( + certificate.type, + stake_credential, + certificate.pool, + ) + else: + certificate_for_cbor = (certificate.type, stake_credential) + + result.append(certificate_for_cbor) + + return result + + +def _build_withdrawals( + keychain: seed.Keychain, + withdrawals: List[CardanoTxWithdrawalType], + protocol_magic: int, + network_id: int, +) -> Dict[bytes, int]: + result = {} + for withdrawal in withdrawals: + reward_address = derive_address_bytes( + keychain, + CardanoAddressParametersType( + address_type=CardanoAddressType.REWARD, address_n=withdrawal.path, + ), + protocol_magic, + network_id, + ) + + result[reward_address] = withdrawal.amount + + return result + + def _hash_tx_body(tx_body: Dict) -> bytes: tx_body_cbor = cbor.encode(tx_body) return hashlib.blake2b(data=tx_body_cbor, outlen=32).digest() @@ -178,10 +288,14 @@ def _hash_tx_body(tx_body: Dict) -> bytes: def _build_witnesses( keychain: seed.Keychain, inputs: List[CardanoTxInputType], + certificates: List[CardanoTxCertificateType], + withdrawals: List[CardanoTxWithdrawalType], tx_body_hash: bytes, protocol_magic: int, ) -> Dict: - shelley_witnesses = _build_shelley_witnesses(keychain, inputs, tx_body_hash) + shelley_witnesses = _build_shelley_witnesses( + keychain, inputs, certificates, withdrawals, tx_body_hash + ) byron_witnesses = _build_byron_witnesses( keychain, inputs, tx_body_hash, protocol_magic ) @@ -189,33 +303,59 @@ def _build_witnesses( # 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: + if shelley_witnesses: witnesses[0] = shelley_witnesses - if len(byron_witnesses) > 0: + if byron_witnesses: witnesses[2] = byron_witnesses return witnesses def _build_shelley_witnesses( - keychain: seed.Keychain, inputs: List[CardanoTxInputType], tx_body_hash: bytes, + keychain: seed.Keychain, + inputs: List[CardanoTxInputType], + certificates: List[CardanoTxCertificateType], + withdrawals: List[CardanoTxWithdrawalType], + tx_body_hash: bytes, ) -> List[Tuple[bytes, bytes]]: shelley_witnesses = [] + + paths = set() for input in inputs: if not is_shelley_path(input.address_n): continue + paths.add(tuple(input.address_n)) + for certificate in certificates: + if not _is_certificate_witness_required(certificate.type): + continue + paths.add(tuple(certificate.path)) + for withdrawal in withdrawals: + paths.add(tuple(withdrawal.path)) - 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)) + for path in paths: + witness = _build_shelley_witness(keychain, tx_body_hash, list(path)) + shelley_witnesses.append(witness) return shelley_witnesses +def _build_shelley_witness( + keychain: seed.Keychain, tx_body_hash: bytes, path: List[int] +) -> List[Tuple[bytes, bytes]]: + node = keychain.derive(path) + + signature = ed25519.sign_ext( + node.private_key(), node.private_key_ext(), tx_body_hash + ) + public_key = remove_ed25519_prefix(node.public_key()) + + return public_key, signature + + +def _is_certificate_witness_required(certificate_type: int) -> bool: + return certificate_type != CardanoCertificateType.STAKE_REGISTRATION + + def _build_byron_witnesses( keychain: seed.Keychain, inputs: List[CardanoTxInputType], @@ -245,6 +385,16 @@ async def _show_tx( ctx: wire.Context, keychain: seed.Keychain, msg: CardanoSignTx ) -> None: total_amount = await _show_outputs(ctx, keychain, msg) + + for certificate in msg.certificates: + await confirm_certificate(ctx, certificate) + + for withdrawal in msg.withdrawals: + await confirm_withdrawal(ctx, withdrawal) + + if msg.metadata_hash: + await confirm_metadata_hash(ctx, msg.metadata_hash) + await confirm_transaction(ctx, total_amount, msg.fee, msg.protocol_magic)