feat: Detect a stale cold-pool aggregate during checkpoint validation - #11037
Draft
Dfinity-Bjoern wants to merge 1 commit into
Draft
feat: Detect a stale cold-pool aggregate during checkpoint validation#11037Dfinity-Bjoern wants to merge 1 commit into
Dfinity-Bjoern wants to merge 1 commit into
Conversation
`CanisterStates` keeps a precomputed `ColdStats` aggregate over the cold pool so that `total_consumed_cycles()` and `total_canister_memory_usage()` are `O(|hot|)` rather than `O(|all canisters|)`. Until now nothing verified that the aggregate still matches the pool it summarizes outside of `debug_assert`s, even though it is already consensus-critical: `cold_stats.memory_usage` feeds `total_canister_memory_usage()`, which message routing writes to `subnet_metrics.canister_state_bytes`, which is certified in the state tree. A silently drifted aggregate therefore diverges subnets today, with nothing to say so. Adds `CanisterStates::validate_cold_stats()`, which recomputes the aggregate from the cold pool and reports a mismatch, and calls it from checkpoint validation. Scope and limits, stated plainly: * This is **advisory**. `validate_eq_checkpoint` discards the error, logs `CRITICAL_ERROR_REPLICATED_STATE_ALTERED_AFTER_CHECKPOINT`, increments `replicated_state_altered_after_checkpoint`, and finalizes the checkpoint regardless. Detection also lags a full checkpoint interval. It is detection and attribution, not prevention. Making it enforce would change shared state-manager behaviour for every subnet and every field it validates, and belongs in its own change if wanted. * It costs `O(|cold|)` arithmetic over already-resident canisters, on a path that already loads every canister from disk and deep-compares it. The cost is negligible against what surrounds it. * It runs *after* the per-canister comparison and the two errors are combined rather than `?`-propagated. In the scenario where this check fires, the per-canister diagnostics are what tell the operator *which* canister drifted; an advisory check must not cost them that. Split out of the `subnet_metrics` management endpoint work, where it was originally written. That endpoint does not depend on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds an explicit runtime validation that the CanisterStates precomputed cold_stats aggregate still matches the actual contents of the cold pool, and wires that check into checkpoint validation so drift is detected (and attributed) in release builds.
Changes:
- Introduces
CanisterStates::validate_cold_stats()to recompute cold-pool aggregates and report mismatches. - Extends checkpoint canister-state validation to run
validate_cold_stats()and combine its error with per-canister validation errors (without short-circuiting). - Adds unit tests covering both the consistent case and a deliberately-staled aggregate case.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| rs/state_manager/src/checkpoint.rs | Runs cold-pool aggregate validation during checkpoint equality validation and combines outcomes with per-canister validation results. |
| rs/replicated_state/src/canister_states.rs | Adds validate_cold_stats() to detect stale cold_stats vs a recomputation over the cold pool, with rationale documented for checkpoint validation usage. |
| rs/replicated_state/src/canister_states/tests.rs | Adds tests to ensure validate_cold_stats() accepts consistent aggregates and rejects intentionally-staled aggregates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CanisterStateskeeps a precomputedColdStatsaggregate over the cold pool so thattotal_consumed_cycles()andtotal_canister_memory_usage()areO(|hot|)rather thanO(|all canisters|).Nothing verified that the aggregate still matches the pool it summarizes, outside of
debug_asserts — even though it is already consensus-critical:cold_stats.memory_usagefeedstotal_canister_memory_usage(), which message routing writes tosubnet_metrics.canister_state_bytes(message_routing.rs:1503), which is certified in the state tree. So a silently drifted aggregate diverges subnets today, with nothing to say so.This adds
CanisterStates::validate_cold_stats(), which recomputes the aggregate from the cold pool and reports a mismatch, and calls it from checkpoint validation.Scope and limits
Stated up front, because it would be easy to over-read what this buys:
It is advisory, not enforcing.
validate_eq_checkpointdiscards the error viaunwrap_or_else, logsCRITICAL_ERROR_REPLICATED_STATE_ALTERED_AFTER_CHECKPOINT, incrementsreplicated_state_altered_after_checkpoint, and finalizes the checkpoint regardless. Detection also lags a full checkpoint interval, so a drifted value has already been served before the alert fires. This is detection and attribution, not prevention.Making it actually enforce would change shared state-manager behaviour for every subnet and every field that path validates. That is a deliberate non-goal here; if wanted it should be its own change.
The cost is negligible.
O(|cold|)arithmetic over already-resident canisters, on a path that already loads every canister from disk and deep-compares it against the in-memory state.The ordering is deliberate. It runs after the per-canister comparison, and the two errors are combined rather than
?-propagated. In the exact scenario where this check fires, the per-canister diagnostics are what tell the operator which canister drifted — an advisory check must not cost them that information.Provenance
Split out of the
subnet_metricsmanagement endpoint work (#11032), where it was written after review challenged the determinism argument for reading the hot/cold partition. That endpoint does not depend on this check, and the two were separated so this hardening of pre-existing code can be reviewed on its own merits rather than alongside a new public API.Verification
cargo check --all-targetsclean,cargo fmt -- --checkexit 0, targeted clippy with--deny warningsclean apart from three pre-existingunused_imports.bazel teston the two affected targets passes. Contains no reference tosubnet_metricsand stands alone onmaster.🤖 Generated with Claude Code