Skip to content

Commit 5c97886

Browse files
committed
persist envelop and recover full state by it
1 parent ec9aa2c commit 5c97886

File tree

9 files changed

+245
-34
lines changed

9 files changed

+245
-34
lines changed

cl/phase1/forkchoice/fork_graph/fork_graph_disk.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,109 @@ func (f *forkGraphDisk) hasBeaconState(blockRoot common.Hash) bool {
495495
return err == nil && exists
496496
}
497497

498+
// GetExecutionPayloadState reconstructs the post-execution-payload state by replaying.
499+
// Similar to getState() but handles GLOAS FULL/EMPTY paths.
500+
// [New in Gloas:EIP7732]
501+
func (f *forkGraphDisk) GetExecutionPayloadState(blockRoot common.Hash) (*state.CachingBeaconState, error) {
502+
// 1. Read target envelope
503+
targetEnvelope, err := f.ReadEnvelopeFromDisk(blockRoot)
504+
if err != nil {
505+
return nil, fmt.Errorf("GetExecutionPayloadState: failed to read envelope: %w", err)
506+
}
507+
508+
// 2. Get block_state for the target block (handles FULL/EMPTY paths)
509+
blockState, err := f.getStateWithEnvelopes(blockRoot)
510+
if err != nil {
511+
return nil, fmt.Errorf("GetExecutionPayloadState: failed to get block state: %w", err)
512+
}
513+
514+
// 3. Apply target envelope to get exec_payload_state
515+
if err := transition.DefaultMachine.ProcessExecutionPayloadEnvelope(blockState, targetEnvelope); err != nil {
516+
return nil, fmt.Errorf("GetExecutionPayloadState: failed to process envelope: %w", err)
517+
}
518+
519+
return blockState, nil
520+
}
521+
522+
// getStateWithEnvelopes is like getState but handles GLOAS FULL/EMPTY parent paths.
523+
// When replaying blocks, if a block was built on parent's FULL path, applies parent's envelope first.
524+
// [New in Gloas:EIP7732]
525+
func (f *forkGraphDisk) getStateWithEnvelopes(blockRoot common.Hash) (*state.CachingBeaconState, error) {
526+
// Collect blocks with their parent path info
527+
type blockInfo struct {
528+
block *cltypes.SignedBeaconBlock
529+
parentWasFull bool
530+
parentEnvelope *cltypes.SignedExecutionPayloadEnvelope
531+
}
532+
blocksInTheWay := []blockInfo{}
533+
currentRoot := blockRoot
534+
var checkpointState *state.CachingBeaconState
535+
536+
// Walk back to find checkpoint
537+
for checkpointState == nil {
538+
block, ok := f.GetBlock(currentRoot)
539+
if !ok {
540+
// Check if it's a header-only checkpoint
541+
bHeader, headerOk := f.GetHeader(currentRoot)
542+
if headerOk && bHeader.Slot%dumpSlotFrequency == 0 {
543+
checkpointState, _ = f.readBeaconStateFromDisk(currentRoot)
544+
}
545+
if checkpointState != nil {
546+
break
547+
}
548+
return nil, fmt.Errorf("getStateWithEnvelopes: block not found: %x", currentRoot)
549+
}
550+
551+
// Try to read checkpoint state
552+
if block.Block.Slot%dumpSlotFrequency == 0 {
553+
checkpointState, _ = f.readBeaconStateFromDisk(currentRoot)
554+
if checkpointState != nil {
555+
break
556+
}
557+
}
558+
559+
// Check if this block was built on parent's FULL path
560+
parentRoot := block.Block.ParentRoot
561+
parentEnvelope, _ := f.ReadEnvelopeFromDisk(parentRoot)
562+
parentWasFull := false
563+
if parentEnvelope != nil && block.Block.Body.SignedExecutionPayloadBid != nil &&
564+
block.Block.Body.SignedExecutionPayloadBid.Message != nil {
565+
// Compare block's bid.ParentBlockHash with parent envelope's payload.BlockHash
566+
bidHash := block.Block.Body.SignedExecutionPayloadBid.Message.ParentBlockHash
567+
envHash := parentEnvelope.Message.Payload.BlockHash
568+
parentWasFull = (bidHash == envHash)
569+
}
570+
571+
blocksInTheWay = append(blocksInTheWay, blockInfo{
572+
block: block,
573+
parentWasFull: parentWasFull,
574+
parentEnvelope: parentEnvelope,
575+
})
576+
currentRoot = parentRoot
577+
}
578+
579+
// Replay forward from checkpoint
580+
// Note: checkpointState is always a block_state (from DumpBeaconStateOnDisk)
581+
replayState := checkpointState
582+
for i := len(blocksInTheWay) - 1; i >= 0; i-- {
583+
bi := blocksInTheWay[i]
584+
585+
// If this block was built on parent's FULL path, apply parent envelope first
586+
if bi.parentWasFull && bi.parentEnvelope != nil {
587+
if err := transition.DefaultMachine.ProcessExecutionPayloadEnvelope(replayState, bi.parentEnvelope); err != nil {
588+
return nil, fmt.Errorf("getStateWithEnvelopes: failed to process parent envelope: %w", err)
589+
}
590+
}
591+
592+
// Apply process_block to get this block's block_state
593+
if err := transition.TransitionState(replayState, bi.block, nil, false); err != nil {
594+
return nil, fmt.Errorf("getStateWithEnvelopes: failed to transition state: %w", err)
595+
}
596+
}
597+
598+
return replayState, nil
599+
}
600+
498601
func (f *forkGraphDisk) Prune(pruneSlot uint64) (err error) {
499602
oldRoots := make([]common.Hash, 0, f.beaconCfg.SlotsPerEpoch)
500603
highestStoredBeaconStateSlot := uint64(0)
@@ -529,6 +632,8 @@ func (f *forkGraphDisk) Prune(pruneSlot uint64) (err error) {
529632
f.headers.Delete(root)
530633
f.blockRewards.Delete(root)
531634
f.fs.Remove(getBeaconStateFilename(root))
635+
// [New in Gloas:EIP7732] Also remove envelope files
636+
f.fs.Remove(getEnvelopeFilename(root))
532637
}
533638
log.Debug("Pruned old blocks", "pruneSlot", pruneSlot)
534639
return

cl/phase1/forkchoice/fork_graph/fork_graph_disk_fs.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"github.com/golang/snappy"
2626
"github.com/spf13/afero"
2727

28+
"github.com/erigontech/erigon/cl/clparams"
29+
"github.com/erigontech/erigon/cl/cltypes"
2830
"github.com/erigontech/erigon/cl/phase1/core/state"
2931
"github.com/erigontech/erigon/common"
3032
"github.com/erigontech/erigon/common/log/v3"
@@ -34,6 +36,12 @@ func getBeaconStateFilename(blockRoot common.Hash) string {
3436
return fmt.Sprintf("%x.snappy_ssz", blockRoot)
3537
}
3638

39+
// getEnvelopeFilename returns the filename for execution payload envelopes.
40+
// [New in Gloas:EIP7732]
41+
func getEnvelopeFilename(blockRoot common.Hash) string {
42+
return fmt.Sprintf("%x.envelope.snappy_ssz", blockRoot)
43+
}
44+
3745
func (f *forkGraphDisk) readBeaconStateFromDisk(blockRoot common.Hash) (bs *state.CachingBeaconState, err error) {
3846
var file afero.File
3947
f.stateDumpLock.Lock()
@@ -136,3 +144,104 @@ func (f *forkGraphDisk) DumpBeaconStateOnDisk(blockRoot common.Hash, bs *state.C
136144

137145
return
138146
}
147+
148+
// HasEnvelope checks if an envelope exists on disk for the given block root.
149+
// [New in Gloas:EIP7732]
150+
func (f *forkGraphDisk) HasEnvelope(blockRoot common.Hash) bool {
151+
exists, err := afero.Exists(f.fs, getEnvelopeFilename(blockRoot))
152+
return err == nil && exists
153+
}
154+
155+
// ReadEnvelopeFromDisk reads an execution payload envelope from disk.
156+
// [New in Gloas:EIP7732]
157+
func (f *forkGraphDisk) ReadEnvelopeFromDisk(blockRoot common.Hash) (envelope *cltypes.SignedExecutionPayloadEnvelope, err error) {
158+
var file afero.File
159+
f.stateDumpLock.Lock()
160+
defer f.stateDumpLock.Unlock()
161+
162+
file, err = f.fs.Open(getEnvelopeFilename(blockRoot))
163+
if err != nil {
164+
return
165+
}
166+
defer file.Close()
167+
168+
if f.sszSnappyReader == nil {
169+
f.sszSnappyReader = snappy.NewReader(file)
170+
} else {
171+
f.sszSnappyReader.Reset(file)
172+
}
173+
174+
// Read the length
175+
lengthBytes := make([]byte, 8)
176+
var n int
177+
n, err = io.ReadFull(f.sszSnappyReader, lengthBytes)
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to read length: %w, root: %x", err, blockRoot)
180+
}
181+
if n != 8 {
182+
return nil, fmt.Errorf("failed to read length: %d, want 8, root: %x", n, blockRoot)
183+
}
184+
185+
f.sszBuffer = f.sszBuffer[:binary.BigEndian.Uint64(lengthBytes)]
186+
n, err = io.ReadFull(f.sszSnappyReader, f.sszBuffer)
187+
if err != nil {
188+
return nil, fmt.Errorf("failed to read snappy buffer: %w, root: %x", err, blockRoot)
189+
}
190+
f.sszBuffer = f.sszBuffer[:n]
191+
192+
envelope = &cltypes.SignedExecutionPayloadEnvelope{}
193+
if err = envelope.DecodeSSZ(f.sszBuffer, int(clparams.GloasVersion)); err != nil {
194+
return nil, fmt.Errorf("failed to decode envelope: %w, root: %x, len: %d", err, blockRoot, n)
195+
}
196+
197+
return
198+
}
199+
200+
// DumpEnvelopeOnDisk dumps an execution payload envelope to disk.
201+
// [New in Gloas:EIP7732]
202+
func (f *forkGraphDisk) DumpEnvelopeOnDisk(blockRoot common.Hash, envelope *cltypes.SignedExecutionPayloadEnvelope) (err error) {
203+
f.stateDumpLock.Lock()
204+
defer f.stateDumpLock.Unlock()
205+
206+
// Encode the envelope
207+
f.sszBuffer, err = envelope.EncodeSSZ(f.sszBuffer[:0])
208+
if err != nil {
209+
return
210+
}
211+
212+
dumpedFile, err := f.fs.OpenFile(getEnvelopeFilename(blockRoot), os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o755)
213+
if err != nil {
214+
return err
215+
}
216+
defer dumpedFile.Close()
217+
218+
if f.sszSnappyWriter == nil {
219+
f.sszSnappyWriter = snappy.NewBufferedWriter(dumpedFile)
220+
} else {
221+
f.sszSnappyWriter.Reset(dumpedFile)
222+
}
223+
224+
// Write the length
225+
length := make([]byte, 8)
226+
binary.BigEndian.PutUint64(length, uint64(len(f.sszBuffer)))
227+
if _, err := f.sszSnappyWriter.Write(length); err != nil {
228+
log.Error("failed to write length", "err", err)
229+
return err
230+
}
231+
// Write the envelope
232+
if _, err := f.sszSnappyWriter.Write(f.sszBuffer); err != nil {
233+
log.Error("failed to write ssz buffer", "err", err)
234+
return err
235+
}
236+
if err = f.sszSnappyWriter.Flush(); err != nil {
237+
log.Error("failed to flush snappy writer", "err", err)
238+
return err
239+
}
240+
241+
if err = dumpedFile.Sync(); err != nil {
242+
log.Error("failed to sync dumped file", "err", err)
243+
return
244+
}
245+
246+
return
247+
}

cl/phase1/forkchoice/fork_graph/interface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,10 @@ type ForkGraph interface {
5858
GetCurrentParticipationIndicies(epoch uint64) (*solid.ParticipationBitList, error)
5959
GetPreviousParticipationIndicies(epoch uint64) (*solid.ParticipationBitList, error)
6060
DumpBeaconStateOnDisk(blockRoot common.Hash, state *state.CachingBeaconState, forced bool) error
61+
// [New in Gloas:EIP7732] Execution payload envelope persistence and state reconstruction
62+
DumpEnvelopeOnDisk(blockRoot common.Hash, envelope *cltypes.SignedExecutionPayloadEnvelope) error
63+
ReadEnvelopeFromDisk(blockRoot common.Hash) (*cltypes.SignedExecutionPayloadEnvelope, error)
64+
HasEnvelope(blockRoot common.Hash) bool
65+
// GetExecutionPayloadState reconstructs the post-execution-payload state by replaying the envelope.
66+
GetExecutionPayloadState(blockRoot common.Hash) (*state.CachingBeaconState, error)
6167
}

cl/phase1/forkchoice/forkchoice.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package forkchoice
1818

1919
import (
20-
"fmt"
2120
"slices"
2221
"sort"
2322
"sync"
@@ -148,10 +147,6 @@ type ForkChoiceStore struct {
148147
optimisticStore optimistic.OptimisticStore
149148
probabilisticHeadGetter bool
150149

151-
// [New in Gloas:EIP7732] Stores post-execution-payload states keyed by beacon block root.
152-
// In GLOAS, beacon block and execution payload have separate state transitions.
153-
// This stores the state after ProcessExecutionPayloadEnvelope is applied.
154-
executionPayloadStates sync.Map // map[common.Hash]*state.CachingBeaconState
155150
// [New in Gloas:EIP7732]
156151
ptcVote sync.Map // map[common.Hash][clparams.PtcSize]bool
157152
// [New in Gloas:EIP7732] Indexed weight store for optimized weight calculation
@@ -302,12 +297,6 @@ func NewForkChoiceStore(
302297
f.highestSeen.Store(anchorState.Slot())
303298
f.time.Store(anchorState.GenesisTime() + anchorState.BeaconConfig().SecondsPerSlot*anchorState.Slot())
304299

305-
// [New in Gloas:EIP7732] Initialize anchor root entries with anchor state copy
306-
anchorStateCopy, err := anchorState.Copy()
307-
if err != nil {
308-
return nil, fmt.Errorf("failed to copy anchor state for execution payload states: %w", err)
309-
}
310-
f.executionPayloadStates.Store(anchorRoot, anchorStateCopy)
311300
f.ptcVote.Store(anchorRoot, [clparams.PtcSize]bool{})
312301

313302
// [New in Gloas:EIP7732] Initialize indexed weight store

cl/phase1/forkchoice/get_head.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (f *ForkChoiceStore) GetHead(auxilliaryState *state.CachingBeaconState) (co
119119
if f.beaconCfg.GetCurrentStateVersion(currentEpoch) >= clparams.GloasVersion {
120120
return f.getHeadGloas()
121121
}
122-
return f.getHeadPreGloas(auxilliaryState)
122+
return f.getHead(auxilliaryState)
123123
}
124124

125125
// getHeadGloas returns the head using GLOAS fork choice rules.
@@ -192,8 +192,8 @@ func (f *ForkChoiceStore) getHeadGloas() (common.Hash, uint64, error) {
192192
}
193193
}
194194

195-
// getHeadPreGloas returns the head using pre-GLOAS fork choice rules.
196-
func (f *ForkChoiceStore) getHeadPreGloas(auxilliaryState *state.CachingBeaconState) (common.Hash, uint64, error) {
195+
// getHead returns the head using pre-GLOAS fork choice rules.
196+
func (f *ForkChoiceStore) getHead(auxilliaryState *state.CachingBeaconState) (common.Hash, uint64, error) {
197197
justifiedCheckpoint := f.justifiedCheckpoint.Load().(solid.Checkpoint)
198198
var justificationState *checkpointState
199199
var err error

cl/phase1/forkchoice/on_block.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,15 @@ func (f *ForkChoiceStore) OnBlock(ctx context.Context, block *cltypes.SignedBeac
217217
// This ensures the new block builds on the correct canonical state.
218218
var parentFullState *state.CachingBeaconState
219219
if isGloas && f.isParentNodeFull(block.Block) {
220-
if s, ok := f.executionPayloadStates.Load(block.Block.ParentRoot); ok {
221-
parentFullState = s.(*state.CachingBeaconState)
220+
// Check disk for envelope existence (handles both normal operation and restart recovery)
221+
if f.forkGraph.HasEnvelope(block.Block.ParentRoot) {
222+
// Reconstruct the execution payload state from disk
223+
parentFullState, err = f.forkGraph.GetExecutionPayloadState(block.Block.ParentRoot)
224+
if err != nil {
225+
log.Warn("Failed to get execution payload state for parent", "parentRoot", block.Block.ParentRoot, "err", err)
226+
// Fall back to block_state via AddChainSegment's normal path
227+
parentFullState = nil
228+
}
222229
}
223230
}
224231

cl/phase1/forkchoice/on_execution_payload.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ func (f *ForkChoiceStore) OnExecutionPayload(ctx context.Context, signedEnvelope
6262
if err := transition.DefaultMachine.ProcessExecutionPayloadEnvelope(blockStateCopy, signedEnvelope); err != nil {
6363
return fmt.Errorf("OnExecutionPayload: failed to process execution payload: %w", err)
6464
}
65-
66-
// Store the post-execution-payload state.
67-
// In GLOAS, beacon block and execution payload have separate state transitions.
68-
// When the head has PayloadStatus=FULL, this state becomes the canonical state.
69-
// TODO: Persistently store the execution payload states
70-
f.executionPayloadStates.Store(beaconBlockRoot, blockStateCopy)
65+
// Persist envelope to disk for recovery after restart.
66+
// The full state can be reconstructed via GetExecutionPayloadState() which replays the envelope.
67+
// HasEnvelope() checks disk for existence, replacing in-memory tracking.
68+
if err := f.forkGraph.DumpEnvelopeOnDisk(beaconBlockRoot, signedEnvelope); err != nil {
69+
return fmt.Errorf("OnExecutionPayload: failed to dump envelope: %w", err)
70+
}
7171

7272
return nil
7373
}

cl/phase1/forkchoice/payload_vote.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (f *ForkChoiceStore) isPayloadTimely(root common.Hash) bool {
6262

6363
// If the payload is not locally available, the payload
6464
// is not considered available regardless of the PTC vote
65-
if _, ok := f.executionPayloadStates.Load(root); !ok {
65+
if !f.forkGraph.HasEnvelope(root) {
6666
return false
6767
}
6868

@@ -224,7 +224,8 @@ func (f *ForkChoiceStore) getNodeChildren(node ForkChoiceNode, blocks map[common
224224
children := []ForkChoiceNode{
225225
{Root: node.Root, PayloadStatus: cltypes.PayloadStatusEmpty},
226226
}
227-
if _, ok := f.executionPayloadStates.Load(node.Root); ok {
227+
// Check disk for envelope existence to determine if FULL status is available
228+
if f.forkGraph.HasEnvelope(node.Root) {
228229
children = append(children, ForkChoiceNode{
229230
Root: node.Root, PayloadStatus: cltypes.PayloadStatusFull,
230231
})
@@ -259,8 +260,8 @@ func (f *ForkChoiceStore) getNodeChildren(node ForkChoiceNode, blocks map[common
259260
// [New in Gloas:EIP7732]
260261
func (f *ForkChoiceStore) validateParentPayloadPath(block *cltypes.BeaconBlock) error {
261262
if f.isParentNodeFull(block) {
262-
// Parent is FULL - verify execution payload state exists
263-
if _, ok := f.executionPayloadStates.Load(block.ParentRoot); !ok {
263+
// Parent is FULL - verify execution payload envelope exists on disk
264+
if !f.forkGraph.HasEnvelope(block.ParentRoot) {
264265
return errors.New("parent execution payload state not found for FULL parent")
265266
}
266267
} else {

cl/phase1/forkchoice/utils.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ func (f *ForkChoiceStore) onNewFinalized(newFinalized solid.Checkpoint) {
7878
return true
7979
})
8080

81-
// [New in Gloas:EIP7732] Clean up ptcVote and executionPayloadStates for finalized blocks
81+
// [New in Gloas:EIP7732] Clean up ptcVote for finalized blocks
82+
// Note: envelope files are cleaned up in forkGraph.Prune()
8283
finalizedSlot := newFinalized.Epoch * f.beaconCfg.SlotsPerEpoch
8384
f.ptcVote.Range(func(k, v any) bool {
8485
root := k.(common.Hash)
@@ -87,13 +88,6 @@ func (f *ForkChoiceStore) onNewFinalized(newFinalized solid.Checkpoint) {
8788
}
8889
return true
8990
})
90-
f.executionPayloadStates.Range(func(k, v any) bool {
91-
root := k.(common.Hash)
92-
if header, has := f.forkGraph.GetHeader(root); !has || header.Slot <= finalizedSlot {
93-
f.executionPayloadStates.Delete(k)
94-
}
95-
return true
96-
})
9791

9892
slotToPrune := ((newFinalized.Epoch - 3) * f.beaconCfg.SlotsPerEpoch) - 1
9993
f.forkGraph.Prune(slotToPrune)

0 commit comments

Comments
 (0)