Fix vETH proof root rejection: gate the unactivated fork 2/3 gas price rules - #72
Open
devdudeio wants to merge 1 commit into
Open
Fix vETH proof root rejection: gate the unactivated fork 2/3 gas price rules#72devdudeio wants to merge 1 commit into
devdudeio wants to merge 1 commit into
Conversation
…e rules The fork 2 and 3 gas price rules were future dated when written and their heights have long since passed, but notary operators stayed on an older build, so the network still produces the pre-fork gas price. A bridgekeeper that activates the rules rejects every proof root the network produces, and proposes a gas price no other notary agrees with, which stops notarizations. Gate the fork behind ETH_GAS_REDUCTION_FORK23_ACTIVE, defaulting to off, so both the producing and the validating side fall back to the pre-fork rule. Activation heights are left for maintainers to agree with operators. Move the gas price rules and proof root construction into a pure proofRoot.js used by both getProofRoot and checkProofRoot, which also fixes: - check2 was unreachable, so everything from H1 to H3 accepted any gas price - the two functions had drifted apart in the fork 2 band while sharing one cache - checkProofRoot built proof roots without power and cached them in the namespace shared with getProofRoot, so power could be lost on the way out - both indexed past the end of block.transactions for 0 and 1 transaction blocks Add the first tests in the repo, using node:test, with no new dependency.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Any bridgekeeper upgraded past
a0b0552rejects every vETH proof root the notary network produces, so notarizations stop. Notary operators are currently pinned to an older commit because of this.A proof root's
gaspriceis computed by the proposing notary's own bridgekeeper ingetProofRoot().verusdjust carries the value, andnotarizationSerializer.js:147serializes it into the notarization itself. In January 2026,cf6ecfbadded the fork bands*_GAS_REDUCTION_HEIGHT2/3andbda1c47/45a3425/8dfd406added a strict equality gas price check incheckProofRoot(). Both heights were future dated when written and have long since passed — Sepolia fork 3 is 10180885, the chain is at ~11.50M — but since operators stayed on the old build, the network still produces the pre-fork value. The fork never activated.An upgraded node is therefore isolated in both directions: it rejects everyone else's proof roots, and proposes a gas price no other notary agrees with.
Reproduced against live VRSCTEST, feeding the proof root the network actually notarized for Sepolia block 11502305 (taken from
getnotarizationdata) through the exportedgetBestProofRoot():mainbestindex: -1— rejectedbestindex: 0— accepted1.455696325.00000000— matches the networkThe rule was also checked against 40 real Sepolia blocks spanning ~25,000 blocks: 0 mismatches against what the network notarized.
What this changes
Treats fork 2/3 as not activated —
ETH_GAS_REDUCTION_FORK23_ACTIVE: false— which is what the network state actually shows. Both the producing and validating side fall back to the pre-fork rule, restoring parity with the network and witha0b0552. The fork rules stay in the tree, tested and ready.The activation heights are deliberately not picked here — that needs coordination across notary operators, so it's left to maintainers.
Four further defects in the same code, each confirmed:
check2was unreachable. The nestedcheck1/check2/check3cascade only evaluatedcheck2whencheck1was true, and H2 > H1, so everything from H1 to H3 fell through to an unconditional pass. At height 10100000 it accepts agaspriceof999.0.getProofRootandcheckProofRoothad drifted apart in the fork 2 band while sharing one block cache, so a node could reject the value it would have proposed itself.checkProofRootbuilt proof roots withoutpowerand cached them in the namespace shared withgetProofRoot, so a proof root returned to the daemon could silently lose itspowerfield. Reproduced against a live keeper.block.transactions[Math.ceil(len/2)], which is off the end of the array for 0 and 1 transaction blocks.The rules and proof root construction move into a new pure
proofRoot.jsused by both functions, so the proposed value can no longer drift from the accepted one.ethInteractor.jsloses 77 lines net.When fork 2/3 is eventually activated,
checkProofRootaccepts both the new and the pre-fork value, so the rollout can cross a mixed network instead of isolating whoever upgrades first — which is the failure this PR fixes.Tests
First tests in the repo.
node:test, no new dependency;npm testwas theexit 1stub.8 tests covering the regression (real Sepolia 11502143 data), band reachability, producer/validator agreement, the rule floors, degenerate blocks, and proof root shape. They were verified to bite: flipping the gate to
truefails the regression tests, reproducing currentmain.Not covered by unit tests:
getProofRoot/checkProofRootaren't exported, so the tests cover the extracted rules. The end-to-end result in the table above came from a harness driving the realgetBestProofRoot()against live Sepolia.