Skip to content

explorer: fix /blocks per-element state and adj; consensus: pbft cfg checksum - #2079

Open
Maxnflaxl wants to merge 2 commits into
masterfrom
issue/2077-2078
Open

explorer: fix /blocks per-element state and adj; consensus: pbft cfg checksum#2079
Maxnflaxl wants to merge 2 commits into
masterfrom
issue/2077-2078

Conversation

@Maxnflaxl

@Maxnflaxl Maxnflaxl commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

Two bugs in the explorer's /blocks (plural) endpoint, reported by @dbadol as #2077 and #2078. Both live in Adapter::get_blocks, so they're fixed together.

Plus an unrelated one-line Rules fix, found while investigating why @dbadol's dappnet2 explorer wouldn't start on a post-HF6 build — see the third section.

#2077 — every element carries the first block's header

Regression from ba181a9, which replaced the old per-row get_block_impl() walk with a StateID/m_Number loop. The loop advances the row but never reloads the state for it, so every element after the first is rendered against the first block's Block::SystemState::Full:

sid.m_Number.v++;
sid.m_Row = _nodeBackend.FindActiveAtStrict(sid.m_Number);   // `s` left stale

Because the body is read from sid.m_Row (correct) while the header and context come from s (stale), the damage is wider than the report: on top of h and info, the totals table (Chainwork, Current Emission, Current Circulation, Size Compressed, Size Archive) and outputs[].Maturity are all wrong. Verified against mainnet — for block 27730 in a height=27729&n=4 series, emission reads 2,218,320 instead of 2,218,400 and the coinbase maturity 27969 instead of 27970. inputs and kernels were the only correct sections.

Fixed by reloading the state after each step.

#2078adj is ignored

adj simply never reached the backend: on_request_blocks parsed only height and n, and get_blocks had no adj parameter. What looked like "defaults to adj=1" was FindBlockByHeightFindAtivePastHeight snapping to the next active height unconditionally, which also meant /blocks had no not-found path at all.

Fixed per the three points in the issue:

  1. adj is parsed and passed through, and applies to the first block of the series only; the rest always follow upwards.
  2. A vacant height with no adj returns {"found":false,"height":N}, matching /block.
  3. Once the walk passes the tip, the remaining elements are filled with the same not-found objects, so the array always has n entries.

To keep /block and /blocks from drifting apart again, the first-block resolution is now shared: get_block's logic is factored into ResolveBlock(sid, s, h, height, adj) and both endpoints call it.

Rules — PBFT networks can't start after HF6

Separate from the above, and the reason the /blocks fixes can't be tested on dappnet2 right now: any build after 3c1e73f ("consensus: fees go into coinbase from HF6") refuses to start on dappnet2 and warp_dev3 with

EXCEPTION: Data configuration is incompatible: 28c7c7b263b6254a

That commit added << (uint32_t) 1 // fees in coinbase to the fork6 oracle, changing the HF6 hash. Both networks use SetForksFrom(0, 0), so all their forks sit at height 0 and their DBs are stamped with the HF6 hash — which FindFork() no longer finds, so NodeProcessor::Initialize throws. mainnet/dappnet/testnet/masternet are unaffected: they stamp HF5, which didn't change.

The rule itself never applies to PBFT — both the validation (assert(Rules::Consensus::Pbft != rules.m_Consensus) in block_validation.cpp) and the block assembly are gated on non-PBFT. Only the checksum line wasn't, so the PBFT nets break over a rule that can't fire on them. Gating it matches the existing conditionals in UpdateChecksum (CA.ForeignEnd, the Shielded.MaxIns/MaxOuts pair, the whole PBFT block).

Fork tables dumped per network, before and after:

Network fork6 before fork6 after
dappnet2 c94b179468bd193f 28c7c7b263b6254a (the hash in @dbadol's DB)
warp_dev3 1a430a4062b84b5d e2fc30342d1b6d4c
mainnet 96df3f33ee02ad9e 96df3f33ee02ad9e — unchanged

So both PBFT nets start again on their existing DBs, with no resync and no coordinated upgrade, and mainnet — already past HF6 at 3,928,666 — is untouched. Chain data is unaffected either way, since the rule never applied to those chains.

Changes

  • explorer/server.cppon_request_blocks parses adj and passes it on.
  • explorer/adapter.hget_blocks(Height, uint64_t, int adj).
  • explorer/adapter.cpp — new ResolveBlock helper shared with get_block; get_blocks refreshes the state per element, honors adj for the first block, and emits not-found placeholders.
  • core/block_crypt.cpp — the fees-in-coinbase value is fed to the fork6 checksum for non-PBFT consensus only.

Behavior

Request (dappnet2, PoS with gaps) Before After
blocks?height=2743&n=4 4 blocks, all with 2743's header 4 blocks, own headers
blocks?height=2742&n=4 (vacant) series from 2743 [{"found":false,"height":2742}]
blocks?height=2742&n=4&adj=1 series from 2743 series from 2743
blocks?height=2744&n=4&adj=-1 series from 2795 ❌ series from 2743 ✅
series running past the tip short array, empty blocks n entries, tail is found:false

Two judgement calls worth a look, since the spec is ambiguous for PoS chains:

  • Point 2 returns a one-element array rather than a bare object, to keep /blocks type-stable for clients. Easy to change if the bare object is preferred.
  • The placeholder heights past the tip are consecutive (tip+1, tip+2, …), which only carries meaning on PoW. On a PoS chain with gaps they should be read as "no block here", not as a height prediction.

Closes #2077
Closes #2078

@Maxnflaxl Maxnflaxl changed the title explorer: fix stale per-element state and ignored adj in /blocks explorer: fix /blocks per-element state and adj; consensus: pbft cfg checksum Aug 18, 2026
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.

Explorer node: Parameter adj is ignored in queries of type blocks Explorer node: Wrong info in queries with type 'blocks'

1 participant