Skip to content

[Storehouse] 007 Payloadless Checkpoint (v7) - #8578

Open
zhangchiqing wants to merge 13 commits into
leo/payloadless-execution-builderfrom
leo/payloadless-checkpoint-v7
Open

[Storehouse] 007 Payloadless Checkpoint (v7)#8578
zhangchiqing wants to merge 13 commits into
leo/payloadless-execution-builderfrom
leo/payloadless-checkpoint-v7

Conversation

@zhangchiqing

@zhangchiqing zhangchiqing commented Jun 9, 2026

Copy link
Copy Markdown
Member

The previous PR #8577 has integrated the payloadless feature to the execution node behind a feature flag, but the payloadless feature only works for empty state. To support non-empty state, which practically to support restart, we need to support a v7 payloadless checkpoint.

  • Add Version7 Checkpoint file format (Payloadless).
  • Compactor will still use V6 checkpointer. V7 support will be added in the future.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1abc5c01-e757-4d56-acc2-88233c5e340a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch leo/payloadless-checkpoint-v7

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

// Read the V6 checkpoint fully — the V6 reader already reads the 16 subtrie
// part files concurrently. The resulting tries share sub-tries via Go pointer
// identity, which lets FromV6Tries memoize and avoid redundant conversion.
v6Tries, err := LoadCheckpoint(v6Header, logger)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loads the entire checkpoint in memory, which requires a lot of memory. An improvement would be streaming the file, iterate each node, if it's a leaf node, then convert into payload less. This could save lots of memory. Skipped for now, because that requires additional code, can be implemented later.

return nil, nil
}
if index > totalSubTrieNodeCount {
nodePos := index - totalSubTrieNodeCount

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is same logic as v6 (getNodeByIndex) but slightly different in style, where v6 extracted it into getTopNodeByIndex, which adds little value. Instead, the logic here for getPayloadlessTopNodeByIndex is inlined.

@@ -0,0 +1,421 @@
package wal

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vimdiff ledger/complete/wal/checkpoint_v6_reader.go ledger/complete/wal/checkpoint_v7_reader.go

This file is similar to v6_reader, except some minimal coding style changes. Some read functions are not copied over but reused, such as readCheckpointHeader, allPartFileExist, readSubTriesFooter etc

@@ -0,0 +1,484 @@
package wal

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vimdiff ledger/complete/wal/checkpoint_v6_writer.go ledger/complete/wal/checkpoint_v7_writer.go

v7 is similar to v6 except some comments changes.

@@ -0,0 +1,372 @@
package payloadless

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mirrored from ledger/complete/mtrie/flattener/encoding.go and ledger/complete/mtrie/flattener/iterator.go

encNodeIndexSize = 8
encRegCountSize = 8

encLeafHashFlagSize = 1

@zhangchiqing zhangchiqing Jun 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encRegSizeSize and encPayloadLengthSize in v6 checkpoint node (see ledger/complete/mtrie/flattener/encoding.go) is replaced by encLeafHashFlagSize in payloadless

return nil, fmt.Errorf("cannot read leaf hash flag: %w", err)
}

flag := scratch[0]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is different from readPayloadFromReader


type EncodedTrie struct {
RootIndex uint64
RegCount uint64

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegSize is removed

Comment on lines +64 to +65
encLeafHashFlagSize +
encLeafHashSize

@zhangchiqing zhangchiqing Jun 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encPayloadLengthSize + encPayloadSize is replaced by encLeafHashFlagSize + encLeafHashSize in payloadless

copy(buf[pos:], path[:])
pos += encPathSize

// Encode leaf hash flag (1 byte) and optional leaf hash (0 or 32 bytes)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is different from full trie node's encodeLeafNode

@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from 7b14692 to 2625248 Compare June 9, 2026 21:05
}

lastCheckpointNum, err := c.checkpointer.LatestCheckpoint()
lastCheckpointNum, err := c.checkpointer.LatestCheckpointV6()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compactor will eventually need to support both V6 and V7, but this PR only supports V6. To avoid ambiguity, I renamed the function to LatestCheckpointV6.

