Skip to content

state: Recognize chain-id protected legacy transaction - #1616

Merged
chfast merged 1 commit into
masterfrom
state/tx-chain-id-protected
Jul 27, 2026
Merged

state: Recognize chain-id protected legacy transaction#1616
chfast merged 1 commit into
masterfrom
state/tx-chain-id-protected

Conversation

@chfast

@chfast chfast commented Jul 27, 2026

Copy link
Copy Markdown
Member

The chain id check exempted every legacy transaction with chain_id 0, because the decoder folded the wire v into (chain_id, y_parity), and that makes v = 35/36 -- protected for chain 0 (EIP-155) -- indistinguishable from the unprotected v = 27/28. A transaction signed for chain 0 was therefore accepted on any chain.

Keep v verbatim instead, the way rlp_encode() writes it and the JSON transaction loader reads it, and derive chain_id next to it: v >= 35 is exactly the EIP-155 form, so Transaction::chain_id_protected() decides the check, and decoding a transaction becomes the inverse of encoding it. Requiring the whole v to fit uint64_t bounds a legacy chain id to 2**63 - 18, the limit the loader already had.

A state test identifies its sender directly and usually carries no signature, so the exported transaction now also carries v; without it a re-executed test would take every legacy transaction for an unprotected one.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes legacy (EIP-155) chain-id protection handling—especially the ambiguous chainId == 0 case—by keeping the legacy signature v verbatim through encode/decode and using v >= 35 as the definitive “chain-id protected” signal. It updates state-test JSON import/export to preserve v, makes RLP decoding the inverse of encoding for legacy v, and expands unit coverage for chain-id validation and legacy v bounds.

Changes:

  • Preserve legacy wire v in state::Transaction decode/encode and derive chain_id from it.
  • Introduce Transaction::chain_id_protected() and use it in chain-id validation (including chain 0 protected legacy tx).
  • Export/load v in state-test JSON and update unit tests to cover chain 0 protection and max legacy v behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/utils/statetest_loader.cpp Loads optional v for state-test multi-tx templates so legacy chain protection can be determined.
test/utils/statetest_export.cpp Exports v to state-test JSON to preserve legacy chain binding semantics for re-execution.
test/utils/rlp_encode.cpp Clarifies legacy v semantics in encoder comment (verbatim v carries chain binding since EIP-155).
test/unittests/state_transition_tx_test.cpp Adds chain-id validation coverage for protected legacy tx with chainId == 0.
test/unittests/state_rlp_decode_test.cpp Updates legacy round-trip expectations to keep v verbatim; adds max-legacy-v case.
test/state/transaction.hpp Adds chain_id_protected() and documents v as wire value for legacy transactions.
test/state/transaction.cpp Decodes legacy v as uint64_t verbatim and derives chain_id alongside it.
test/state/state.cpp Switches chain-id validation to chain_id_protected() gating (pre-EIP-155 legacy allowed cross-chain).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/state/state.cpp Outdated
Comment on lines 441 to 444
// chain_id must match the chain, unless the transaction is not signed for any chain in
// particular: a legacy transaction predating EIP-155 may be included in any of them.
if (tx.chain_id_protected() && tx.chain_id != block.chain_id)
return make_error_code(INVALID_CHAIN_ID);
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.46%. Comparing base (fd14fd8) to head (0f5b23c).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1616      +/-   ##
==========================================
- Coverage   97.46%   97.46%   -0.01%     
==========================================
  Files         170      170              
  Lines       15388    15383       -5     
  Branches     3596     3593       -3     
==========================================
- Hits        14998    14993       -5     
  Misses        282      282              
  Partials      108      108              
Flag Coverage Δ
eest-develop 88.28% <57.14%> (+0.02%) ⬆️
eest-develop-gmp 25.60% <16.00%> (-0.01%) ⬇️
eest-legacy 16.75% <0.00%> (+<0.01%) ⬆️
eest-libsecp256k1 27.73% <16.00%> (-0.01%) ⬇️
eest-stable 88.25% <57.14%> (+0.02%) ⬆️
evmone-unittests 93.01% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 95.99% <100.00%> (-0.01%) ⬇️
tooling 90.31% <100.00%> (+0.01%) ⬆️
tests 99.80% <100.00%> (-0.01%) ⬇️
Files with missing lines Coverage Δ
test/state/state.cpp 99.69% <100.00%> (ø)
test/state/transaction.cpp 100.00% <100.00%> (ø)
test/state/transaction.hpp 100.00% <100.00%> (ø)
test/unittests/state_rlp_decode_test.cpp 99.74% <100.00%> (-0.01%) ⬇️
test/unittests/state_transition_tx_test.cpp 100.00% <100.00%> (ø)
test/utils/rlp_encode.cpp 100.00% <ø> (ø)
test/utils/statetest_export.cpp 73.01% <100.00%> (+0.21%) ⬆️
test/utils/statetest_loader.cpp 92.94% <100.00%> (+0.02%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread test/state/state.cpp Outdated
Comment thread test/state/transaction.cpp Outdated
Comment thread test/state/transaction.hpp Outdated
Comment thread test/state/transaction.hpp Outdated
Comment thread test/state/transaction.hpp Outdated
Comment thread test/utils/rlp_encode.cpp Outdated
Comment thread test/utils/statetest_export.cpp Outdated
Comment thread test/utils/statetest_loader.cpp Outdated
…d one

The chain id check exempted every legacy transaction with chain_id 0,
because the decoder folded the wire v into (chain_id, y_parity), and that
makes v = 35/36 -- protected for chain 0 (EIP-155) -- indistinguishable
from the unprotected v = 27/28. A transaction signed for chain 0 was
therefore accepted on any chain.

Keep v verbatim instead, the way rlp_encode() writes it and the JSON
transaction loader reads it, and derive chain_id next to it: v >= 35 is
exactly the EIP-155 form, so Transaction::chain_id_protected() decides the
check, and decoding a transaction becomes the inverse of encoding it.
Requiring the whole v to fit uint64_t bounds a legacy chain id to
2**63 - 18, the limit the loader already had.

A state test identifies its sender directly and usually carries no
signature, so the exported transaction now also carries v; without it a
re-executed test would take every legacy transaction for an unprotected
one.
@chfast
chfast force-pushed the state/tx-chain-id-protected branch from 0f5b23c to 13a7971 Compare July 27, 2026 11:25
@chfast
chfast requested a review from Copilot July 27, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@chfast
chfast merged commit aa82563 into master Jul 27, 2026
22 checks passed
@chfast
chfast deleted the state/tx-chain-id-protected branch July 27, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants