Skip to content

Commit bfb7374

Browse files
committed
fix: throw errors on chain IDs above what app-ethereum plays well with. Issue #41
1 parent c2504ef commit bfb7374

File tree

4 files changed

+198
-2
lines changed

4 files changed

+198
-2
lines changed

ledgereth/objects.py

+47
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"chainId",
5454
"accessList",
5555
]
56+
MAX_LEGACY_CHAIN_ID = 0xFFFFFFFF + 1
57+
MAX_CHAIN_ID = 0x38D7EA4C67FFF
5658

5759

5860
class TransactionType(IntEnum):
@@ -241,6 +243,9 @@ def to_rpc_dict(self) -> Dict[str, Any]:
241243
class Transaction(SerializableTransaction):
242244
"""Unsigned legacy or `EIP-155`_ transaction
243245
246+
.. warning:: chain_id for type 0 ("Legacy") transactions must be less than
247+
4294967295, the largest 32-bit unsigned integer.
248+
244249
.. note:: A chain_id is set by default (``1``). It is not required to be
245250
a valid legacy transaction, but without it your transaction is
246251
suceptible to replay attack. If for some reason you absolutely do not
@@ -289,6 +294,18 @@ def __init__(
289294
:param dummy1: (``int``) **DO NOT SET**
290295
:param dummy2: (``int``) **DO NOT SET**
291296
"""
297+
298+
if chain_id > MAX_LEGACY_CHAIN_ID:
299+
"""Chain IDs above 32-bits seems to cause app-ethereum to create
300+
invalid signatures. It's not yet clear why this is, or where the
301+
bug is, or even if it's a bug. See the following issue for details:
302+
303+
https://github.com/mikeshultz/ledger-eth-lib/issues/41
304+
"""
305+
raise ValueError(
306+
"chain_id must be a 32-bit integer for type 0 transactions. (See issue #41)"
307+
)
308+
292309
super().__init__(
293310
nonce,
294311
gas_price,
@@ -321,6 +338,9 @@ def from_rawtx(cls, rawtx: bytes) -> Transaction:
321338
class Type1Transaction(SerializableTransaction):
322339
"""An unsigned Type 1 transaction.
323340
341+
.. warning:: chain_id for type 1 transactions must be less than 999999999999999,
342+
the largest unsigned integer that the device can render on-screen.
343+
324344
Encoded tx format spec:
325345
326346
.. code::
@@ -366,6 +386,18 @@ def __init__(
366386
list
367387
"""
368388
access_list = access_list or []
389+
390+
if chain_id > MAX_CHAIN_ID:
391+
"""Chain IDs above 999999999999999 cause app-ethereum to throw an error
392+
because its unable to render on the device.
393+
394+
Ref: https://github.com/mikeshultz/ledger-eth-lib/issues/41
395+
Ref: https://github.com/LedgerHQ/app-ethereum/issues/283
396+
"""
397+
raise ValueError(
398+
"chain_id must not be above 999999999999999. (See issue #41)"
399+
)
400+
369401
super().__init__(
370402
chain_id,
371403
nonce,
@@ -400,6 +432,9 @@ def from_rawtx(cls, rawtx: bytes) -> Type1Transaction:
400432
class Type2Transaction(SerializableTransaction):
401433
"""An unsigned Type 2 transaction.
402434
435+
.. warning:: chain_id for type 2 transactions must be less than 999999999999999,
436+
the largest unsigned integer that the device can render on-screen.
437+
403438
Encoded TX format spec:
404439
405440
.. code::
@@ -450,6 +485,18 @@ def __init__(
450485
list
451486
"""
452487
access_list = access_list or []
488+
489+
if chain_id > MAX_CHAIN_ID:
490+
"""Chain IDs above 999999999999999 cause app-ethereum to throw an error
491+
because its unable to render on the device.
492+
493+
Ref: https://github.com/mikeshultz/ledger-eth-lib/issues/41
494+
Ref: https://github.com/LedgerHQ/app-ethereum/issues/283
495+
"""
496+
raise ValueError(
497+
"chain_id must not be above 999999999999999. (See issue #41)"
498+
)
499+
453500
super().__init__(
454501
chain_id,
455502
nonce,

tests/conftest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def _reset(self):
4141
self.stack = []
4242

4343
def _resp_to_bytearray(self, resp):
44-
# for large chain_id, v can be bigger than a byte. we'll take the lowest byte for the signature
44+
# for large chain_id, v can be bigger than a byte. we'll take the lowest
45+
# byte for the signature
4546
v = resp.v.to_bytes(8, "big")[7].to_bytes(1, "big")
4647
r = resp.r.to_bytes(32, "big")
4748
s = resp.s.to_bytes(32, "big")

tests/test_chain_id.py

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Test signing for multiple chains"""
2+
import pytest
3+
from eth_account import Account
4+
5+
from ledgereth.accounts import get_accounts
6+
from ledgereth.objects import MAX_CHAIN_ID, MAX_LEGACY_CHAIN_ID
7+
from ledgereth.transactions import create_transaction
8+
9+
10+
def test_max_legacy_chain_ids(yield_dongle):
11+
"""Test that the max legacy chain ID works"""
12+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e900"
13+
14+
with yield_dongle() as dongle:
15+
sender = get_accounts(dongle=dongle, count=1)[0].address
16+
17+
signed = create_transaction(
18+
destination=destination,
19+
amount=int(10e17),
20+
gas=int(1e6),
21+
gas_price=int(1e9),
22+
data="",
23+
nonce=2023,
24+
chain_id=MAX_LEGACY_CHAIN_ID,
25+
dongle=dongle,
26+
)
27+
28+
assert sender == Account.recover_transaction(signed.rawTransaction)
29+
30+
31+
def test_invalid_legacy_chain_ids(yield_dongle):
32+
"""Test that chain IDs above max legacy chain ID fail"""
33+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e901"
34+
35+
with yield_dongle() as dongle:
36+
sender = get_accounts(dongle=dongle, count=1)[0].address
37+
38+
with pytest.raises(
39+
ValueError,
40+
match="chain_id must be a 32-bit integer for type 0 transactions",
41+
):
42+
create_transaction(
43+
destination=destination,
44+
amount=int(10e17),
45+
gas=int(1e6),
46+
gas_price=int(1e9),
47+
data="",
48+
nonce=2023,
49+
chain_id=MAX_LEGACY_CHAIN_ID + 1,
50+
dongle=dongle,
51+
)
52+
53+
54+
def test_max_type1_chain_ids(yield_dongle):
55+
"""Test that the max type-1 chain ID works"""
56+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e902"
57+
58+
with yield_dongle() as dongle:
59+
sender = get_accounts(dongle=dongle, count=1)[0].address
60+
61+
signed = create_transaction(
62+
destination=destination,
63+
amount=int(10e17),
64+
gas=int(1e6),
65+
access_list=[],
66+
gas_price=int(1e9),
67+
data="",
68+
nonce=2023,
69+
chain_id=MAX_CHAIN_ID,
70+
dongle=dongle,
71+
)
72+
73+
assert sender == Account.recover_transaction(signed.rawTransaction)
74+
75+
76+
def test_invalid_type1_chain_ids(yield_dongle):
77+
"""Test that IDs above the max chain ID fail"""
78+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e903"
79+
80+
with yield_dongle() as dongle:
81+
sender = get_accounts(dongle=dongle, count=1)[0].address
82+
83+
with pytest.raises(
84+
ValueError,
85+
match="chain_id must not be above 999999999999999",
86+
):
87+
create_transaction(
88+
destination=destination,
89+
amount=int(10e17),
90+
gas=int(1e6),
91+
access_list=[],
92+
gas_price=int(1e9),
93+
data="",
94+
nonce=2023,
95+
chain_id=MAX_CHAIN_ID + 1,
96+
dongle=dongle,
97+
)
98+
99+
100+
def test_max_type2_chain_ids(yield_dongle):
101+
"""Test that the max chain ID works for type-2 transactions"""
102+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e904"
103+
104+
with yield_dongle() as dongle:
105+
sender = get_accounts(dongle=dongle, count=1)[0].address
106+
107+
signed = create_transaction(
108+
destination=destination,
109+
amount=int(10e17),
110+
gas=int(1e6),
111+
max_fee_per_gas=int(1e9),
112+
max_priority_fee_per_gas=int(1e8),
113+
data="",
114+
nonce=2023,
115+
chain_id=MAX_CHAIN_ID,
116+
dongle=dongle,
117+
)
118+
119+
assert sender == Account.recover_transaction(signed.rawTransaction)
120+
121+
122+
def test_invalid_type2_chain_ids(yield_dongle):
123+
"""Test that IDs above the max chain ID fail for type-2 transactions"""
124+
destination = "0xf0155486a14539f784739be1c02e93f28eb8e905"
125+
126+
with yield_dongle() as dongle:
127+
sender = get_accounts(dongle=dongle, count=1)[0].address
128+
129+
with pytest.raises(
130+
ValueError,
131+
match="chain_id must not be above 999999999999999",
132+
):
133+
create_transaction(
134+
destination=destination,
135+
amount=int(10e17),
136+
gas=int(1e6),
137+
max_fee_per_gas=int(1e9),
138+
max_priority_fee_per_gas=int(1e8),
139+
data="",
140+
nonce=2023,
141+
chain_id=MAX_CHAIN_ID + 1,
142+
dongle=dongle,
143+
)

tests/test_web3.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from web3 import Web3
55
from web3.datastructures import AttributeDict
66
from web3.providers.eth_tester import EthereumTesterProvider
7+
from web3.providers.eth_tester.defaults import API_ENDPOINTS, static_return
78
from web3.types import TxReceipt, Wei
89

910
from ledgereth.accounts import get_accounts
@@ -38,7 +39,11 @@ def fund_account(web3: Web3, address: str, amount: int = int(1e18)) -> TxReceipt
3839

3940
def test_web3_middleware_legacy(yield_dongle):
4041
"""Test LedgerSignerMiddleware with a legacy transaction"""
41-
provider = EthereumTesterProvider()
42+
endpoints = {**API_ENDPOINTS}
43+
# Max chain ID for legacy transactions
44+
# Ref: https://github.com/mikeshultz/ledger-eth-lib/issues/41
45+
endpoints["eth"]["chainId"] = static_return(4294967295)
46+
provider = EthereumTesterProvider(api_endpoints=endpoints)
4247
web3 = Web3(provider)
4348
clean_web3 = Web3(provider)
4449
alice_address = web3.eth.accounts[0]

0 commit comments

Comments
 (0)