Skip to content

Commit 7618495

Browse files
Merge pull request #739 from btchip/7702-phase1
Implementation of EIP 7702 - phase 1
2 parents 415ad29 + bd1bb2c commit 7618495

File tree

182 files changed

+1483
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+1483
-19
lines changed

client/src/ledger_app_clients/ethereum/client.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .tlv import format_tlv, FieldTag
1818
from .response_parser import pk_addr
1919
from .tx_simu import TxSimu
20+
from .tx_auth_7702 import TxAuth7702
2021

2122

2223
class StatusWord(IntEnum):
@@ -212,28 +213,35 @@ def eip712_filtering_trusted_name(self,
212213
def eip712_filtering_raw(self, name: str, sig: bytes, discarded: bool):
213214
return self._exchange_async(self._cmd_builder.eip712_filtering_raw(name, sig, discarded))
214215

215-
def serialize_tx(self, tx_params: dict) -> Tuple[bytes, bytes]:
216+
def serialize_tx(self, tx_params: dict, tx_raw: Optional[bytes] = None) -> Tuple[bytes, bytes]:
216217
"""Computes the serialized TX and its hash"""
217218

218-
tx = Web3().eth.account.create().sign_transaction(tx_params).rawTransaction
219+
if tx_raw is not None:
220+
tx = tx_raw
221+
else:
222+
tx = Web3().eth.account.create().sign_transaction(tx_params).rawTransaction
219223
prefix = bytes()
220224
suffix = []
221-
if tx[0] in [0x01, 0x02]:
225+
if tx[0] in [0x01, 0x02, 0x04]:
222226
prefix = tx[:1]
223227
tx = tx[len(prefix):]
224228
else: # legacy
225229
if "chainId" in tx_params:
226230
suffix = [int(tx_params["chainId"]), bytes(), bytes()]
227-
decoded_tx = rlp.decode(tx)[:-3] # remove already computed signature
231+
if tx_raw is None:
232+
decoded_tx = rlp.decode(tx)[:-3] # remove already computed signature
233+
else:
234+
decoded_tx = rlp.decode(tx)
228235
encoded_tx = prefix + rlp.encode(decoded_tx + suffix)
229236
tx_hash = keccak(encoded_tx)
230237
return encoded_tx, tx_hash
231238

232239
def sign(self,
233240
bip32_path: str,
234241
tx_params: dict,
242+
tx_raw: Optional[bytes] = None, # Introduced for 7702 until web3.py supports authorization lists
235243
mode: SignMode = SignMode.BASIC):
236-
tx, _ = self.serialize_tx(tx_params)
244+
tx, _ = self.serialize_tx(tx_params, tx_raw)
237245
chunks = self._cmd_builder.sign(bip32_path, tx, mode)
238246
for chunk in chunks[:-1]:
239247
self._exchange(chunk)
@@ -671,3 +679,9 @@ def provide_proxy_info(self, payload: bytes) -> RAPDU:
671679
for chunk in chunks[:-1]:
672680
self._exchange(chunk)
673681
return self._exchange(chunks[-1])
682+
683+
def sign_eip7702_authorization(self, auth_params: TxAuth7702) -> RAPDU:
684+
chunks = self._cmd_builder.sign_eip7702_authorization(auth_params.serialize())
685+
for chunk in chunks[:-1]:
686+
self._exchange(chunk)
687+
return self._exchange_async(chunks[-1])

client/src/ledger_app_clients/ethereum/command_builder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc
33

44
import struct
5+
import math
56
from enum import IntEnum
67
from typing import Optional
78
from ragger.bip import pack_derivation_path
@@ -31,6 +32,7 @@ class InsType(IntEnum):
3132
PROVIDE_PROXY_INFO = 0x2a
3233
PROVIDE_NETWORK_INFORMATION = 0x30
3334
PROVIDE_TX_SIMULATION = 0x32
35+
SIGN_EIP7702_AUTHORIZATION = 0x34
3436

3537

3638
class P1Type(IntEnum):
@@ -64,6 +66,11 @@ class P2Type(IntEnum):
6466
class CommandBuilder:
6567
_CLA: int = 0xE0
6668

69+
def _intToBytes(self, i: int) -> bytes:
70+
if i == 0:
71+
return b"\x00"
72+
return i.to_bytes(math.ceil(i.bit_length() / 8), 'big')
73+
6774
def _serialize(self,
6875
ins: InsType,
6976
p1: int,
@@ -456,6 +463,9 @@ def provide_network_information(self,
456463
p1 = P1Type.FOLLOWING_CHUNK
457464
return chunks
458465

466+
def sign_eip7702_authorization(self, tlv_payload: bytes) -> list[bytes]:
467+
return self.common_tlv_serialize(InsType.SIGN_EIP7702_AUTHORIZATION, tlv_payload)
468+
459469
def provide_enum_value(self, tlv_payload: bytes) -> list[bytes]:
460470
return self.common_tlv_serialize(InsType.PROVIDE_ENUM_VALUE, tlv_payload)
461471

client/src/ledger_app_clients/ethereum/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class SettingID(Enum):
1111
NONCE = auto()
1212
VERBOSE_EIP712 = auto()
1313
DEBUG_DATA = auto()
14+
EIP7702 = auto()
1415

1516

1617
def get_device_settings(firmware: Firmware) -> list[SettingID]:
@@ -28,6 +29,7 @@ def get_device_settings(firmware: Firmware) -> list[SettingID]:
2829
SettingID.NONCE,
2930
SettingID.VERBOSE_EIP712,
3031
SettingID.DEBUG_DATA,
32+
SettingID.EIP7702,
3133
]
3234
return [
3335
SettingID.WEB3_CHECK,
@@ -36,6 +38,7 @@ def get_device_settings(firmware: Firmware) -> list[SettingID]:
3638
SettingID.NONCE,
3739
SettingID.VERBOSE_EIP712,
3840
SettingID.DEBUG_DATA,
41+
SettingID.EIP7702,
3942
]
4043

4144

@@ -75,6 +78,11 @@ def get_setting_position(firmware: Firmware, setting: SettingID) -> tuple[int, i
7578
page, y = 2, 130
7679
else:
7780
page, y = 2, 315
81+
elif setting == SettingID.EIP7702:
82+
if firmware == Firmware.STAX:
83+
page, y = 2, 300
84+
else:
85+
page, y = 3, 140
7886
else:
7987
raise ValueError(f"Unknown setting: {setting}")
8088
return page, x, y
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Optional
2+
from enum import IntEnum
3+
4+
from bip_utils import Bip32Utils
5+
6+
from .tlv import TlvSerializable
7+
8+
9+
class FieldTag(IntEnum):
10+
STRUCT_VERSION = 0x00
11+
DERIVATION_IDX = 0x01
12+
DELEGATE_ADDR = 0x02
13+
CHAIN_ID = 0x03
14+
NONCE = 0x04
15+
16+
17+
class TxAuth7702(TlvSerializable):
18+
bip32_path: str
19+
delegate: bytes
20+
nonce: int
21+
chain_id: int
22+
23+
def __init__(self,
24+
bip32_path: str,
25+
delegate: bytes,
26+
nonce: int,
27+
chain_id: Optional[int]) -> None:
28+
self.bip32_path = bip32_path
29+
self.delegate = delegate
30+
self.nonce = nonce
31+
if chain_id is None:
32+
self.chain_id = 0
33+
else:
34+
self.chain_id = chain_id
35+
36+
def serialize(self) -> bytes:
37+
payload: bytes = self.serialize_field(FieldTag.STRUCT_VERSION, 1)
38+
split = self.bip32_path.split("/")
39+
if split[0] != "m":
40+
raise ValueError("Error master expected in bip32 path")
41+
for value in split[1:]:
42+
if value == "":
43+
raise ValueError(f'Error missing value in split bip 32 path list "{split}"')
44+
if value.endswith('\''):
45+
payload += self.serialize_field(FieldTag.DERIVATION_IDX,
46+
Bip32Utils.HardenIndex(int(value[:-1])).to_bytes(4, byteorder='big'))
47+
else:
48+
payload += self.serialize_field(FieldTag.DERIVATION_IDX,
49+
int(value).to_bytes(4, byteorder='big'))
50+
payload += self.serialize_field(FieldTag.DELEGATE_ADDR, self.delegate)
51+
payload += self.serialize_field(FieldTag.NONCE, self.nonce.to_bytes(8, 'big'))
52+
payload += self.serialize_field(FieldTag.CHAIN_ID, self.chain_id.to_bytes(8, 'big'))
53+
return payload

client/src/ledger_app_clients/ethereum/utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Optional
12
from eth_account import Account
23
from eth_account.messages import encode_defunct, encode_typed_data
34
import rlp
@@ -25,10 +26,13 @@ def recover_message(msg, vrs: tuple) -> bytes:
2526
return bytes.fromhex(addr[2:])
2627

2728

28-
def recover_transaction(tx_params, vrs: tuple) -> bytes:
29-
raw_tx = Account.create().sign_transaction(tx_params).rawTransaction
29+
def recover_transaction(tx_params, vrs: tuple, raw_tx_param: Optional[bytes] = None) -> bytes:
30+
if raw_tx_param is None:
31+
raw_tx = Account.create().sign_transaction(tx_params).rawTransaction
32+
else:
33+
raw_tx = raw_tx_param
3034
prefix = bytes()
31-
if raw_tx[0] in [0x01, 0x02]:
35+
if raw_tx[0] in [0x01, 0x02, 0x04]:
3236
prefix = raw_tx[:1]
3337
raw_tx = raw_tx[len(prefix):]
3438
else:
@@ -58,6 +62,9 @@ def recover_transaction(tx_params, vrs: tuple) -> bytes:
5862
# Pre EIP-155 TX
5963
assert False
6064
decoded = rlp.decode(raw_tx)
61-
reencoded = rlp.encode(decoded[:-3] + list(normalize_vrs(vrs)))
65+
if raw_tx_param is None:
66+
reencoded = rlp.encode(decoded[:-3] + list(normalize_vrs(vrs)))
67+
else:
68+
reencoded = rlp.encode(decoded + list(normalize_vrs(vrs)))
6269
addr = Account.recover_transaction(prefix + reencoded)
6370
return bytes.fromhex(addr[2:])

doc/ethapp.adoc

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ _Output data_
9797
9898
#### Description
9999
100-
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md
100+
The application supports signing legacy or EIP 2718 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transactions for Type 2 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) and Type 4 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md)
101101
102102
This command signs an Ethereum transaction after having the user validate the following parameters
103103
@@ -1455,6 +1455,56 @@ None
14551455
| Web3 Check setting status | 1
14561456
|====================================
14571457
1458+
### SIGN EIP 7702 AUTHORIZATION
1459+
1460+
#### Description
1461+
1462+
This command computes the signature for an element of a EIP 7702 Authorization list (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) on the given account, for the given delegate, chain ID and nonce.
1463+
1464+
The user is prompted to confirm the operation before the signature is issued.
1465+
1466+
#### Coding
1467+
1468+
_Command_
1469+
1470+
[width="80%"]
1471+
|==============================================================
1472+
| *CLA* | *INS* | *P1* | *P2* | *LC*
1473+
| E0 | 34 | 01 : first chunk
1474+
1475+
00 : following chunk
1476+
| 00 | 00
1477+
|==============================================================
1478+
1479+
_Input data_
1480+
1481+
##### If P1 == first chunk
1482+
1483+
[width="80%"]
1484+
|====================================================================
1485+
| *Description* | *Length (byte)*
1486+
| struct size (BE) | 2
1487+
| link:tlv_structs.md#auth_7702[AUTH_7702 struct] | variable
1488+
|====================================================================
1489+
1490+
##### If P1 == following chunk
1491+
1492+
[width="80%"]
1493+
|====================================================================
1494+
| *Description* | *Length (byte)*
1495+
| link:tlv_structs.md#auth_7702[AUTH_7702 struct] | variable
1496+
|====================================================================
1497+
1498+
_Output data_
1499+
1500+
[width="80%"]
1501+
|==============================================================================================================================
1502+
| *Description* | *Length*
1503+
| Signature parity (0 even, 1 odd) - use as is in EIP 7702 | 1
1504+
| r | 32
1505+
| s | 32
1506+
|==============================================================================================================================
1507+
14581508
## Transport protocol
14591509
14601510
### General transport description

doc/tlv_structs.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,13 @@ In version 1 of the protocol:
312312
| SELECTOR | 0x28 | uint[4] | function selector | x |
313313
| IMPL_ADDRESS | 0x29 | uint8[20] | implementation contract address | |
314314
| SIGNATURE | 0x15 | uint8[] | signature of the structure | |
315+
316+
## AUTH_7702
317+
318+
| Name | Tag | Payload type | Description | Optional |
319+
|----------------|------|-----------------|---------------------------------|----------|
320+
| STRUCT_VERSION | 0x00 | uint8 | structure version (currently 1) | |
321+
| DERIVATION_IDX | 0x01 | uint32 | BIP32 derivation path item | |
322+
| DELEGATE_ADDR | 0x02 | uint8[20] | delegate address | |
323+
| CHAIN_ID | 0x03 | uint64 | Chain ID (00 for no restriction)| |
324+
| NONCE | 0x04 | uint64 | nonce | |
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python
2+
"""
3+
*******************************************************************************
4+
* Ledger Ethereum App
5+
* (c) 2016-2019 Ledger
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
********************************************************************************
19+
"""
20+
from __future__ import print_function
21+
22+
from ledgerblue.comm import getDongle
23+
from ledgerblue.commException import CommException
24+
import argparse
25+
import struct
26+
import binascii
27+
import math
28+
import rlp
29+
30+
31+
32+
def der_encode(value):
33+
value_bytes = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big')
34+
if value >= 0x80:
35+
value_bytes = (0x80 | len(value_bytes)).to_bytes(1, 'big') + value_bytes
36+
return value_bytes
37+
38+
def tlv_encode(tag, value):
39+
return der_encode(tag) + der_encode(len(value)) + value
40+
41+
def parse_bip32_path(path):
42+
if len(path) == 0:
43+
return b""
44+
result = b""
45+
elements = path.split('/')
46+
for pathElement in elements:
47+
element = pathElement.split('\'')
48+
result = result + der_encode(0x01) + der_encode(0x04)
49+
if len(element) == 1:
50+
result = result + struct.pack(">I", int(element[0]))
51+
else:
52+
result = result + struct.pack(">I", 0x80000000 | int(element[0]))
53+
return result
54+
55+
56+
parser = argparse.ArgumentParser()
57+
parser.add_argument('--path', help="BIP 32 path to retrieve")
58+
parser.add_argument('--chainid', help="Chain ID", type=int, required=True)
59+
parser.add_argument('--nonce', help="Account Nonce", type=int, required=True)
60+
parser.add_argument('--delegate', help="Delegate address", type=str, required=True)
61+
args = parser.parse_args()
62+
63+
if args.path == None:
64+
args.path = "44'/60'/0'/0/0"
65+
66+
tmp = tlv_encode(0x00, struct.pack(">B", 0x01))
67+
tmp += parse_bip32_path(args.path)
68+
data = binascii.unhexlify(args.delegate[2:])
69+
tmp += tlv_encode(0x02, data)
70+
tmp += tlv_encode(0x03, struct.pack(">Q", args.chainid))
71+
tmp += tlv_encode(0x04, struct.pack(">Q", args.nonce))
72+
73+
tmp = struct.pack(">H", len(tmp)) + tmp
74+
75+
apdu = bytearray.fromhex("e0340100")
76+
apdu += struct.pack(">B", len(tmp))
77+
apdu += tmp
78+
79+
dongle = getDongle(True)
80+
result = dongle.exchange(bytes(apdu))
81+
82+
v = result[0]
83+
r = result[1 : 1 + 32]
84+
s = result[1 + 32 :]
85+
86+
print("v = " + str(v))
87+
print("r = " + binascii.hexlify(r).decode('utf-8'))
88+
print("s = " + binascii.hexlify(s).decode('utf-8'))
89+
90+
rlpData = [ args.chainid, binascii.unhexlify(args.delegate[2:]), args.nonce, v, r, s ]
91+
print(binascii.hexlify(rlp.encode(rlpData)).decode('utf-8'))

0 commit comments

Comments
 (0)