fix(core/vm,params): fail 0x0a calls instead of faking success - #2666
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
8fa97f6 to
e60373d
Compare
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The implementation is well tested, but future-fork precompile routing changes consensus behavior and warrants final human review.
Review effort: Balanced
Findings: None
What changed in this PR
Adds an always-failing 0x0a KZG precompile stub for future Prague and Osaka rules without changing active network semantics.
Changes:
- Adds Prague precompile routing and address registration.
- Registers the failing KZG stub in Prague and Osaka.
- Adds gas constants and focused membership/behavior tests.
| File | Description |
|---|---|
params/protocol_params.go |
Defines the stub’s gas cost. |
core/vm/contracts.go |
Adds Prague routing and the failing stub. |
core/vm/contracts_kzg_stub_test.go |
Tests registration, composition, and failure behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
389cebe to
dc5ddce
Compare
b2caad1 to
15c65af
Compare
15c65af to
76bd21f
Compare
XDC has no blob ecosystem, so the EIP-4844 point evaluation precompile at 0x0a
was never implemented. The address was also missing from every precompile
bucket, so a call to it fell through to the empty-account path: it returned
success with empty output and refunded the gas given to the frame. A contract
that checks only the call status would conclude the KZG proof had been verified.
This registers an always-failing stub for 0x0a in a new Prague bucket and in the
Osaka bucket. The stub returns a plain error rather than ErrExecutionReverted, so
evm.Call consumes the whole frame, which is what geth already does for a
malformed proof. It is not a KZG implementation: every input fails, including a
well-formed 192-byte proof. It only stops the address from faking success.
PrecompiledContractsOsaka is written with the same hexadecimal keys as the new
Prague bucket, so its nine existing entries are rewritten from []byte{1} to
[]byte{0x01} and so on. The values are unchanged, and the hexadecimal spelling is
what upstream geth uses for both buckets.
Restricting the entry to Prague and Osaka is the whole safety argument.
Precompile sets are selected by block number, so a single entry added to an
already-active bucket would replay every block since that fork under different
semantics: gas accounting, account creation and value transfer all change, and
any historical call to 0x0a would flip from success to failure. This fork has no
Cancun bucket at all, mainnet and Apothem still resolve precompiles through the
EIP1559 bucket, and Prague and Osaka are nil on both, so no active bucket
changes. Devnet (chainId 551) schedules Prague at block 50000 and is treated as
resetable, so a devnet past that block is the only chain whose behaviour flips.
EIP-2537 (0x0b-0x11) is still absent from the Prague bucket and is tracked
separately. Whoever adds it must keep the 0x0a entry: the buckets are
hand-written literal maps with no canonical address list to check against.
The tests pin what the above relies on. Buckets are compared as address sets
rather than by length, because Prague and Osaka hold the same ten addresses and a
length check cannot tell them apart; rule selection is pinned on the only entry
that varies per fork, the bigModExp variant behind activePrecompiledContracts
(eip7823/eip7883 from Osaka on), without which pointing the Prague rules at the
Osaka bucket would pass every other assertion. The stub is exercised from empty
input to 200 bytes so a change that starts accepting some inputs fails a test
instead of quietly weakening the guarantee.
docs/solidity.md now describes 0x0a per network instead of the blanket "not
available either" added by XinFinOrg#2689: registered and always failing once the Prague
set is active, unregistered and succeeding as an empty-account call on mainnet
and Apothem.
76bd21f to
447659f
Compare
…um#30978 Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: XinFinOrg#2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub XinFinOrg#2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
…um#30978 Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: XinFinOrg#2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub XinFinOrg#2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
…um#30978 Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: XinFinOrg#2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub XinFinOrg#2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
…um#30978 Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: XinFinOrg#2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub XinFinOrg#2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
…um#30978 Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: XinFinOrg#2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub XinFinOrg#2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
…um#30978 (#2668) Port the EIP-2537 precompiles (0x0b-0x11) from go-ethereum: G1ADD, G1MSM, G2ADD, G2MSM, PAIRING, MAP_FP_TO_G1 and MAP_FP_TO_G2, along with the Bls12381 gas constants and the multi-exponentiation discount tables. ported from geth ethereum#21018, ethereum#29441, ethereum#29445, ethereum#29552, ethereum#30978 The precompiles are added to PrecompiledContractsPrague and to PrecompiledContractsOsaka, the two buckets upstream registers them in. Both buckets already exist on dev-upgrade: #2666 introduced PrecompiledContractsPrague holding the always-failing 0x0a stub, so 0x0b-0x11 were still empty accounts under Prague rules. The upstream-only Cacheable/NormalizeInput helpers are dropped because this fork has no precompile result cache. The 0x0a point evaluation precompile itself is deliberately not ported, since XDC has no blob ecosystem: the address keeps the failing stub #2666 registered, and the 0x100 p256Verify precompile is still absent. Test vectors and cases are taken from upstream, including the failure vectors (testJsonFail/loadJsonFail are introduced for them).
What
0x0a(the EIP-4844 point evaluation precompile) is not implemented: XDC has no blob ecosystem. The address was also missing from every precompile bucket, so a call to it fell through to the empty-account path — success, empty output, and the gas handed to the frame refunded. A contract that checks only the call status would conclude that a KZG proof had been verified.This registers an always-failing stub for
0x0ain a newPrecompiledContractsPraguebucket and inPrecompiledContractsOsaka.Behaviour
The stub returns a plain error rather than
ErrExecutionReverted, soevm.Callconsumes the whole frame — the same thing geth already does for a malformed proof.It is not a KZG implementation: every input fails, including a well-formed 192-byte proof. It only stops the address from faking success.
Why no activated bucket is touched
Precompile sets are selected by block number, so a single entry added to an already-active bucket would replay every block since that fork under different semantics: gas accounting, account creation and value transfer all change, and any historical call to
0x0awould flip from success to failure.This fork has no Cancun precompile bucket. Mainnet (
cancunBlock98802000) and Apothem (cancunBlock71551800) are both past Cancun, butactivePrecompiledContractshas noIsCancuncase, so they keep resolving precompiles through the EIP1559 bucket, which does not gain the entry. Prague and Osaka are unset on both.0x0astays unregistered0x0astays unregistered0x0astart failing at block 50000No activated bucket changes. Devnet is treated as resetable, so the only chain whose behaviour flips is one that gets reset anyway. No network runs the Osaka set, because no named network configuration sets
osakaBlock.Known gaps (deliberately not addressed here)
EIP-2537 (
0x0b-0x11) is not provided by the new Prague bucket and is tracked separately in #2668.0x100(P256VERIFY, #2694) is out of scope.Landing order matters.
git merge-tree --write-tree, re-verified against the currentdev-upgrade, reports a content conflict incore/vm/contracts.gowith both #2668 and #2694;params/protocol_params.gomerges on its own. #2668 also introducesPrecompiledContractsPrague, and its bucket has no0x0aentry at all, so whoever rebases second has to re-add the stub by hand.TestKZGPointEvaluationStubSetis the checkpoint to re-run afterwards.Visibility changes
XDPoS_getConfigreports aKZG_POINT_EVALUATIONentry under Prague rules, becauseparams/chainconfigviewkeys the active precompiles by name. This repository has noeth_configmethod.Tracers built on
ActivePrecompiles(4byte, flat callTracer, the legacy JS tracers) now classify0x0aas a precompile under those rules.core/state_transition.goalso passesvm.ActivePrecompiles(rules)intostate.Prepare, so under Prague and Osaka rules0x0ajoins the EIP-2929 access list and is warm from the start of every transaction. A first access to the address —BALANCE,EXTCODEHASH, aCALL— is then charged the warm cost (100) instead of the cold one (2600). This is in addition to the call itself now failing, and like the rest of this PR it only takes effect once a Prague or Osaka bucket is active.Tests
core/vm/contracts_kzg_stub_test.gopins:0x0apresent in Prague/Osaka, absent from Homestead/Byzantium/Istanbul/XDCv2/EIP1559 and from the Cancun-era rules the live networks run under, so the entry can never leak into an activated bucket;eip2565from Berlin on,eip7823/eip7883from Osaka on);bigModExpvariant behindactivePrecompiledContractsis what tells the two rule sets apart — without it, pointing the Prague rules at the Osaka bucket would pass every other assertion;evm.Callto0x0aunder Prague rules fails withleftOverGas == 0.Buckets are compared as address sets rather than by length, because a length check cannot tell two buckets apart when they hold the same number of addresses. The price is pinned to the upstream literal (
50000), and the stub is exercised with inputs from empty to 200 bytes, including the 192-byte shape a real point evaluation would accept.Every assertion was mutation-checked: selecting the Prague bucket from Cancun rules, wrong modexp flags, a dropped address, a succeeding
Run, a missing registration, an address list of the same length holding a different address, a changed gas literal, an accepted 192-byte input, and pointing the Prague rules at the Osaka bucket each fail a distinct assertion.One limit is worth stating: the buckets are hand-written literal maps with no canonical address list to compare against, so an entry added to or removed from one of them is only caught by the tests that check composition against the activated EIP1559 bucket. That pattern is inherited from upstream and is not changed here.
make allandmake quick-testpass.