Skip to content

add block number to ReadBlockByHash#3703

Open
cody-littley wants to merge 2 commits into
mainfrom
cjl/store-block-number
Open

add block number to ReadBlockByHash#3703
cody-littley wants to merge 2 commits into
mainfrom
cjl/store-block-number

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Add an additional return value to ReadBlockByHash() that provides the block's number.

Testing performed to validate your change

unit tests

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Breaking BlockDB API and a new littblock on-disk block/QC value layout (version-prefixed) can break existing persisted stores and any callers not updated to the new signature.

Overview
ReadBlockByHash now returns the block’s GlobalBlockNumber alongside the optional block, so hash-only lookups no longer need a separate number read. The BlockDB contract documents that the number is undefined on a miss.

littblock persists blocks as [version][8-byte number][proto] (and adds a version byte to QC values) so hash-alias reads can recover the number from the shared value. memblock mirrors that with a hashEntry mapping. Shared contract tests assert the returned number matches the written height.

Reviewed by Cursor Bugbot for commit 175dccb. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 7, 2026, 3:46 PM

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.97872% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.39%. Comparing base (ccfbb21) to head (175dccb).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/ledger_db/block/littblock/codec.go 80.00% 3 Missing and 3 partials ⚠️
sei-db/ledger_db/block/littblock/litt_block_db.go 80.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3703      +/-   ##
==========================================
- Coverage   59.30%   58.39%   -0.91%     
==========================================
  Files        2273     2186      -87     
  Lines      188364   178775    -9589     
==========================================
- Hits       111705   104399    -7306     
+ Misses      66596    65112    -1484     
+ Partials    10063     9264     -799     
Flag Coverage Δ
sei-chain-pr 87.72% <82.97%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...b/ledger_db/block/littblock/litt_block_iterator.go 64.44% <100.00%> (ø)
sei-db/ledger_db/block/memblock/mem_block_db.go 98.03% <100.00%> (ø)
sei-db/ledger_db/block/littblock/litt_block_db.go 69.56% <80.00%> (+0.19%) ⬆️
sei-db/ledger_db/block/littblock/codec.go 87.50% <80.00%> (-12.50%) ⬇️

... and 90 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clean, well-tested change that adds a GlobalBlockNumber return value to ReadBlockByHash by embedding the number in a newly version-framed on-disk value; the signature change is propagated to both implementations and all (test-only) callers. No blockers, but a doc typo and a documented backward-incompatibility with pre-existing DBs are worth noting.

Findings: 0 blocking | 3 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Backward compatibility: the new version-prefixed decoders reject any pre-existing unversioned LittDB values, and recoverCursors calls decodeQC at startup (litt_block_db.go:119), so reopening a DB written by the previous code would fail to recover. The author documents this as intentional ("this store is not yet in production, so no prior format is supported") in littblock/codec.go, so this is an accepted design decision rather than a blocker — but if any dev/test stores exist they must be wiped on upgrade. (Codex flagged this as a blocker; downgraded here given the explicit documented non-production status.)
  • Cursor's second-opinion review produced no output (empty file).
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

// for the block that was passed to WriteBlock.
//
// This method returns the block number for the block if it is found.
// not found, the block number returned is undefined.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Doc typo: this sentence is missing its opening word. It should read "If not found, the block number returned is undefined." (currently "not found, the block number returned is undefined.").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — straightforward API extension with proper versioned framing and thorough tests; the two nits are inline.

Extended reasoning...

Overview

This PR extends BlockDB.ReadBlockByHash to also return the block's GlobalBlockNumber. On the durable littblock side, block values are reframed as [version:1][number:8][proto] so a hash-alias read (which only carries the hash key) can recover the number; QC values also get a 1-byte version prefix. The in-memory memblock mirrors this via a new hashEntry struct. Interface docs, callers, and tests are updated in lock-step.

Security risks

None material. This touches only the block DB serialization layer — no auth, crypto, permissions, or user-facing input handling. The new decode paths validate length and reject unknown version bytes with explicit errors rather than partial values, and short-buffer rejection is covered by tests.

Level of scrutiny

Low. The change is mechanical and self-contained: an API extension plus a versioned framing bump for on-disk values. The littblock store is explicitly documented as pre-production ("no prior format is supported"), so the on-disk format change carries no migration risk. Tests cover round-trip with the embedded number, unknown-version rejection, garbage rejection, and hash-lookup correctness across both implementations and gapped writes.

Other factors

The two findings surfaced by the bug hunter are both pure documentation nits (a misleading version-constant comment and a missing If in a godoc sentence) with no runtime impact — worth fixing but not blocking. The QC-before-block invariant, watermark semantics, and iterator snapshot semantics are preserved. Shared test suite assertBlocksReadable now asserts hash-lookup returns the correct number for both impls, giving strong contract coverage.

Comment thread sei-db/ledger_db/block/littblock/codec.go
Comment thread sei-tendermint/autobahn/types/block_db.go Outdated
// diverge. Decode rejects any other version outright: this store is not yet in
// production, so no prior format is supported.
const blockSerializationVersion byte = 1
const qcSerializationVersion byte = 1

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.

why do we need qcSerializationVersion to return a block number?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't. I just added a 1 byte serialization version to the data schema since it's good practice to encode serialization versions (and it was originally missing). This way, if we ever decide to alter the serialization schema for a QC in the future, we will be able to do so with little headache.

