diff --git a/common/protob/messages-ethereum.proto b/common/protob/messages-ethereum.proto index 9fab13c2c..2f6ff12e5 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 557e37ae0..05a481353 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 000000000..4eb50cf08 --- /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 82100b224..6a2b756d5 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 5693fa119..ab1b6789b 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 0e37bb634..859905357 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 5468e2982..cca7d64f4 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -23,29 +23,14 @@ 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) - 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") + # Handle ERC20s + token, address_bytes, recipient, value = await handle_erc20(ctx, msg) - if token is tokens.UNKNOWN_TOKEN: - await require_confirm_unknown_token(ctx, address_bytes) + data_total = msg.data_length await require_confirm_tx(ctx, recipient, value, msg.chain_id, token, msg.tx_type) if token is None and msg.data_length > 0: @@ -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 000000000..b2533757c --- /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 9aa4234aa..244e903a2 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 2d6a38fc9..6c45510e1 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 08054d3b1..4466e6b77 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 e507a1f52..342a066ba 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 000000000..4eb50cf08 --- /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 ff27b9fd0..2f88a8d43 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 76a6121c4..1b9e6de23 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 5b44eaa97..4acc24239 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 000000000..a6677b679 --- /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 35ab04d54..0c7a05ef3 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",