From 38fa9197cabad9b9aaea1510d852ba23eb75a27e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Sat, 5 Jun 2021 02:15:25 +0200 Subject: [PATCH] feat(core,python): support for Ethereum EIP1559 transactions Initial EIP1559 implementation Fix a few small issues Progress on Python lib implementation and firmware Fix RLP length Start fixing tests Fix legacy transactions Simplify API and logic Add EIP1559 tests Fix access list formatting Fix UI visiblity issue Fix commented out code fix: correct linting issues Fix access_list protobuf formatting Remove unneeded code Remove dead code Check tx_type bounds for EIP 2718 Reduce code duplication Prefer eip2718_type over re-using tx_type Add more tests Simplify format_access_list Simplify sign_tx slightly Change Access List format and add logic to encode it Fix a bunch of small PR comments Fix a linting issue Move tests out of class and regenerate Remove copy-pasted comments Add access list to CLI Simplify _parse_access_list_item Fix small mistakes following rebase Fix linting Refactor to use a separate message for EIP 1559 tx Simplify changed legacy code Fix a few small PR comments Fix linting fix(legacy): recognize SignTxEIP1559 on legacy build Fix PR comments --- common/protob/messages-ethereum.proto | 28 +- common/protob/messages.proto | 1 + core/.changelog.d/1604.added | 1 + core/src/all_modules.py | 2 + core/src/apps/ethereum/keychain.py | 5 +- core/src/apps/ethereum/layout.py | 30 +- core/src/apps/ethereum/sign_tx.py | 59 ++-- core/src/apps/ethereum/sign_tx_eip1559.py | 154 +++++++++ core/src/apps/workflow_handlers.py | 2 + core/src/trezor/enums/MessageType.py | 1 + core/src/trezor/enums/__init__.py | 1 + core/src/trezor/messages.py | 50 +++ python/.changelog.d/1604.added | 1 + python/src/trezorlib/cli/ethereum.py | 114 ++++++- python/src/trezorlib/ethereum.py | 43 ++- python/src/trezorlib/messages.py | 62 ++++ .../test_msg_ethereum_signtx_eip1559.py | 308 ++++++++++++++++++ tests/ui_tests/fixtures.json | 7 + 18 files changed, 824 insertions(+), 45 deletions(-) create mode 100644 core/.changelog.d/1604.added create mode 100644 core/src/apps/ethereum/sign_tx_eip1559.py create mode 100644 python/.changelog.d/1604.added create mode 100644 tests/device_tests/test_msg_ethereum_signtx_eip1559.py diff --git a/common/protob/messages-ethereum.proto b/common/protob/messages-ethereum.proto index 9fab13c2c5..2f6ff12e56 100644 --- a/common/protob/messages-ethereum.proto +++ b/common/protob/messages-ethereum.proto @@ -66,7 +66,33 @@ message EthereumSignTx { optional bytes data_initial_chunk = 7; // The initial data chunk (<= 1024 bytes) optional uint32 data_length = 8; // Length of transaction payload optional uint32 chain_id = 9; // Chain Id for EIP 155 - optional uint32 tx_type = 10; // (only for Wanchain) + optional uint32 tx_type = 10; // Used for Wanchain +} + +/** + * Request: Ask device to sign EIP1559 transaction + * Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. + * @start + * @next EthereumTxRequest + * @next Failure + */ +message EthereumSignTxEIP1559 { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes nonce = 2; // <=256 bit unsigned big endian + required bytes max_gas_fee = 3; // <=256 bit unsigned big endian (in wei) + required bytes max_priority_fee = 4; // <=256 bit unsigned big endian (in wei) + required bytes gas_limit = 5; // <=256 bit unsigned big endian + optional string to = 6 [default='']; // recipient address + required bytes value = 7; // <=256 bit unsigned big endian (in wei) + optional bytes data_initial_chunk = 8 [default='']; // The initial data chunk (<= 1024 bytes) + required uint32 data_length = 9; // Length of transaction payload + required uint32 chain_id = 10; // Chain Id for EIP 155 + repeated EthereumAccessList access_list = 11; // Access List + + message EthereumAccessList { + required string address = 1; + repeated bytes storage_keys = 2; + } } /** diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 557e37ae00..05a4813539 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -174,6 +174,7 @@ enum MessageType { MessageType_EthereumGetAddress = 56 [(wire_in) = true]; MessageType_EthereumAddress = 57 [(wire_out) = true]; MessageType_EthereumSignTx = 58 [(wire_in) = true]; + MessageType_EthereumSignTxEIP1559 = 452 [(wire_in) = true]; MessageType_EthereumTxRequest = 59 [(wire_out) = true]; MessageType_EthereumTxAck = 60 [(wire_in) = true]; MessageType_EthereumSignMessage = 64 [(wire_in) = true]; diff --git a/core/.changelog.d/1604.added b/core/.changelog.d/1604.added new file mode 100644 index 0000000000..4eb50cf08f --- /dev/null +++ b/core/.changelog.d/1604.added @@ -0,0 +1 @@ +Support for Ethereum EIP1559 transactions diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 82100b2245..6a2b756d50 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -496,6 +496,8 @@ if utils.BITCOIN_ONLY: import apps.ethereum.sign_message apps.ethereum.sign_tx import apps.ethereum.sign_tx + apps.ethereum.sign_tx_eip1559 + import apps.ethereum.sign_tx_eip1559 apps.ethereum.tokens import apps.ethereum.tokens apps.ethereum.verify_message diff --git a/core/src/apps/ethereum/keychain.py b/core/src/apps/ethereum/keychain.py index 5693fa1190..ab1b6789b7 100644 --- a/core/src/apps/ethereum/keychain.py +++ b/core/src/apps/ethereum/keychain.py @@ -1,4 +1,5 @@ from trezor import wire +from trezor.messages import EthereumSignTxEIP1559 from apps.common import paths from apps.common.keychain import get_keychain @@ -72,7 +73,9 @@ def _schemas_from_chain_id(msg: EthereumSignTx) -> Iterable[paths.PathSchema]: if info is None: # allow Ethereum or testnet paths for unknown networks slip44_id = (60, 1) - elif networks.is_wanchain(msg.chain_id, msg.tx_type): + elif not EthereumSignTxEIP1559.is_type_of(msg) and networks.is_wanchain( + msg.chain_id, msg.tx_type + ): slip44_id = (networks.SLIP44_WANCHAIN,) elif info.slip44 != 60 and info.slip44 != 1: # allow cross-signing with Ethereum unless it's testnet diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 0e37bb634e..8599053572 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -3,7 +3,12 @@ from ubinascii import hexlify from trezor import ui from trezor.enums import ButtonRequestType from trezor.strings import format_amount -from trezor.ui.layouts import confirm_address, confirm_blob, confirm_output +from trezor.ui.layouts import ( + confirm_address, + confirm_amount, + confirm_blob, + confirm_output, +) from trezor.ui.layouts.tt.altcoin import confirm_total_ethereum from . import networks, tokens @@ -36,6 +41,29 @@ async def require_confirm_fee( ) +async def require_confirm_eip1559_fee( + ctx, max_priority_fee, max_gas_fee, gas_limit, chain_id +): + await confirm_amount( + ctx, + title="Confirm fee", + description="Maximum fee per gas", + amount=format_ethereum_amount(max_gas_fee, None, chain_id), + ) + await confirm_amount( + ctx, + title="Confirm fee", + description="Priority fee per gas", + amount=format_ethereum_amount(max_priority_fee, None, chain_id), + ) + await confirm_amount( + ctx, + title="Confirm fee", + description="Maximum fee", + amount=format_ethereum_amount(max_gas_fee * gas_limit, None, chain_id), + ) + + async def require_confirm_unknown_token(ctx, address_bytes): contract_address_hex = "0x" + hexlify(address_bytes).decode() await confirm_address( diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index 5468e29826..cca7d64f4e 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -23,30 +23,15 @@ MAX_CHAIN_ID = 2147483629 @with_keychain_from_chain_id async def sign_tx(ctx, msg, keychain): msg = sanitize(msg) + check(msg) await paths.validate_path(ctx, keychain, msg.address_n) + # Handle ERC20s + token, address_bytes, recipient, value = await handle_erc20(ctx, msg) + data_total = msg.data_length - # detect ERC - 20 token - token = None - address_bytes = recipient = address.bytes_from_address(msg.to) - value = int.from_bytes(msg.value, "big") - if ( - len(msg.to) in (40, 42) - and len(msg.value) == 0 - and data_total == 68 - and len(msg.data_initial_chunk) == 68 - and msg.data_initial_chunk[:16] - == b"\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - ): - token = tokens.token_by_chain_address(msg.chain_id, address_bytes) - recipient = msg.data_initial_chunk[16:36] - value = int.from_bytes(msg.data_initial_chunk[36:68], "big") - - if token is tokens.UNKNOWN_TOKEN: - await require_confirm_unknown_token(ctx, address_bytes) - await require_confirm_tx(ctx, recipient, value, msg.chain_id, token, msg.tx_type) if token is None and msg.data_length > 0: await require_confirm_data(ctx, msg.data_initial_chunk, data_total) @@ -99,6 +84,28 @@ async def sign_tx(ctx, msg, keychain): return result +async def handle_erc20(ctx, msg): + token = None + address_bytes = recipient = address.bytes_from_address(msg.to) + value = int.from_bytes(msg.value, "big") + if ( + len(msg.to) in (40, 42) + and len(msg.value) == 0 + and msg.data_length == 68 + and len(msg.data_initial_chunk) == 68 + and msg.data_initial_chunk[:16] + == b"\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ): + token = tokens.token_by_chain_address(msg.chain_id, address_bytes) + recipient = msg.data_initial_chunk[16:36] + value = int.from_bytes(msg.data_initial_chunk[36:68], "big") + + if token is tokens.UNKNOWN_TOKEN: + await require_confirm_unknown_token(ctx, address_bytes) + + return token, address_bytes, recipient, value + + def get_total_length(msg: EthereumSignTx, data_total: int) -> int: length = 0 if msg.tx_type is not None: @@ -120,6 +127,7 @@ def get_total_length(msg: EthereumSignTx, data_total: int) -> int: length += rlp.header_length(data_total, msg.data_initial_chunk) length += data_total + return length @@ -157,9 +165,14 @@ def check(msg: EthereumSignTx): if msg.tx_type not in [1, 6, None]: raise wire.DataError("tx_type out of bounds") - if msg.chain_id < 0: - raise wire.DataError("chain_id out of bounds") + check_data(msg) + # safety checks + if not check_gas(msg) or not check_to(msg): + raise wire.DataError("Safety check failed") + + +def check_data(msg: EthereumSignTx): if msg.data_length > 0: if not msg.data_initial_chunk: raise wire.DataError("Data length provided, but no initial chunk") @@ -170,10 +183,6 @@ def check(msg: EthereumSignTx): if len(msg.data_initial_chunk) > msg.data_length: raise wire.DataError("Invalid size of initial chunk") - # safety checks - if not check_gas(msg) or not check_to(msg): - raise wire.DataError("Safety check failed") - def check_gas(msg: EthereumSignTx) -> bool: if msg.gas_price is None or msg.gas_limit is None: diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py new file mode 100644 index 0000000000..b2533757c0 --- /dev/null +++ b/core/src/apps/ethereum/sign_tx_eip1559.py @@ -0,0 +1,154 @@ +from trezor import wire +from trezor.crypto import rlp +from trezor.crypto.curve import secp256k1 +from trezor.crypto.hashlib import sha3_256 +from trezor.messages import EthereumAccessList, EthereumSignTxEIP1559, EthereumTxRequest +from trezor.utils import HashWriter + +from apps.common import paths + +from . import address +from .keychain import with_keychain_from_chain_id +from .layout import ( + require_confirm_data, + require_confirm_eip1559_fee, + require_confirm_tx, +) +from .sign_tx import check_data, check_to, handle_erc20, sanitize, send_request_chunk + +TX_TYPE = 2 + + +def access_list_item_length(item: EthereumAccessList) -> int: + address_length = rlp.length(address.bytes_from_address(item.address)) + keys_length = rlp.length(item.storage_keys) + return ( + rlp.header_length(address_length + keys_length) + address_length + keys_length + ) + + +def access_list_length(access_list: list[EthereumAccessList]) -> int: + payload_length = sum(access_list_item_length(i) for i in access_list) + return rlp.header_length(payload_length) + payload_length + + +def write_access_list(w: HashWriter, access_list: list[EthereumAccessList]) -> None: + payload_length = sum(access_list_item_length(i) for i in access_list) + rlp.write_header(w, payload_length, rlp.LIST_HEADER_BYTE) + for item in access_list: + address_bytes = address.bytes_from_address(item.address) + address_length = rlp.length(address_bytes) + keys_length = rlp.length(item.storage_keys) + rlp.write_header(w, address_length + keys_length, rlp.LIST_HEADER_BYTE) + rlp.write(w, address_bytes) + rlp.write(w, item.storage_keys) + + +@with_keychain_from_chain_id +async def sign_tx_eip1559(ctx, msg, keychain): + msg = sanitize(msg) + + check(msg) + + await paths.validate_path(ctx, keychain, msg.address_n) + + # Handle ERC20s + token, address_bytes, recipient, value = await handle_erc20(ctx, msg) + + data_total = msg.data_length + + await require_confirm_tx(ctx, recipient, value, msg.chain_id, token) + if token is None and msg.data_length > 0: + await require_confirm_data(ctx, msg.data_initial_chunk, data_total) + + await require_confirm_eip1559_fee( + ctx, + int.from_bytes(msg.max_priority_fee, "big"), + int.from_bytes(msg.max_gas_fee, "big"), + int.from_bytes(msg.gas_limit, "big"), + msg.chain_id, + ) + + data = bytearray() + data += msg.data_initial_chunk + data_left = data_total - len(msg.data_initial_chunk) + + total_length = get_total_length(msg, data_total) + + sha = HashWriter(sha3_256(keccak=True)) + + rlp.write(sha, TX_TYPE) + + rlp.write_header(sha, total_length, rlp.LIST_HEADER_BYTE) + + for field in ( + msg.chain_id, + msg.nonce, + msg.max_priority_fee, + msg.max_gas_fee, + msg.gas_limit, + address_bytes, + msg.value, + ): + rlp.write(sha, field) + + if data_left == 0: + rlp.write(sha, data) + else: + rlp.write_header(sha, data_total, rlp.STRING_HEADER_BYTE, data) + sha.extend(data) + + while data_left > 0: + resp = await send_request_chunk(ctx, data_left) + data_left -= len(resp.data_chunk) + sha.extend(resp.data_chunk) + + write_access_list(sha, msg.access_list) + + digest = sha.get_digest() + result = sign_digest(msg, keychain, digest) + + return result + + +def get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int: + length = 0 + + for item in ( + msg.nonce, + msg.gas_limit, + address.bytes_from_address(msg.to), + msg.value, + msg.chain_id, + msg.max_gas_fee, + msg.max_priority_fee, + ): + length += rlp.length(item) + + length += rlp.header_length(data_total, msg.data_initial_chunk) + length += data_total + + length += access_list_length(msg.access_list) + + return length + + +def sign_digest(msg: EthereumSignTxEIP1559, keychain, digest): + node = keychain.derive(msg.address_n) + signature = secp256k1.sign( + node.private_key(), digest, False, secp256k1.CANONICAL_SIG_ETHEREUM + ) + + req = EthereumTxRequest() + req.signature_v = signature[0] - 27 + req.signature_r = signature[1:33] + req.signature_s = signature[33:] + + return req + + +def check(msg: EthereumSignTxEIP1559): + check_data(msg) + + if not check_to(msg): + raise wire.DataError("Safety check failed") diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 9aa4234aa9..244e903a20 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -103,6 +103,8 @@ def find_message_handler_module(msg_type: int) -> str: return "apps.ethereum.get_public_key" elif msg_type == MessageType.EthereumSignTx: return "apps.ethereum.sign_tx" + elif msg_type == MessageType.EthereumSignTxEIP1559: + return "apps.ethereum.sign_tx_eip1559" elif msg_type == MessageType.EthereumSignMessage: return "apps.ethereum.sign_message" elif msg_type == MessageType.EthereumVerifyMessage: diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index 2d6a38fc92..6c45510e19 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -91,6 +91,7 @@ if not utils.BITCOIN_ONLY: EthereumGetAddress = 56 EthereumAddress = 57 EthereumSignTx = 58 + EthereumSignTxEIP1559 = 452 EthereumTxRequest = 59 EthereumTxAck = 60 EthereumSignMessage = 64 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 08054d3b1c..4466e6b778 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -96,6 +96,7 @@ if TYPE_CHECKING: EthereumGetAddress = 56 EthereumAddress = 57 EthereumSignTx = 58 + EthereumSignTxEIP1559 = 452 EthereumTxRequest = 59 EthereumTxAck = 60 EthereumSignMessage = 64 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index e507a1f52c..342a066ba0 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -2962,6 +2962,40 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumSignTx"]: return isinstance(msg, cls) + class EthereumSignTxEIP1559(protobuf.MessageType): + address_n: "list[int]" + nonce: "bytes" + max_gas_fee: "bytes" + max_priority_fee: "bytes" + gas_limit: "bytes" + to: "str" + value: "bytes" + data_initial_chunk: "bytes" + data_length: "int" + chain_id: "int" + access_list: "list[EthereumAccessList]" + + def __init__( + self, + *, + nonce: "bytes", + max_gas_fee: "bytes", + max_priority_fee: "bytes", + gas_limit: "bytes", + value: "bytes", + data_length: "int", + chain_id: "int", + address_n: "list[int] | None" = None, + access_list: "list[EthereumAccessList] | None" = None, + to: "str | None" = None, + data_initial_chunk: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumSignTxEIP1559"]: + return isinstance(msg, cls) + class EthereumTxRequest(protobuf.MessageType): data_length: "int | None" signature_v: "int | None" @@ -3046,6 +3080,22 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumVerifyMessage"]: return isinstance(msg, cls) + class EthereumAccessList(protobuf.MessageType): + address: "str" + storage_keys: "list[bytes]" + + def __init__( + self, + *, + address: "str", + storage_keys: "list[bytes] | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["EthereumAccessList"]: + return isinstance(msg, cls) + class LiskGetAddress(protobuf.MessageType): address_n: "list[int]" show_display: "bool | None" diff --git a/python/.changelog.d/1604.added b/python/.changelog.d/1604.added new file mode 100644 index 0000000000..4eb50cf08f --- /dev/null +++ b/python/.changelog.d/1604.added @@ -0,0 +1 @@ +Support for Ethereum EIP1559 transactions diff --git a/python/src/trezorlib/cli/ethereum.py b/python/src/trezorlib/cli/ethereum.py index ff27b9fd0a..2f88a8d43d 100644 --- a/python/src/trezorlib/cli/ethereum.py +++ b/python/src/trezorlib/cli/ethereum.py @@ -17,6 +17,7 @@ import re import sys from decimal import Decimal +from typing import List import click @@ -74,6 +75,25 @@ def _amount_to_int(ctx, param, value): raise click.BadParameter("Amount not understood") +def _parse_access_list(ctx, param, value): + try: + return [_parse_access_list_item(val) for val in value] + + except Exception: + raise click.BadParameter("Access List format invalid") + + +def _parse_access_list_item(value): + try: + arr = value.split(":") + address, storage_keys = arr[0], arr[1:] + storage_keys_bytes = [_decode_hex(key) for key in storage_keys] + return ethereum.messages.EthereumAccessList(address, storage_keys_bytes) + + except Exception: + raise click.BadParameter("Access List format invalid") + + def _list_units(ctx, param, value): if not value or ctx.resilient_parsing: return @@ -107,6 +127,14 @@ def _erc20_contract(w3, token_address, to_address, amount): return contract.encodeABI("transfer", [to_address, amount]) +def _format_access_list(access_list: List[ethereum.messages.EthereumAccessList]): + mapped = map( + lambda item: [_decode_hex(item.address), item.storage_keys], + access_list, + ) + return list(mapped) + + ##################### # # commands start here @@ -166,8 +194,22 @@ def get_public_node(client, address, show_display): ) @click.option("-d", "--data", help="Data as hex string, e.g. 0x12345678") @click.option("-p", "--publish", is_flag=True, help="Publish transaction via RPC") -@click.option("-x", "--tx-type", type=int, help="TX type (used only for Wanchain)") +@click.option("-x", "--tx-type", type=int, help="TX type") @click.option("-t", "--token", help="ERC20 token address") +@click.option( + "-a", + "--access-list", + help="Access List", + callback=_parse_access_list, + multiple=True, +) +@click.option("--max-gas-fee", help="Max Gas Fee (EIP1559)", callback=_amount_to_int) +@click.option( + "--max-priority-fee", + help="Max Priority Fee (EIP1559)", + callback=_amount_to_int, +) +@click.option("-e", "--eip2718-type", type=int, help="EIP2718 tx type") @click.option( "--list-units", is_flag=True, @@ -192,6 +234,10 @@ def sign_tx( to_address, tx_type, token, + max_gas_fee, + max_priority_fee, + access_list, + eip2718_type, ): """Sign (and optionally publish) Ethereum transaction. @@ -216,9 +262,11 @@ def sign_tx( click.echo(" pip install web3 rlp") sys.exit(1) + is_eip1559 = eip2718_type == 2 w3 = web3.Web3() if ( - any(x is None for x in (gas_price, gas_limit, nonce)) + (not is_eip1559 and gas_price is None) + or any(x is None for x in (gas_limit, nonce)) or publish and not w3.isConnected() ): @@ -246,7 +294,7 @@ def sign_tx( else: data = b"" - if gas_price is None: + if gas_price is None and not is_eip1559: gas_price = w3.eth.gasPrice if gas_limit is None: @@ -262,27 +310,61 @@ def sign_tx( if nonce is None: nonce = w3.eth.getTransactionCount(from_address) - sig = ethereum.sign_tx( - client, - n=address_n, - tx_type=tx_type, - nonce=nonce, - gas_price=gas_price, - gas_limit=gas_limit, - to=to_address, - value=amount, - data=data, - chain_id=chain_id, + sig = ( + ethereum.sign_tx_eip1559( + client, + n=address_n, + nonce=nonce, + gas_limit=gas_limit, + to=to_address, + value=amount, + data=data, + chain_id=chain_id, + max_gas_fee=max_gas_fee, + max_priority_fee=max_priority_fee, + access_list=access_list, + ) + if is_eip1559 + else ethereum.sign_tx( + client, + n=address_n, + tx_type=tx_type, + nonce=nonce, + gas_price=gas_price, + gas_limit=gas_limit, + to=to_address, + value=amount, + data=data, + chain_id=chain_id, + ) ) to = _decode_hex(to_address) - if tx_type is None: + if is_eip1559: + transaction = rlp.encode( + ( + chain_id, + nonce, + max_priority_fee, + max_gas_fee, + gas_limit, + to, + amount, + data, + _format_access_list(access_list) if access_list is not None else [], + ) + + sig + ) + elif tx_type is None: transaction = rlp.encode((nonce, gas_price, gas_limit, to, amount, data) + sig) else: transaction = rlp.encode( (tx_type, nonce, gas_price, gas_limit, to, amount, data) + sig ) - tx_hex = "0x%s" % transaction.hex() + tx_hex = "0x%s%s" % ( + str(eip2718_type).zfill(2) if eip2718_type is not None else "", + transaction.hex(), + ) if publish: tx_hash = w3.eth.sendRawTransaction(tx_hex).hex() diff --git a/python/src/trezorlib/ethereum.py b/python/src/trezorlib/ethereum.py index 76a6121c4f..1b9e6de236 100644 --- a/python/src/trezorlib/ethereum.py +++ b/python/src/trezorlib/ethereum.py @@ -55,7 +55,7 @@ def sign_tx( msg = messages.EthereumSignTx( address_n=n, nonce=int_to_big_endian(nonce), - gas_price=int_to_big_endian(gas_price), + gas_price=int_to_big_endian(gas_price) if gas_price is not None else None, gas_limit=int_to_big_endian(gas_limit), value=int_to_big_endian(value), to=to, @@ -83,6 +83,47 @@ def sign_tx( return response.signature_v, response.signature_r, response.signature_s +@session +def sign_tx_eip1559( + client, + n, + *, + nonce, + gas_limit, + to, + value, + data=b"", + chain_id, + max_gas_fee, + max_priority_fee, + access_list=() +): + length = len(data) + data, chunk = data[1024:], data[:1024] + msg = messages.EthereumSignTxEIP1559( + address_n=n, + nonce=int_to_big_endian(nonce), + gas_limit=int_to_big_endian(gas_limit), + value=int_to_big_endian(value), + to=to, + chain_id=chain_id, + max_gas_fee=int_to_big_endian(max_gas_fee), + max_priority_fee=int_to_big_endian(max_priority_fee), + access_list=access_list, + data_length=length, + data_initial_chunk=chunk, + ) + + response = client.call(msg) + + while response.data_length is not None: + data_length = response.data_length + data, chunk = data[data_length:], data[:data_length] + response = client.call(messages.EthereumTxAck(data_chunk=chunk)) + + return response.signature_v, response.signature_r, response.signature_s + + @expect(messages.EthereumMessageSignature) def sign_message(client, n, message): message = normalize_nfc(message) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 5b44eaa97b..4acc24239d 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -117,6 +117,7 @@ class MessageType(IntEnum): EthereumGetAddress = 56 EthereumAddress = 57 EthereumSignTx = 58 + EthereumSignTxEIP1559 = 452 EthereumTxRequest = 59 EthereumTxAck = 60 EthereumSignMessage = 64 @@ -3971,6 +3972,50 @@ class EthereumSignTx(protobuf.MessageType): self.tx_type = tx_type +class EthereumSignTxEIP1559(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 452 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("nonce", "bytes", repeated=False, required=True), + 3: protobuf.Field("max_gas_fee", "bytes", repeated=False, required=True), + 4: protobuf.Field("max_priority_fee", "bytes", repeated=False, required=True), + 5: protobuf.Field("gas_limit", "bytes", repeated=False, required=True), + 6: protobuf.Field("to", "string", repeated=False, required=False), + 7: protobuf.Field("value", "bytes", repeated=False, required=True), + 8: protobuf.Field("data_initial_chunk", "bytes", repeated=False, required=False), + 9: protobuf.Field("data_length", "uint32", repeated=False, required=True), + 10: protobuf.Field("chain_id", "uint32", repeated=False, required=True), + 11: protobuf.Field("access_list", "EthereumAccessList", repeated=True, required=False), + } + + def __init__( + self, + *, + nonce: "bytes", + max_gas_fee: "bytes", + max_priority_fee: "bytes", + gas_limit: "bytes", + value: "bytes", + data_length: "int", + chain_id: "int", + address_n: Optional[List["int"]] = None, + access_list: Optional[List["EthereumAccessList"]] = None, + to: Optional["str"] = '', + data_initial_chunk: Optional["bytes"] = b'', + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.access_list = access_list if access_list is not None else [] + self.nonce = nonce + self.max_gas_fee = max_gas_fee + self.max_priority_fee = max_priority_fee + self.gas_limit = gas_limit + self.value = value + self.data_length = data_length + self.chain_id = chain_id + self.to = to + self.data_initial_chunk = data_initial_chunk + + class EthereumTxRequest(protobuf.MessageType): MESSAGE_WIRE_TYPE = 59 FIELDS = { @@ -4062,6 +4107,23 @@ class EthereumVerifyMessage(protobuf.MessageType): self.address = address +class EthereumAccessList(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("address", "string", repeated=False, required=True), + 2: protobuf.Field("storage_keys", "bytes", repeated=True, required=False), + } + + def __init__( + self, + *, + address: "str", + storage_keys: Optional[List["bytes"]] = None, + ) -> None: + self.storage_keys = storage_keys if storage_keys is not None else [] + self.address = address + + class LiskGetAddress(protobuf.MessageType): MESSAGE_WIRE_TYPE = 114 FIELDS = { diff --git a/tests/device_tests/test_msg_ethereum_signtx_eip1559.py b/tests/device_tests/test_msg_ethereum_signtx_eip1559.py new file mode 100644 index 0000000000..a6677b6799 --- /dev/null +++ b/tests/device_tests/test_msg_ethereum_signtx_eip1559.py @@ -0,0 +1,308 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import pytest + +from trezorlib import ethereum, messages +from trezorlib.tools import parse_path + +TO_ADDR = "0x1d1c328764a41bda0492b66baa30c4a339ff85ef" + + +pytestmark = [pytest.mark.altcoin, pytest.mark.ethereum, pytest.mark.skip_t1] + + +def test_ethereum_signtx_nodata(client): + with client: + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/100"), + nonce=0, + gas_limit=20, + to=TO_ADDR, + chain_id=1, + value=10, + max_gas_fee=20, + max_priority_fee=1, + ) + + assert sig_v == 1 + assert ( + sig_r.hex() + == "2ceeaabc994fbce2fbd66551f9d48fc711c8db2a12e93779eeddede11e41f636" + ) + assert ( + sig_s.hex() + == "2db4a9ecc73da91206f84397ae9287a399076fdc01ed7f3c6554b1c57c39bf8c" + ) + + +def test_ethereum_signtx_data(client): + with client: + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/0"), + nonce=0, + gas_limit=20, + chain_id=1, + to=TO_ADDR, + value=10, + data=b"abcdefghijklmnop" * 16, + max_gas_fee=20, + max_priority_fee=1, + ) + assert sig_v == 0 + assert ( + sig_r.hex() + == "8e4361e40e76a7cab17e0a982724bbeaf5079cd02d50c20d431ba7dde2404ea4" + ) + assert ( + sig_s.hex() + == "411930f091bb508e593e22a9ee45bd4d9eeb504ac398123aec889d5951bdebc3" + ) + + with client: + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/0"), + nonce=123456, + gas_limit=20000, + to=TO_ADDR, + chain_id=1, + value=12345678901234567890, + data=b"ABCDEFGHIJKLMNOP" * 256 + b"!!!", + max_gas_fee=20, + max_priority_fee=1, + ) + assert sig_v == 0 + assert ( + sig_r.hex() + == "2e4f4c0e7c4e51270b891480060712e9d3bcab01e8ad0fadf2dfddd71504ca94" + ) + assert ( + sig_s.hex() + == "2599beb32757a144dedc82b79153c21269c9939a9245342bcf35764115b62bc1" + ) + + +def test_ethereum_signtx_access_list(client): + with client: + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/100"), + nonce=0, + gas_limit=20, + to=TO_ADDR, + chain_id=1, + value=10, + max_gas_fee=20, + max_priority_fee=1, + access_list=[ + messages.EthereumAccessList( + address="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + storage_keys=[ + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000003" + ), + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000007" + ), + ], + ) + ], + ) + + assert sig_v == 1 + assert ( + sig_r.hex() + == "9f8763f3ff8d4d409f6b96bc3f1d84dd504e2c667b162778508478645401f121" + ) + assert ( + sig_s.hex() + == "51e30b68b9091cf8138c07380c4378c2711779b68b2e5264d141479f13a12f57" + ) + + +def test_ethereum_signtx_access_list_larger(client): + with client: + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/100"), + nonce=0, + gas_limit=20, + to=TO_ADDR, + chain_id=1, + value=10, + max_gas_fee=20, + max_priority_fee=1, + access_list=[ + messages.EthereumAccessList( + address="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + storage_keys=[ + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000003" + ), + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000007" + ), + ], + ), + messages.EthereumAccessList( + address="0xbb9bc244d798123fde783fcc1c72d3bb8c189413", + storage_keys=[ + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000006" + ), + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000007" + ), + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000009" + ), + ], + ), + ], + ) + + assert sig_v == 1 + assert ( + sig_r.hex() + == "718a3a30827c979975c846d2f60495310c4959ee3adce2d89e0211785725465c" + ) + assert ( + sig_s.hex() + == "7d0ea2a28ef5702ca763c1f340427c0020292ffcbb4553dd1c8ea8e2b9126dbc" + ) + + +def test_ethereum_signtx_known_erc20_token(client): + with client: + + data = bytearray() + # method id signalizing `transfer(address _to, uint256 _value)` function + data.extend(bytes.fromhex("a9059cbb")) + # 1st function argument (to - the receiver) + data.extend( + bytes.fromhex( + "000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b" + ) + ) + # 2nd function argument (value - amount to be transferred) + data.extend( + bytes.fromhex( + "000000000000000000000000000000000000000000000000000000000bebc200" + ) + ) + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/0"), + nonce=0, + max_gas_fee=20, + max_priority_fee=1, + gas_limit=20, + # ADT token address + to="0xd0d6d6c5fe4a677d343cc433536bb717bae167dd", + chain_id=1, + # value needs to be 0, token value is set in the contract (data) + value=0, + data=data, + ) + + assert sig_v == 1 + assert ( + sig_r.hex() + == "94d67bacb7966f881339d91103f5d738d9c491fff4c01a6513c554ab15e86cc0" + ) + assert ( + sig_s.hex() + == "405bd19a7bf4ae62d41fcb7844e36c786b106b456185c3d0877a7ce7eab6c751" + ) + + +def test_ethereum_signtx_unknown_erc20_token(client): + with client: + data = bytearray() + # method id signalizing `transfer(address _to, uint256 _value)` function + data.extend(bytes.fromhex("a9059cbb")) + # 1st function argument (to - the receiver) + data.extend( + bytes.fromhex( + "000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b" + ) + ) + # 2nd function argument (value - amount to be transferred) + data.extend( + bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000123" + ) + ) + # since this token is unknown trezor should display "unknown token value" + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/1"), + nonce=0, + max_gas_fee=20, + max_priority_fee=1, + gas_limit=20, + # unknown token address (Grzegorz Brzęczyszczykiewicz Token) + to="0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93", + chain_id=1, + # value needs to be 0, token value is set in the contract (data) + value=0, + data=data, + ) + + assert sig_v == 1 + assert ( + sig_r.hex() + == "e631b56bcc596844cb8686b2046e36cf33634aa396e7e1ea94a97aac02c18bda" + ) + assert ( + sig_s.hex() + == "399bff8752539176c4b2f1d5d2a8f6029f79841d28802149ab339a033ffe4c1f" + ) + + +def test_ethereum_signtx_large_chainid(client): + with client: + + sig_v, sig_r, sig_s = ethereum.sign_tx_eip1559( + client, + n=parse_path("44'/60'/0'/0/100"), + nonce=0, + gas_limit=20, + to=TO_ADDR, + chain_id=3125659152, # Pirl chain id, doesn't support EIP1559 at this time, but chosen for large chain id + value=10, + max_gas_fee=20, + max_priority_fee=1, + ) + + assert sig_v == 0 + assert ( + sig_r.hex() + == "07f8c967227c5a190cb90525c3387691a426fe61f8e0503274280724060ea95c" + ) + assert ( + sig_s.hex() + == "0bf83eaf74e24aa9146b23e06f9edec6e25acb81d3830e8d146b9e7b6923ad1e" + ) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 35ab04d541..0c7a05ef3d 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -235,6 +235,13 @@ "test_msg_ethereum_signtx_eip155.py::test_chain_ids[609112567-60-sig6]": "c8e01d20eccadcca4f05e4e8351c3bfc38d0fdbe4a61f63dfd74e065faea86e7", "test_msg_ethereum_signtx_eip155.py::test_chain_ids[61-61-sig3]": "cd5f04cc7b055503e83f0538709a7ac577445c6089ead12f1fc3a3c45ad96419", "test_msg_ethereum_signtx_eip155.py::test_with_data": "670913def0b7268671258f70dfbdc794a8405e1e432e423aae10b616028f3db9", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_access_list": "0d73046641e1d148ed01a36314509c7d38284c37d98b8a16b92f59cea543055c", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_access_list_larger": "0d73046641e1d148ed01a36314509c7d38284c37d98b8a16b92f59cea543055c", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_data": "17f52fa7d2edc36201050df432398839b2c27e82d6e01a2feafd58bbf5de19aa", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_known_erc20_token": "ca5c69923a79c1112bbdd81159adfbc389e1538c1776c141ce28fc72d0feeb81", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_large_chainid": "95ea813ceeb0782ff3554eafd463831f60490aa418385fc9cf113140dfa8c75c", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_nodata": "0d73046641e1d148ed01a36314509c7d38284c37d98b8a16b92f59cea543055c", +"test_msg_ethereum_signtx_eip1559.py::test_ethereum_signtx_unknown_erc20_token": "a25e1171b7dc9bdcfcfe02bf10e7ce8bfa02a0ec9b57fc3dce2b591cdec798e5", "test_msg_ethereum_verifymessage.py-test_verify": "f97754c9168c436209997dc17b4cf4fa0b55171fb2dca4b06256ed87fd910fdc", "test_msg_ethereum_verifymessage.py-test_verify_invalid": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586", "test_msg_getaddress.py-test_bch": "5a80508a71a9ef64f94762b07636f90e464832f0f4a3102af8fa1a8c69e94586",