// is framed as [version:1][GlobalBlockNumber:8 big-endian][proto(Block)]. The
// number is embedded so a by-hash lookup — which reaches this same shared value
// through a secondary key that carries only the hash — can still recover it.
func encodeBlock(n types.GlobalBlockNumber, blk *types.Block) []byte {

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.

I thought we didn't have to encode block number into the block because it was the key. Would this hurt the performance? I guess not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is in the primary key, but it's not in the secondary key. LittDB keeps no map from secondary keys to primary keys, and so if we want this data when we get by block hash, it needs to go into the value data.

Technically yes, this does add some overhead. But in practical terms, this overhead is negligible. We're adding 8 bytes onto a 2 megabyte block (this is the size of the blocks Greg asked me to simulate in my benchmark).

@cody-littley cody-littley requested a review from Kbhat1 July 7, 2026 15:31

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A focused, well-tested change that adds the block's GlobalBlockNumber to ReadBlockByHash; the interface and both implementations are updated consistently. The only point worth confirming is that the change alters the on-disk value format for both blocks and QCs (adding a version prefix), which would reject any pre-existing littblock data on upgrade — likely a non-issue since the store appears to be pre-production, but worth verifying.

Findings: 0 blocking | 4 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • On-disk format compatibility (raised by Codex): decodeBlock/decodeQC now require a leading version byte, and the block value additionally embeds an 8-byte number, so the previous raw-protobuf values become undecodable after upgrade. This affects both blocks AND QCs, so recoverCursors (which decodes the newest persisted QC in NewBlockDB) would fail to open any store containing legacy values. I could not find littblock.NewBlockDB wired into any production path (only tests and blocksim), so this is most likely pre-production with no persisted data to migrate — please confirm; if any node has persisted littblock data, a migration or legacy decoder is required before merge.
  • Cursor's second-opinion review file (cursor-review.md) was empty — that pass produced no output. REVIEW_GUIDELINES.md was also empty, so no repo-specific standards were applied.
  • The new GlobalBlockNumber return value of ReadBlockByHash is not yet consumed by any production caller (grep finds no non-test/non-impl callers); this is a forward-looking API addition, which is fine but worth noting.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

if len(value) < blockValuePrefixLen {
return 0, nil, fmt.Errorf("block value too short: %d bytes", len(value))
}
if value[0] != blockSerializationVersion {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This introduces a versioned on-disk value format ([version][number][proto] for blocks, and [version][proto] for QCs at line 122). Any block/QC value persisted by a prior build was raw protobuf with no version byte, so this check will reject it on upgrade — and since recoverCursors in NewBlockDB decodes the newest persisted QC, an existing store would fail to open. This looks safe only because littblock.NewBlockDB doesn't appear to be wired into any production path yet. Please confirm there is no persisted littblock data in the field; otherwise a migration or a legacy (unversioned) decode fallback is needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no production use yet

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — small, well-tested API extension; prior doc nits are fixed.

Extended reasoning...

Overview

This PR adds a GlobalBlockNumber return value to BlockDB.ReadBlockByHash so callers can recover a block's number from a hash lookup. To support this on the durable littblock backend (where the by-hash secondary key does not map to the by-number primary key), the block value's on-disk format changes: it is now framed as [version:1][number:8 big-endian][proto(Block)]. QC values pick up a matching 1-byte version prefix as a schema-evolution hook (not functionally required for this PR). All three implementations (interface, litt, mem), their tests, and the block iterator's decode call site are updated in lockstep.

Security risks

None material. The storage format change is a breaking bytes-on-disk change, but the author confirmed on the thread that this store is not yet in production, so there is no migration concern. Decode does bounds- and version-check before reading, so malformed input surfaces an error rather than a panic or partial value.

Level of scrutiny

Low. This is a self-contained API/serialization tweak in a pre-production ledger store, with unit tests covering the round-trip, version rejection, and the memblock/littblock parity. The interface signature is threaded correctly to every caller (verified via grep — only 4 files reference ReadBlockByHash, all included in the PR).

Other factors

Both nits from my prior review — the missing If in the interface godoc, and the misleading "must never share a version number" comment on the two version constants — were addressed in the latest commit. Reviewer questions from wen-coding were answered by the author (QC version byte is defensive, per-block overhead is 8 bytes on a 2 MB block). No outstanding concerns.


// blockValuePrefixLen is the fixed header preceding a block's proto bytes: one
// version byte followed by the 8-byte big-endian GlobalBlockNumber.
const blockValuePrefixLen = 1 + 8

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: since Block is already a protobuf, would it be easier for the version and blockNumber be in a protobuf wrap as well?

rng := utils.TestRngFromSeed(1)
for range 16 {
for i := range 16 {
n := types.GlobalBlockNumber(i * 7)

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: why i*7? in most cases we should have consecutive blocks.

}

func getBlock(table littdb.Table, key []byte) (utils.Option[*types.Block], error) {
func getBlock(table littdb.Table, key []byte) (utils.Option[*types.Block], types.GlobalBlockNumber, error) {

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: would it be more natural to return utils.Option[(*types.Block, types.GlobalBlockNumber)]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants