Repository navigation
Conversation
Glamsterdam's consensus fork (Gloas) moves the execution payload out of the beacon block. v0.30.2 is the first go-eth2-client release that can decode Gloas beacon blocks and fetch execution payload envelopes; with v0.27.1 the oracle stops at the first Gloas slot because the block can't be decoded. v0.30 requires Go 1.25, so the Dockerfile and CI move to it as well. go-bitfield moved from prysmaticlabs to OffchainLabs upstream, which only affects one test import. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vanilla reward was calculated as every fee paid minus the block gas_used times the base fee. That relies on the block gas_used being the sum of the gas charged to each tx, which Glamsterdam breaks: EIP-7778 stops subtracting refunds from it, and EIP-8037 makes it the largest of two gas dimensions. The pool would have been credited less than it received, and the onchain reconciliation would refuse to freeze the checkpoint. Sum each tx's priority fee instead, using the gas its receipt charged. Until Glamsterdam both calculations are equal, which the new tests check for every supported tx type, so no published reward changes and the change needs no activation slot. It also fixes a tx/receipt hash mismatch silently returning a nil reward with a nil error, as the error was wrapped from a nil one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Gloas, Glamsterdam's consensus fork, the beacon block no longer carries the execution payload (EIP-7732). It carries a builder's bid, which commits to the payload by its hash, and the builder publishes the payload separately. A slot can now be full, empty (beacon block but no execution payload) or missed. - GloasForkSlot holds the fork slot per chain, only Hoodi for now. The oracle stops at a beacon block whose version doesn't match it, so a chain must be listed before it forks and history is never processed with the wrong rules. - A slot is full when the next beacon block builds on its payload. Its payload is then fetched from the beacon node's envelope endpoint and checked against the bid. Empty slots have no execution block, so no events, txs or receipts are fetched for them. - The FullBlock getters read the payload of Gloas slots, and the code reading the execution block (donations, forced payments, the latest processed block) now also skips empty slots, not only missed ones. - The outcome of a Gloas slot is only settled after it is finalized: its payload's inclusion is decided by the next beacon block, and a withheld payload's payment at the end of the next epoch. The main loop waits until that epoch is finalized, trailing finality by ~1 epoch. Classifying Gloas proposals needs the builder payment, which the next commits add. Until then the oracle stops at the first Gloas proposal rather than classifying it with pre-Gloas rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Gloas, a builder's bid promises a payment to the proposer's fee recipient, which the protocol takes from the builder's deposit. It is paid when the next beacon block builds on the bid's execution payload. When the payload is withheld, the protocol still pays it if the beacon block got 60% of its slot's stake in same-slot votes, deciding it in the epoch transition after the next epoch. Slashing the proposer cancels a payment not decided yet. The oracle reproduces that decision instead of trusting it: it reads the builder pending payments and the total active balance from the beacon state right before the transition, at the last slot of the next epoch, and applies the spec's quorum. Effective balances only change after the payments are decided, so the quorum is the one the protocol used. The active set is heavy to fetch, so this only happens for empty slots whose bid paid the pool. Only payments to the pool are recorded, in FullBlock.BuilderPayment. Nothing uses them yet; the next commits classify proposals and track the payments until they reach the pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Gloas proposal is judged on what the proposer chose, the bid and who it pays, and never on what a builder failed to do: - Self-built payload: judged as before Gloas, on the payload's fee recipient. If its payload was not included, the slot counts as missed. - Bid paying the pool: a valid proposal, rewarded with whatever reached the pool: the bid value when the protocol paid it, plus anything the payload itself paid (an execution_payment tx, or the tips when the builder made the pool its fee recipient). An unpaid execution_payment is the builder's fault, so it no longer gets the validator banned. - Bid paying the pool but withheld payload and dropped payment: missed, as the beacon block was too late to collect its votes. - Bid paying someone else: wrong fee recipient, as before. If the payload still paid the pool, that ETH is credited rather than left unallocated. Missed and wrong fee keep their existing consequences, so no new block types or state machine events are introduced and the API is unchanged. The part of a reward paid from the builder's deposit is stored in the new SummarizedBlock.BuilderPaymentWei, since it arrives with no event. The offchain reconciliation counts it as an asset. The field is omitted when nil, so states saved before this change keep their hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A builder payment is credited when its slot is processed, but reaches the pool later, as a withdrawal in another execution payload: usually the next one, about an epoch later when the payload was withheld. Until then the oracle owes ETH the pool doesn't hold yet, and the exact onchain reconciliation run before every checkpoint would fail. - Credited builder payments are kept in the state as in flight. They are removed when a builder withdrawal to the pool with the same builder and amount is processed. A builder withdrawal to the pool that matches no credited payment rejects the slot, as that ETH would otherwise be left unallocated. - The onchain reconciliation counts the payments in flight as part of what the pool holds. It stays exact, with no tolerance. - The forced payment detector reads the pool balance across a block, so it now explains builder withdrawals to the pool instead of rejecting them. Any other withdrawal to the pool is still rejected. The state only keeps the payments in flight, so it doesn't grow. The new field is omitted when empty, and a test checks states saved before Gloas still load with their hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Gloas fork on, the oracle processes a slot only once the epoch after it is finalized, so in sync it trails the finalized slot by 33 slots. The API still measured sync against the finalized slot: - /status would report is_oracle_in_sync false forever, since it required the processed slot to equal the finalized one. - OracleReady(64), which gates the proofs and validators endpoints, allows 64 slots behind finality. Right after each epoch is finalized the oracle is 65 behind until it processes it, so those endpoints would refuse requests briefly every epoch. LatestSettledSlot returns the latest slot the oracle can process given the finalized one. The main loop, /status and OracleReady now all use it, so they can't disagree, and a test checks it against the per-slot settlement rule by brute force. oracle_sync_distance_slots and the time to the next checkpoint are counted from it too. Before the fork it is the finalized slot, so nothing changes until then. The distance_from_finalized_slot metric keeps its meaning, so from the fork on it will read about 33 when the oracle is in sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bid whose payload the next beacon block built on was assumed paid. The next block does settle it before anything else, but slashing the proposer cancels a pending payment, and a slashing of the proposer included in its own beacon block happens before that. It is not as unlikely as it sounds: a validator run on two machines gets slashed with evidence that its own node may then include. Had it happened, the oracle would have credited ETH that never comes, and the onchain reconciliation would have counted it in flight forever, hiding that the pool lacks it rather than stopping. So a full slot's payment is now checked like a withheld one's, against the beacon state: it is paid if its builder_pending_payments entry survived its own beacon block. The exception, also from the spec, is a next beacon block two or more epochs later, which appends the payment directly whatever that entry says. Only read for bids paying the pool, and the endpoint returns 64 entries. A cancelled payment is not credited nor tracked. The proposer still chose a bid paying the pool, so it is not banned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A builder withdrawal of zero to the pool moves no ETH, so it is not matched against credited payments. Before, it would have stopped the oracle as an unknown payment. The forced payment check already skips zero amounts. Builder payments reach the pool within about two epochs. One credited but still in flight after four is now logged once per epoch: it may never arrive, and until it does it counts as a pool asset in the onchain reconciliation. It only warns. Slots are processed in order, so stopping would also stop the oracle from reaching the slot where a slow payment lands, and with whether payments are made now decided exactly, one missing would point at a consensus client bug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Makes the oracle Glamsterdam-ready. Glamsterdam activates on Hoodi at slot 4,235,264 (Oct 26 2026, 17:42 UTC). Without this PR, the oracle stops at that slot because it can't decode Gloas beacon blocks. The mainnet fork date isn't set yet.
Why so much changes
Gloas, Glamsterdam's consensus fork, takes the execution payload out of the beacon block (EIP-7732, ePBS):
valuetofee_recipient. The builder publishes the payload separately, a few seconds later.valuefrom the builder's deposit as a withdrawal in a later execution payload. There is no transaction and no event.execution_payment), which nobody enforces.Separately, EIP-7778 and EIP-8037 make a payload's
gas_useddiffer from the sum of its receipts, which broke the vanilla reward formula.Review commit by commit
Each commit builds, vets, and passes all tests on its own; this was checked commit by commit.
These three were added after a full self-review of 1–6:
/statusandOracleReady, which commit 3 would have broken (details below).How proposals are judged
Ban for what the proposer chose, never for what a builder failed to do.
value(unless the proposer's slashing cancelled it) + anything the payload sent to the poolvalue, which the protocol still paysHow rewards are shared doesn't change: still proportional to effective balance, with the same pool fee.
Behaviour changes to know about
is_oracle_in_sync,oracle_sync_distance_slots, the next-checkpoint countdown andOracleReadynow count from the latest settled slot rather than the finalized one. Before the fork they're identical.distance_from_finalized_slotmetric keeps its meaning, so it will read ~33 when in sync after the fork.builder_pending_payments(64 entries).RefreshBeaconValidatorsalready does every 1,200 slots. From mainnet's last 90 days (~15 pool proposals a day), and assuming an empty-slot rate like today's missed rate, that's roughly once a week.GloasForkSlotbefore its fork;404error text, which is unchanged;Testing
go test ./...andgo test -race ./...pass, with 113 new test cases (counting subtests). They cover:AdvanceStateToNextSlot, with both reconciliations;staticcheckreports nothing in code this PR adds or changes.Not done yet, needed before release:
🤖 Generated with Claude Code