Conversation
|
Flagging a gap in the same function this PR touches ( The gap. historyStorage.get(chunkId).flatMap(bs => SubtreeSerializer.parseBytesTry(bs).toOption)so a subtree that is missing on read-back ( Witness. A local spec that registers one unparseable chunk ( Suggested addition — a completeness pre-check at the top of the val restored = downloadedChunksIterator().size
if (restored != manifest.subtreesIds.size) {
val m = s"Incomplete UTXO snapshot restore: $restored of ${manifest.subtreesIds.size} subtrees; failing closed"
log.error(m)
Failure(new IllegalStateException(m))
} else {
// existing reconstructStateContextBeforeEpoch → recreate → StateWriteFailure recovery
}Two caveats:
Happy to open this as a small commit against your branch or a separate PR — whichever fits your sequencing (#2545 → #2546 → into #2496's |
|
@cafebedouin, thanks for flagging this. I checked the read-back and restore paths at Normal network intake already validates requested chunk identities. This gap concerns the later storage read-back; the supplied witness should not be interpreted as malformed peer bytes bypassing that intake check. Please post a small commit and the failing spec here against the current #2546 head, rather than opening a parallel PR. That keeps this within the existing #2545 -> #2546 review sequence. The invariant should cover the chunks actually consumed during restoration. A separate count scan followed by another permissive scan is insufficient if read-back changes; equal counts also do not establish the expected chunk identities. Prefer a bounded streaming check against the manifest, with missing or unparseable expected chunks producing failure. Preserve Please include your complete-snapshot control and separate missing-chunk and unparseable-chunk cases, with the exact tested commit. A parsed wrong-ID substitution is the useful negative for any identity check. A digest taken from the restored pruned root alone should not be described as a full materialization check. We can review the focused increment before integration without changing the unrelated Digest checkpoint behavior. |
A complete-snapshot control and three negatives for createPersistentProver: a chunk missing on read-back, an unparseable stored chunk, and a valid chunk stored under another chunk's id. Each case uses its own processor and chunk storage. The three negatives fail at this commit: restoration returns Success. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
createPersistentProver fed recreate() from downloadedChunksIterator(), which skips a chunk that is absent or fails to parse, so an incomplete tree was recorded under the manifest digest. Restoration now streams exactly the chunks the manifest declares, in manifest order, one read per subtree id, and fails on the first chunk that is missing, unparseable, or does not verify against its expected id. The check runs on the chunks recreate() consumes; there is no separate counting pass. Failures surface through the existing StateWriteFailure wrapping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@a-shannon — done as asked: two commits on top of the current #2546 head Branch:
The invariant, per your notes. The count pre-check I first suggested is dropped.
Failure typing. The iterator is consumed inside Specs (each case builds its own processor with its own chunk storage, so nothing stale from another property can mask a result):
The wrong-id case keeps the chunk count equal to the manifest and every stored chunk parseable — only identity differs — so it is the negative a count check would have passed. Regression on Not claimed. Agreed on your intake point: this is a storage read-back gap, not malformed peer bytes bypassing the network-side identity check; the specs write directly through Cherry-pick, squash or rewrite as suits your sequence. AI-assisted (Claude Fable 5.1, via Claude Code); tests run locally by me before posting. |
|
@a-shannon — separate from the restore increment above: I read the #2546 increment ( 1.
|
same store, restart with utxoBootstrap = true |
result |
|---|---|
#2545 parent 036c2c258f7aceef00ead6d92cfa238f1e4722bf |
starts; view returned, floor 20 |
#2546 head 63c9b79dc576461f6266c234612c35d8ed393d73 |
actor system terminates: requirement failed: Stored snapshot root does not match canonical history (DigestState.readSnapshot) |
Control in both runs: restarting with the original utxoBootstrap = false config starts normally. Fixture: real ErgoNodeViewRef over reopened stores (your Session/closeOwnedStores pattern), Digest, blocksToKeep = 10, votingLength = 20, 45 fake-PoW headers, no full blocks. Caveat: in this fixture header application did not itself advance the floor, so the witness calls history.updateBestFullBlock(lastHeader) directly — the function toDownload calls when the header chain first syncs — to write it. Spec: PrunedDigestFlipWitness (one file on top of 63c9b79d; it reports both outcomes via info rather than asserting the flip, so it runs unchanged at either commit).
It is a narrow, operator-induced window (config change between header sync and the first full block) and reverting the config recovers, so I would not call it severe. But the fail-closed branch is keyed on a predicate that does not mean what the branch assumes, and FullBlockProcessor's new utxoBootstrap && isUtxoSnapshotApplied condition shares it. Suggested change, smallest first:
- in
readStateForStartup, take the fail-closed path only when the state store actually carries snapshot evidence (e.g. the snapshot header-id version key thatfromSnapshotwrites), and otherwise fall through toErgoState.readOrGenerateas before; or - persist an explicit snapshot-applied marker in
onUtxoSnapshotAppliedand key both new consumers on that rather than on the floor.
Either way, the flip case above makes a cheap regression.
2. The post-write shutdown now applies to UTXO mode but is only tested in Digest
case Failure(t: StateWriteFailure) => abortSnapshotStatePreparation(t) is mode-independent, so a UTXO-mode node now also shuts down where it previously logged and continued. I think that is the right behaviour. The lifecycle matrix only runs the postWriteFailure case for StateType.Digest, though; a UTXO variant of that case (the fault-injecting holder is currently built around a DigestState, so it is a small fixture addition rather than one more row) would cover the mode most nodes run, and the description could say the change is not Digest-only.
3. Imported AVL payload in a Digest store — question, not a blocker
You already note that the imported AVL payload stays on disk. For a Digest node that is the full UTXO set, retained indefinitely and never read after fromSnapshot. Is removing it after the anchor is written in scope for a follow-up? If so, an issue reference in the description would help whoever sizes Digest-mode disk later. Related, and within your stated "production qualification is outside this evidence": fromSnapshot's store.clean(0) deletes every undo LSN written by recreate in a single WriteBatch, which at mainnet size is one delete per tree node — worth one observed run before this path is relied on.
AI-assisted (Claude Fable 5.1, via Claude Code); witness run locally on JDK 8 at both commits named above.
With
stateType="digest"and UTXO snapshot bootstrap enabled, snapshot reconstruction previously installed a UTXO state and persisted AVL-root versions. The Digest loader interprets versions as block IDs, so a restart before the first full block could fall back to genesis while history still reported the snapshot as applied. Incoming detached chunks were also dropped because the receive branch required a local UTXO reader.This addresses the Digest configuration concern in #2496. The correction installs the configured state representation, persists the snapshot header ID, root and reconstructed context, and preserves a usable Digest rollback anchor across restart. Requested chunks can be received in both modes; Digest does not gain snapshot-serving capabilities. A failure after entering state-store reconstruction or preparation stops the holder in both Digest and UTXO modes, rather than continuing with potentially mutated state.
Two follow-ups address cafebedouin's review:
StateWriteFailure; this is a storage read-back correction, not a bypass of normal network intake checks.Fixed review increments and integration
The current
i2464target atff4d9a1806f4f411e848680a7c4e4b0623725309already includes the ordinary-header synchronization behavior from #2545. That PR is no longer an integration prerequisite. This branch incorporates the updated target, keeps its current startup tests, and removes the duplicate synchronization implementation. The current nine-file diff contains only the snapshot follow-up and its direct tests.Previously reviewed increments remain available:
036c2c258f7aceef00ead6d92cfa238f1e4722bf.#2496 has now merged into
v6.0.6; this follow-up remains open againsti2464and was not part of that merge. Its release/target placement now needs a maintainer decision after validation. Independent review confirmed that the target alignment retains the snapshot patch and all upstream holder changes.The deferred #2424 and combined wallet review retain earlier adapted checkpoint/receive changes. They do not yet contain these two new increments; their refresh belongs to their own integration step.
Validation at
02ae9072e19b55e61a177200f0005fa5cf286146a469d6097: 57 cases across eight snapshot, Digest-history, bootstrap and network suites, plus 3 prepared-UTXO holder cases. This includes 7 bootstrap/lifecycle and 25 direct snapshot-state cases. Java 17, Scala 2.12, Sigma 6.0.6. The subsequent fixture-only change creates its state directory before opening native LevelDB; the affected pruning case passes its focused rerun (1/1), while the remaining local evidence is unchanged.utxoBootstrap=true, then applies the first full block after the corrected restart.02ae9072e: completed with six successful jobs and two failed jobs. A request to rerun only node and integration has been sent to the maintainers; no rerun has started at this update.ErgoNodeViewHolderSpeccasetxScriptFailure carries failing transaction id; its isolated local rerun passes all four configurations, so the CI cause remains unestablished. Current integration has 21/22 passed, withDeepRollBackSpectiming out during convergence. The preceding integration run separately timed out inForkResolutionSpec(21/22 passed). No fix is claimed for these other failures. Repository permissions prevent us from rerunning the failed jobs.Historical evidence: head
63c9b79dhad 35 receive/synchronizer cases plus 44 checkpoint/lifecycle cases and 8/8 CI checks. Those results belong to that earlier head.The fixtures use fake PoW and small states. The network fixture registers its manifest and requests directly; full wire-manifest negotiation, TCP framing, process interruption and production-network qualification are outside this evidence. The per-subtree identity checks do not rederive a full materialized-tree digest. Imported AVL payload remains on disk in Digest mode; cleanup is outside this PR, and the mainnet-sized
clean(0)undo batch has not been measured. Inconsistent stores remain available for recovery. #2464 remains the broader qualification issue. Consensus validation and NiPoPoW proof handling are unchanged.