Skip to content

Fix vETH proof root rejection: gate the unactivated fork 2/3 gas price rules - #72

Open
devdudeio wants to merge 1 commit into
VerusCoin:mainfrom
devdudeio:fix/proofroot-gasprice-compat
Open

Fix vETH proof root rejection: gate the unactivated fork 2/3 gas price rules#72
devdudeio wants to merge 1 commit into
VerusCoin:mainfrom
devdudeio:fix/proofroot-gasprice-compat

Conversation

@devdudeio

Copy link
Copy Markdown

The problem

Any bridgekeeper upgraded past a0b0552 rejects 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 gasprice is computed by the proposing notary's own bridgekeeper in getProofRoot(). verusd just carries the value, and notarizationSerializer.js:147 serializes it into the notarization itself. In January 2026, cf6ecfb added the fork bands *_GAS_REDUCTION_HEIGHT2/3 and bda1c47/45a3425/8dfd406 added a strict equality gas price check in checkProofRoot(). 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 exported getBestProofRoot():

main this PR
the network's real proof root bestindex: -1 — rejected bestindex: 0 — accepted
gas price proposed 1.45569632 5.00000000 — matches the network

The 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 with a0b0552. 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:

  • check2 was unreachable. The nested check1/check2/check3 cascade only evaluated check2 when check1 was true, and H2 > H1, so everything from H1 to H3 fell through to an unconditional pass. At height 10100000 it accepts a gasprice of 999.0.
  • getProofRoot and checkProofRoot had drifted apart in the fork 2 band while sharing one block cache, so a node could reject the value it would have proposed itself.
  • checkProofRoot built proof roots without power and cached them in the namespace shared with getProofRoot, so a proof root returned to the daemon could silently lose its power field. Reproduced against a live keeper.
  • Both indexed 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.js used by both functions, so the proposed value can no longer drift from the accepted one. ethInteractor.js loses 77 lines net.

When fork 2/3 is eventually activated, checkProofRoot accepts 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 test was the exit 1 stub.

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 true fails the regression tests, reproducing current main.

Not covered by unit tests: getProofRoot/checkProofRoot aren't exported, so the tests cover the extracted rules. The end-to-end result in the table above came from a harness driving the real getBestProofRoot() against live Sepolia.

…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.
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.

1 participant