@zhangchiqing
zhangchiqing marked this pull request as ready for review June 9, 2026 21:52
@zhangchiqing
zhangchiqing requested a review from a team as a code owner June 9, 2026 21:52
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-execution-builder branch from d7dea94 to fcc22e5 Compare June 18, 2026 23:18
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from c25cf48 to 267d209 Compare June 18, 2026 23:19
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-execution-builder branch from fcc22e5 to f138980 Compare June 19, 2026 00:00
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from 267d209 to 8d341b5 Compare June 19, 2026 00:00
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-execution-builder branch from f138980 to fc173be Compare July 2, 2026 18:01
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch 3 times, most recently from 1ea0be7 to ea43efc Compare July 2, 2026 18:23
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-execution-builder branch from fc173be to ff4a401 Compare July 13, 2026 17:24
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from ea43efc to a835d1f Compare July 13, 2026 17:24
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-execution-builder branch from ff4a401 to b825a6a Compare July 14, 2026 17:16
@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from a835d1f to 8b9576b Compare July 14, 2026 17:56
Comment thread cmd/execution_builder.go
Comment on lines +1497 to +1506
// HasRootCheckpointV7 guard keeps a re-entry after an interrupted
// bootstrap from hitting ConvertCheckpointV6ToV7's "output exists" check.
//
// TODO: ConvertCheckpointV6ToV7 reads the entire V6 forest into memory
// before emitting V7, a memory/time spike at first boot for mainnet-scale
// root checkpoints. A future optimization is to convert subtrie-by-subtrie
// without loading the whole forest.
if exeNode.exeConf.payloadless {
triedir := exeNode.exeConf.triedir
hasV7Root, err := wal.HasRootCheckpointV7(triedir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the node dies mid-conversion (realistic here: this is the memory-heavy step, so an OOM kill), the part files are left on disk but the header file is not, it is written last. On the next boot HasRootCheckpointV7 returns false (it only checks the header file), so this branch converts again, but ConvertCheckpointV6ToV7 refuses because the leftover part files trip its output-already-exists check. The node then cannot boot until someone deletes the partial files by hand (StoreCheckpointV7 only cleans up after itself on error returns, not on process death).

Suggestion: when hasV7Root is false, first delete any leftover partial output, e.g. deleteCheckpointFiles(triedir, modelbootstrap.FilenameWALRootCheckpoint+wal.V7FileSuffix). The V6 source is untouched, so retrying the conversion from scratch is always safe and the manual util keeps its strict no-clobber behavior.

hashes, err := wal.ReadTriesRootHash(logger, dir, fileName)
var hashes []ledger.RootHash
var err error
if strings.HasSuffix(fileName, wal.V7FileSuffix) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is currently unreachable through its only caller chain: GenerateProtocolSnapshotForCheckpointWithHeightsfindLatestCheckpointFilePath (line 141) picks last from wal.ListCheckpoints, which since this PR includes V7 numbers, but always renders wal.NumberToFilename(last), the V6 name. On a payloadless triedir whose newest checkpoint is V7-only, that produces a path to a nonexistent file, and it can never produce the .v7 path this dispatch handles. findLatestCheckpointFilePath should use ListCheckpointsWithInfo and render NumberToFilenameV7 when the latest checkpoint is V7.

Comment thread ledger/complete/wal/checkpointer.go Outdated
Comment on lines +926 to +930
// If both failed, return combined error
if v6Err != nil && v7Err != nil {
return fmt.Errorf("failed to remove checkpoint %d: v6 error: %w, v7 error: %v", checkpoint, v6Err, v7Err)
}
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteCheckpointFiles succeeds when its glob matches nothing, so the common failure case, V6 files exist but deletion fails, no V7 files present, returns nil here. (%v also drops the v7 error from the wrap chain.) Since both compactors now use the version-specific variants and no production caller of this method remains, consider errors.Join(v6Err, v7Err) (wrapped with the checkpoint number) or deprecating this method like LoadCheckpoint.

Comment on lines +393 to +410
switch flag {
case leafHashAbsent:
return nil, nil
case leafHashPresent:
if len(scratch) < encHashSize {
scratch = make([]byte, encHashSize)
}
_, err := io.ReadFull(reader, scratch[:encHashSize])
if err != nil {
return nil, fmt.Errorf("cannot read leaf hash: %w", err)
}
leafHash, err := hash.ToHash(scratch[:encHashSize])
if err != nil {
return nil, fmt.Errorf("failed to decode leaf hash: %w", err)
}
return &leafHash, nil
default:
return nil, fmt.Errorf("invalid leaf hash flag: %d", flag)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The leaf-hash flag is the one new on-disk encoding mechanism vs V6, and no test executes the leafHashAbsent path or the invalid-flag branch: there is no flattener_test.go, and every checkpoint test fixture uses non-empty values.

Comment thread ledger/complete/wal/checkpointer.go Outdated

// Version 7 includes these changes:
// - payloadless mode: leaf nodes store payload hashes (32 bytes) instead of full payloads
// - used for verification nodes that don't need actual payload values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: V7 checkpoints are read and written by payloadless execution nodes (this PR wires them into the EN builder); verification-node use is future/speculative.

Suggested change
// - used for verification nodes that don't need actual payload values
// - used by payloadless execution nodes, which read register values from the storehouse

Comment thread ledger/factory/factory.go
// For now config.LedgerServiceAddr is ignored.
//
// Expected error returns during normal operation:
// - error if config.Triedir is empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the missing-V7-checkpoint refusal (both the "V6 exists, run checkpoint-convert-v7" return and the plain "no V7 found" return) is equally expected during normal operation.

"github.com/onflow/flow-go/utils/unittest"
)

func TestVersionV7(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: three V6 unit tests have no V7 analog: TestEncodeSubTrie, TestGetNodesByIndex, and TestCannotStoreTwice.

Comment thread ledger/factory/factory.go Outdated
Msg("payloadless ledger: could not also list V6 checkpoints while reporting missing V7")
}
if latestV6 >= 0 {
logger.Warn().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Error level?

Comment thread ledger/factory/factory.go Outdated
v6Numbers, latestV6, v6ListErr := wal.ListV6Checkpoints(config.Triedir)
if v6ListErr != nil {
logger.Warn().Err(v6ListErr).
Str("triedir", config.Triedir).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All loggers contain Str("triedir", config.Triedir). you could create a new logger with that already included.

type PayloadlessLedgerWithCompactor struct {
*PayloadlessLedger
compactor *PayloadlessCompactor
logger zerolog.Logger

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*PayloadlessLedger already has a logger. Can we reuse that one?

@zhangchiqing
zhangchiqing force-pushed the leo/payloadless-checkpoint-v7 branch from 8b9576b to d77747c Compare July 31, 2026 02:44
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.

2 participants