Wire safeTarget validation into fork-choice spec test harness#322
Wire safeTarget validation into fork-choice spec test harness#322pablodeymo wants to merge 3 commits intomainfrom
Conversation
the spec-test runner actually validates safe_target behavior end-to-end. Today the harness explicitly rejects any fixture carrying a \'safeTarget\' field with "'\safeTarget\' check not supported", and it has no field at all for the leanSpec #680 schema (safeTargetSlot, safeTargetRootLabel). The result is that the safe_target semantics shipped in PR #316 have no fixture-level coverage: regenerated fixtures are silently parsed without asserting on the field, and any test using the legacy \'safeTarget\' field errors out instead of validating. Changes: - Add safeTargetSlot and safeTargetRootLabel to StoreChecks, mirroring the latestJustified* / latestFinalized* pattern. - Replace the rejection branch with the same label-resolution pattern used for justified/finalized roots, falling back from safeTarget to safeTargetRootLabel via the step\'s block registry. - Add validation blocks for safeTargetSlot (against st.safe_target_slot()) and the resolved root (against st.safe_target()). - Reorganize StoreChecks into logical groups and drop the now-stale "Unsupported fields (will error if present)" comment, which was misleading: only safeTarget actually errored, everything else was validated. This change is forward-compatible: no current fixture in leanSpec/fixtures/consensus/ contains any safeTarget* field, so the new validation paths are dormant until LEAN_SPEC_COMMIT_HASH is bumped to a revision that includes leanSpec #680. Verified with cargo fmt --all, cargo clippy --workspace --all-targets -- -D warnings, and the forkchoice_spectests harness (47 passing fixtures unchanged). Pre-existing failures from a stale leanSpec submodule checkout (1 fork-choice fixture, 34 ssz fixtures) reproduce identically on main and are unrelated to this change.
🤖 Kimi Code ReviewThe changes correctly extend the fork choice test runner to support the leanSpec #680 schema for safe target validation. The implementation follows existing patterns for root resolution and validation.
Minor suggestion: In Verification note: Ensure that Approval: LGTM. The pattern correctly generalizes the existing root-label resolution infrastructure to support the new safe target fields without breaking legacy test fixtures that specify Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
Greptile SummaryThis PR wires Confidence Score: 4/5Safe to merge; all existing 47 fixtures pass and new code paths are dormant until upstream fixtures change. Only P2 findings — a minor forward-compat struct symmetry concern. No logic errors, no security issues, and the changes closely mirror the well-tested justified/finalized patterns. No files require special attention; the one note is in types.rs regarding a potentially missing safeTargetRoot field for future schema symmetry.
|
| Filename | Overview |
|---|---|
| crates/blockchain/tests/types.rs | Adds safe_target_slot and safe_target_root_label to StoreChecks, reorganizes fields into logical groups, and removes the misleading "Unsupported fields" comment — straightforward struct extension that mirrors the existing justified/finalized pattern. |
| crates/blockchain/tests/forkchoice_spectests.rs | Replaces the safeTarget rejection branch with label-resolution logic and adds safeTargetSlot / safeTarget root validation blocks; logic is consistent with existing justified/finalized patterns but a missing safeTargetRoot (non-label) direct-root field could silently drop fixtures that supply it. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[validate_checks called] --> B{safe_target set?}
B -- yes --> C[resolved_safe_target_root = safe_target H256]
B -- no --> D{safe_target_root_label set?}
D -- yes --> E[lookup label in block_registry]
E --> F[resolved_safe_target_root = root from registry]
D -- no --> G[resolved_safe_target_root = None]
C --> H{safe_target_slot set?}
F --> H
G --> H
H -- yes --> I[compare st.safe_target_slot vs expected_slot]
I -- mismatch --> J[return Err]
I -- match --> K{resolved_safe_target_root set?}
H -- no --> K
K -- yes --> L[compare st.safe_target vs expected_root]
L -- mismatch --> M[return Err]
L -- match --> N[continue validation]
K -- no --> N
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/blockchain/tests/types.rs
Line: 170-178
Comment:
**Missing `safeTargetRoot` (direct-root) field for new schema**
The new leanSpec #680 schema is paired as `safeTargetSlot` + `safeTargetRootLabel`. If a future fixture revision also ships a direct-root counterpart named `safeTargetRoot` (mirroring the `latestJustifiedRoot` / `latestFinalizedRoot` pattern rather than reusing the legacy `safeTarget` field), serde will silently discard it and the validation block in `forkchoice_spectests.rs` will never fire. Consider adding `safeTargetRoot: Option<H256>` now so the struct schema is symmetric with `latestJustified*` / `latestFinalized*`, even if it stays dormant until fixtures use it.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Wire safeTarget store checks in the fork..." | Re-trigger Greptile
| /// Legacy single-field schema; expected safe target block root. | ||
| #[serde(rename = "safeTarget")] | ||
| pub safe_target: Option<H256>, | ||
| /// Expected slot of the safe target block (leanSpec #680 schema). | ||
| #[serde(rename = "safeTargetSlot")] | ||
| pub safe_target_slot: Option<u64>, | ||
| /// Expected safe target block root by label reference (leanSpec #680 schema). | ||
| #[serde(rename = "safeTargetRootLabel")] | ||
| pub safe_target_root_label: Option<String>, |
There was a problem hiding this comment.
Missing
safeTargetRoot (direct-root) field for new schema
The new leanSpec #680 schema is paired as safeTargetSlot + safeTargetRootLabel. If a future fixture revision also ships a direct-root counterpart named safeTargetRoot (mirroring the latestJustifiedRoot / latestFinalizedRoot pattern rather than reusing the legacy safeTarget field), serde will silently discard it and the validation block in forkchoice_spectests.rs will never fire. Consider adding safeTargetRoot: Option<H256> now so the struct schema is symmetric with latestJustified* / latestFinalized*, even if it stays dormant until fixtures use it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/tests/types.rs
Line: 170-178
Comment:
**Missing `safeTargetRoot` (direct-root) field for new schema**
The new leanSpec #680 schema is paired as `safeTargetSlot` + `safeTargetRootLabel`. If a future fixture revision also ships a direct-root counterpart named `safeTargetRoot` (mirroring the `latestJustifiedRoot` / `latestFinalizedRoot` pattern rather than reusing the legacy `safeTarget` field), serde will silently discard it and the validation block in `forkchoice_spectests.rs` will never fire. Consider adding `safeTargetRoot: Option<H256>` now so the struct schema is symmetric with `latestJustified*` / `latestFinalized*`, even if it stays dormant until fixtures use it.
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewNow I have enough context to write the review. Here it is: PR 322 Review: Wire safeTarget validation into fork-choice spec test harnessOverviewThis PR closes a test-coverage gap introduced by PR 316: the Code CorrectnessResolution logic is correct. The Schema asymmetry is intentional and correct. The justified/finalized pattern has three fields each ( Validation order is consistent. Slot check precedes root check (lines 336–358), matching the justified/finalized ordering. Potential IssuesSilent skip when
Style and ConventionsStruct reorganization is a clear improvement. Grouping fields by logical concern (time → head → justified → finalized → safe-target → attestation/lexicographic) with blank-line separators makes Doc comment on Field-level
Test CoverageNo existing fixture carries any SummaryThe change is minimal, focused, and correct. It implements exactly what was described in the PR 316 gap note. The pattern is a faithful copy of the existing label-resolution infrastructure and introduces no new logic risks. The one concern worth tracking as a follow-up (silent skip on unresolved labels) predates this PR and applies equally to all No blocking issues. Ready to merge once CI is green. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code ReviewFindings
Notes I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
spec-test harness. This fixture, shipped with the currently-pinned LEAN_SPEC_COMMIT_HASH, encodes the pre-leanSpec-#680 merged-pool semantics for update_safe_target. PR #316 deliberately diverged from that, switching to the new pool only, which means the fixture's safeTargetSlot=2 expectation no longer holds: with no fresh aggregations in the new pool at interval 3, safe_target now collapses to the latest justified root (slot 0). Until #316 went in this divergence was invisible because the harness silently rejected the legacy safeTarget field. The previous commit on this branch wired safeTargetSlot/safeTargetRootLabel into StoreChecks, turning that silent skip into a real assertion and surfacing the failure. The right resolution is to retire the fixture when leanSpec #680 lands and LEAN_SPEC_COMMIT_HASH is bumped; until then, skip it explicitly with a comment that points back to #316 and documents the empty-new-pool collapse to latest_justified. Verified with cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings.
MegaRedHand
left a comment
There was a problem hiding this comment.
I don't like that we are skipping a test here. Let's check again once #317 is merged, since it bumps leanSpec commit to latest.
Summary
Follow-up to #316. Wires
safeTarget,safeTargetSlot, andsafeTargetRootLabelinto the fork-choice spec-test harness so regenerated fixtures actually assert onupdate_safe_targetbehavior.Motivation
#316 changed
update_safe_targetto consume only thenewattestation pool, in line with leanSpec PR #680. The behavior change is correct but had no fixture-level coverage:safeTargetfield already exists onStoreChecksbut the harness explicitly rejects it with"'safeTarget' check not supported"(crates/blockchain/tests/forkchoice_spectests.rs:224-226). Any fixture using it errors out instead of validating.safeTargetSlot/safeTargetRootLabelfields from leanSpec #680 aren't on the struct at all, so serde silently drops them when fixtures are regenerated.The behavior is therefore only observable indirectly via
attestationTargetSlot(becausesafe_targetclamps the attestation walk-back inget_attestation_target_with_checkpoints), which doesn't pin down wheresafe_targetactually landed. #316's PR description already flagged this gap; this PR closes it.Changes
crates/blockchain/tests/types.rs:safeTargetSlot: Option<u64>andsafeTargetRootLabel: Option<String>toStoreChecks, mirroring thelatestJustified*/latestFinalized*pattern.// Unsupported fields (will error if present)comment block. OnlysafeTargetactually errored; the rest (headRootLabel,latestJustified*,latestFinalized*,lexicographicHeadAmong) were all wired and validated.crates/blockchain/tests/forkchoice_spectests.rs:safeTargetrejection branch with the same label-resolution pattern used for justified/finalized roots:safeTargetfalls back tosafeTargetRootLabelresolved through the step'sblock_registry.safeTargetSlotvalidation block comparing againstst.safe_target_slot().st.safe_target().Forward compatibility
No current fixture in
leanSpec/fixtures/consensus/carries anysafeTarget*field (verified viarg -l "safeTarget" leanSpec/fixtures/consensus), so the new validation paths are dormant untilLEAN_SPEC_COMMIT_HASHis bumped to a revision that ships leanSpec #680. Zero risk of breaking existing tests; the change only takes effect once upstream regenerates fixtures with the new schema.Test plan
cargo fmt --allcargo clippy --workspace --all-targets -- -D warningscargo test --workspace --release --lib --bins— all unit tests passcargo test -p ethlambda-blockchain --release --test forkchoice_spectests— 47/47 fixtures pass against the currently-pinnedLEAN_SPEC_COMMIT_HASHLEAN_SPEC_COMMIT_HASHis bumped, regenerated fixtures will exercise the new assertions end-to-endRelated
latestJustifiedRoot/latestFinalizedRoot.