Skip to content

Commit

Permalink
feat(core,python): support for Ethereum EIP1559 transactions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
FrederikBolding authored and matejcik committed Aug 10, 2021
1 parent 69564a9 commit 38fa919
Show file tree
Hide file tree
Showing 18 changed files with 824 additions and 45 deletions.
28 changes: 27 additions & 1 deletion common/protob/messages-ethereum.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions common/protob/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
1 change: 1 addition & 0 deletions core/.changelog.d/1604.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for Ethereum EIP1559 transactions
2 changes: 2 additions & 0 deletions core/src/all_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@
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
Expand Down
5 changes: 4 additions & 1 deletion core/src/apps/ethereum/keychain.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion core/src/apps/ethereum/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
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
Expand Down Expand Up @@ -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(
Expand Down
59 changes: 34 additions & 25 deletions core/src/apps/ethereum/sign_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,14 @@
@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:
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down
154 changes: 154 additions & 0 deletions core/src/apps/ethereum/sign_tx_eip1559.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 2 additions & 0 deletions core/src/apps/workflow_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions core/src/trezor/enums/MessageType.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
EthereumGetAddress = 56
EthereumAddress = 57
EthereumSignTx = 58
EthereumSignTxEIP1559 = 452
EthereumTxRequest = 59
EthereumTxAck = 60
EthereumSignMessage = 64
Expand Down
1 change: 1 addition & 0 deletions core/src/trezor/enums/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class MessageType(IntEnum):
EthereumGetAddress = 56
EthereumAddress = 57
EthereumSignTx = 58
EthereumSignTxEIP1559 = 452
EthereumTxRequest = 59
EthereumTxAck = 60
EthereumSignMessage = 64
Expand Down

0 comments on commit 38fa919

Please sign in to comment.