Skip to content

fix(validator): slashing-protection review follow-ups: attestation index check, exit on a poisoned record - #789

Open
pablodeymo wants to merge 3 commits into
mainfrom
validator-slashing-protection-followups
Open

pablodeymo wants to merge 3 commits into
mainfrom
validator-slashing-protection-followups

Conversation

@pablodeymo

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

#734 (durable slashing protection) and #736 (EIP-3076 import and export) were approved with a last round of review comments still open, and with two follow-ups set aside for a separate PR. This is that PR. It changes nothing about the slashing rule or the record's format.

Seven of the eight open points are addressed here. The eighth, ethlambda-rpc's dev-dependency on ethlambda-validator, is left alone and explained at the end.

What Changed

From the last round on #734

  • A re-import that was fully undone no longer says "keystore rewritten" (comment). Since b339ff7 the undo puts the key's keystore and password files back as well as its definitions entry, so the status now reads not imported: its slashing-protection record could not be written: …; its keystore and definitions entry are back as they were. The old wording is kept for the case where the undo itself failed. (http_api/keystores.rs)

From the last round on #736

  • The vector runner skips a case only for the one refusal this importer makes (comment). It used to end a case whenever a step flagged contains_slashable_data was refused. Now the step's interchange must also hold an attestation whose source is at or past its target outside genesis; every other slashable step has to import. At v5.3.0, 21 steps are flagged slashable and expected to import, and 4 of them hold such an attestation (the single_validator_source_greater_than_target* cases). The predicate is restated in the test rather than borrowed from the crate, so the importer widening what it refuses cannot widen what the runner forgives. (tests/interchange_vectors.rs)
  • an_import_carrying_history_is_refused_whole is renamed to an_import_whose_history_does_not_parse_is_refused_whole, with its doc, and now asserts that the refusal is for the history's shape (review).
  • The Interchange doc says what the types are for (comment): shared by the record reader and the importer, strict in both, as Lighthouse's are. Behaviour unchanged. (slashing_protection/file.rs)
  • export no longer claims to need a writable directory (comment). The lock is an flock on the directory itself and an export writes nothing there. A test pins it on a mode 0500 directory. (docs/cli.md)

The two follow-ups #734 set aside

  • Attestation data whose index cannot be valid for its fork is refused (review). validate_attestation_data now takes the slot's fork and refuses any index but 0 before gloas, and any above 1 from gloas, where it is the payload signal. Those are gossip's own bounds (attestation.data.index == 0 from electra, < 2 from gloas). Lodestar's validator client refuses the same before signing (validateAttestationDuty); Teku enforces them on its beacon-node side (validateCommitteeIndexValue).
    • It is a liveness check, not a slashing one. Such a vote is rejected by every peer, after slashing protection has spent the validator's one vote for the epoch on it. Refused in the beacon-node implementation, the answer is an Err that failover moves past, and nothing is recorded.
    • Before electra the index is a committee, and this client only ever asks about committee 0, so 0 is the only answer to its question. Every fork is a named match arm, so a fork after gloas has to say what its index means.
    • At gloas only the range is checked: whether 0 or 1 is right depends on the node's view of the block.
  • A poisoned record stops the client, and the panicking access is told the panic was its own (comment).
    • The duty loop asks slashing_protection::ensure_usable at each slot boundary and returns the error from run on a poisoned lock, so the process exits with a failure and systemd or Kubernetes restarts it. Before, it stayed up, answered /health and refused every duty.
    • blocking now tells the call whose own operation panicked this access panicked while holding the record (<panic message>). Only later calls get an earlier access panicked.

Docs: docs/cli.md gets the export wording, the index check in the attestation walkthrough, and a note to run the client under a supervisor.

Correctness / Behavior Guarantees

  • No change to what is signed or recorded for a well-behaved beacon node. The slashing rule, the record's format and the import's merge are untouched.
  • Behavior change, attestations: a beacon node answering with an index outside what the slot's fork allows is now failed over from, where its answer used to be signed. If every configured node answers that way, the slot's attestations are not made and nothing is recorded for them.
  • Behavior change, process lifetime: a validator client whose slashing-protection lock is poisoned now exits non-zero within one slot. That only follows a panic while the record was held, which is a bug; a slow or failing disk does not take this path. A client with no supervisor used to idle and will now stop.
  • Keymanager: only a status message changes. The same request is refused and the same files are restored.

Tests Added / Run

  • Index check:
    • a_nonzero_index_is_refused_before_gloas (every fork before gloas), a_gloas_index_is_zero_or_one, the_lean_fork_has_no_valid_index;
    • a_node_answering_with_an_index_invalid_at_the_fork_is_failed_over_from;
    • an_index_invalid_at_the_slots_fork_is_refused_and_records_nothing: the service still signs a correct answer for the same slot afterwards.
    • ethlambda-rpc's wire tests, which drive this client against the node's own Beacon API at fulu and gloas, pass unchanged.
  • Poisoned record: a_poisoned_record_stops_the_client, the_access_that_panics_is_told_so_and_later_ones_that_an_earlier_one_did, a_panics_message_is_read_whatever_it_was_raised_with. The four lines in run that act on ensure_usable have no test of their own, since run needs a live beacon node.
  • Export: an_export_needs_no_writable_directory.
  • Keymanager: a_failed_registration_restores_a_replaced_definitions_entry now asserts the new wording.
  • Vectors: all 38 still pass. Mutation check, with the importer changed to refuse a pubkey listed twice: the old skip reports 1 failing case and skips the other two duplicate_pubkey_* cases without a word; the new skip reports all 3.
  • Commands run:
    • make fmt: clean
    • make lint: clean
    • make test: 2927 passed, 0 failed (1324 in test-consensus, 1603 in test-node)

Related Issues / PRs

Not changed: ethlambda-rpc's dev-dependency on ethlambda-validator (comment). The dependency predates #734 (it came with #661); #734 only added the test-utils feature to it. It exists for rpc/src/beacon/validator_client_tests.rs, about 1,600 lines that drive the validator client's own HTTP client against this node's Beacon API over a socket, to check the two ends agree on the wire. Removing the dependency means moving that file to a crate that depends on both, and it reaches into ethlambda-rpc internals in about ten places (build_beacon_api_router, the test_utils module, FuluSignedBlockContents; proposal.rs's tests also borrow a helper back from it), so those would have to become a public test surface. That is a PR of its own, and it is worth agreeing where those tests should live before moving them.

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran make test (test-consensus plus test-node, at release-fast) — all passing

)

- A re-import whose registration failed and was fully undone now answers
  "not imported: ...; its keystore and definitions entry are back as they
  were". "keystore rewritten" stopped being true once the undo put the
  files back too, and is kept for the case where the undo itself failed.
- The interchange vector runner ends a case only when the refused step
  holds an attestation whose source is at or past its target outside
  genesis, the one shape this importer refuses, rather than on any step
  flagged as slashable. Every other slashable step must now import.
- an_import_carrying_history_is_refused_whole is renamed for what it has
  tested since the importer landed, history that does not parse, and
  asserts that this is the reason.
- The Interchange types' doc says they are shared by the record reader and
  the importer, both strict.
- docs/cli.md no longer says an export needs a writable directory: the
  lock is on the directory itself and an export writes nothing there. A
  test pins it on a mode 0500 directory.
validate_attestation_data checked the slot, the target epoch and the
source, and passed data.index through unchecked. It now takes the slot's
fork and refuses any index but 0 before gloas and any above 1 from gloas,
where the index is the payload signal. Those are gossip's own bounds
(attestation.data.index == 0 from electra, < 2 from gloas), and Lodestar's
validator client refuses the same before signing.

It is a liveness check rather than a slashing one. A vote whose index
cannot be valid is rejected by every peer, after slashing protection has
recorded its target, so the validator has no second vote to cast in that
epoch. Refused in the beacon-node implementation, the answer is an Err
that failover moves past, and nothing is recorded for it.

Before electra the index is a committee, and this client only ever asks
about committee 0. At gloas only the range is checked, since which of 0
and 1 is right depends on the node's view of the block. Every fork is a
named match arm, so a fork after gloas has to say what its index means.
Once a panic poisoned the record's lock, every duty was refused until a
restart, but the process stayed up and answered /health, so nothing that
supervises it had a reason to restart it. The duty loop now asks
slashing_protection::ensure_usable at each slot boundary and returns the
error from run on a poisoned lock: the process exits with a failure, and
systemd or Kubernetes applies the remedy the refusals ask for. The restart
rereads the record from disk, which every write either replaced whole or
left alone.

The access whose own operation panicked was told "an earlier access
panicked", which points at an event that never happened. It is now told it
was its own panic, with the panic's message; only later accesses get the
old wording.

docs/cli.md says to run the client under a supervisor.
@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

No actionable findings in the diff. The fork-specific attestation index validation runs before recording or signing, preserves beacon-node failover, and correctly allows Gloas indices 0 and 1. The poisoned-lock check propagates a fatal error for supervisor restart.

Could not run validator tests: Rustup attempted to write to the read-only /home/runner/.rustup/tmp directory.


Automated review by OpenAI Codex · gpt-6.1-sol · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Review of PR 789

Overall this is a solid PR. The index check closes a real liveness gap: a bad vote gets recorded by slashing protection and then rejected by every peer. The poisoned-lock exit gives supervisors a signal they can act on. The interchange-vector runner is now stricter in a way that catches regressions it used to hide. A few points are worth addressing.

Correctness

Item 1: Pre-Electra index is the duty's committee, not 0 (beacon_node/mod.rs, highest_attestation_index)

  • Phase0 through Deneb return Ok(0), justified by "this client only ever asks about committee 0".
  • Before EIP-7549, data.index must equal the validator's own committee_index. A validator in committee 3 that signs data with index = 0 produces an attestation that fails verification.
  • The new check therefore codifies a pre-existing limitation: pre-Electra attesting is only correct for committee-0 validators.
  • If pre-Electra signing is unsupported, say so explicitly and consider refusing those forks outright, rather than "succeeding" with 0.
  • Otherwise the expected value should be the duty's committee index, which means comparing for equality rather than checking a range.
  • The new test in attestation.rs builds a duty whose committee appears to be 3 (duty(..., 1337, 96, 3)) at slot 96. That is Phase0 on mainnet, and it expects a successful publish with index = 0. That is exactly the invalid case described above, so the test passes for the wrong reason.

Item 2: ForkName::Lean is reported as InconsistentResponse (mod.rs, highest_attestation_index)

  • This is a caller bug, not a node misbehaving.
  • Because InconsistentResponse is failover-able, FallbackBeaconNode will query every node, discard every answer, and surface AllBeaconNodesFailed. That points the operator at the beacon nodes.
  • In http.rs and mock.rs the check also runs only after the request has been made.
  • Suggestions:
    • Use a non-retryable internal/config error variant.
    • Optionally, validate the fork before issuing the request.

Item 3: ensure_usable only detects poisoning, not a lock stuck past the wait (lib.rs, around line 268; error.rs, around line 117)

  • The updated SlashingProtectionUnusable doc now lists "it stayed held past the wait" alongside poisoning, under "names a process to restart … Never retryable".
  • A permanently held lock (for example a hung fsync or a deadlock) produces that error on every duty. ensure_usable passes, so the client still idles indefinitely, which is the same failure mode this PR fixes for poisoning.
  • Either:
    • narrow the doc so the timeout case is described as transient, or
    • track consecutive timeouts and stop after N slots.

Item 4: Keystore import message claims more than the old one did (http_api/keystores.rs, around line 414)

  • The (true, true) arm now says "its keystore and definitions entry are back as they were". The previous text said only the definitions entry was restored.
  • Please confirm that rolled_back restores the keystore and password files, not just the definitions entry.
  • If it doesn't, this message is now actively misleading, and the test only asserts the string, not the on-disk keystore bytes.
    • A cheap guard would be to assert in that test that the keystore file content equals the original.

Minor

  • mod.rs, error text: "a {} attestation's is at most {highest_index}" reads awkwardly. Consider "index for a {fork} attestation must be at most {highest_index}".
  • Fallback test (fallback.rs, around line 517): it uses ForkName::Electra with slot 96, which is Phase0 on mainnet. This is harmless with the mock, but it is inconsistent with the sibling test's "slot 96 is long before gloas" reasoning. Consider using a slot whose fork matches the fork argument.
  • an_export_needs_no_writable_directory (slashing_protection/mod.rs): it passes vacuously when the tests run as root, which is common in CI containers. Consider skipping when geteuid() == 0. The restore-before-expect ordering is good.
  • panic_message / try_into_panic: looks correct. The comment's claim that only the operation can panic while holding the guard holds, since the try_lock/sleep loop runs before the guard exists.

Looks good

  • holds_impossible_source: restating the rule independently of the crate is the right call. Unparseable epochs fall through to "not forgiven", which is the safe default. source != 0 correctly excludes only the (0, 0) genesis shape.
  • Exhaustive match on ForkName: naming every fork means a post-Gloas fork has to make an explicit decision about its index rather than inheriting Gloas's.
  • Placement of ensure_usable: checking at the slot boundary, before any duty, is the right spot. Returning Err from run gives a non-zero exit for systemd or Kubernetes to restart on.

Automated review by Claude · claude-opus-5-5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review

The changes look solid and well-tested:

  • Fork-aware attestation index validation (crates/validator/src/beacon_node/mod.rs): Exhaustively matching on ForkName in highest_attestation_index (enforcing 0 pre-Gloas, <= 1 in Gloas, and rejecting Lean) and running it inside BeaconNodeApi::attestation_data ensures invalid indices trigger node failover before slashing protection records anything.
  • Poisoned slashing protection handling (crates/validator/src/slashing_protection/mod.rs, crates/validator/src/lib.rs): Checking ensure_usable at the slot boundary so the process exits for supervisor restart—and distinguishing the panicking access (including the extracted panic message) from subsequent accesses—is clean and accurate.
  • Keystore rollback reporting & EIP-3076 vector runner (crates/validator/src/http_api/keystores.rs, crates/validator/tests/interchange_vectors.rs): The 2x2 match on (before.contains_key(&pubkey), rolled_back.is_ok()) fixes the misleading "keystore rewritten" message on successful rollback, and scoping the allowed import failure in interchange_vectors.rs specifically to holds_impossible_source prevents masking regressions on other slashable inputs.

LGTM!


Automated review by Gemini · gemini-4-argon · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

No blocking issues found in the provided diff.

  • crates/validator/src/beacon_node/mod.rs: the fork-specific index bounds match the stated gossip rules, and the exhaustive ForkName match avoids silently inheriting Gloas behavior for future forks. Validation still occurs before slashing-protection recording or signing, and InconsistentResponse preserves failover behavior.
  • crates/validator/src/slashing_protection/mod.rs: JoinError::try_into_panic correctly distinguishes operation panics from cancellation, and panic_message handles the standard panic payload types safely. The once-per-slot poison check addresses the liveness problem without adding meaningful overhead.
  • crates/validator/tests/interchange_vectors.rs: narrowing the allowed import failure to interchanges actually containing an impossible source/target relationship is a useful test-hardening change; the genesis (0, 0) exception is handled correctly.
  • crates/validator/src/http_api/keystores.rs: the rollback message restructuring preserves the existing four cases and makes the successful-rollback message more accurate.

Optional coverage improvement: the tests exercise ensure_usable directly, but not its wiring in run (crates/validator/src/lib.rs, around line 268). A small duty-loop regression test could verify that poisoning actually terminates the client rather than merely returning the expected helper error.


Automated review by Kimi (Moonshot AI) · kimi-k3.1-pro · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Grok Code Review

The attestation-index gate and the poisoned-record exit are the right safety direction. A few spots are weaker than the comments claim.

  1. Gloas accepts index 1 with no payload check in highest_attestation_index (crates/validator/src/beacon_node/mod.rs, the new match). That is only safe if gloas gossip really allows index < 2 on this attestation. If beacon attestations are still index == 0 and 1 is a different message, a bad node can return 1, this client signs it, every peer drops it, and slashing protection has already spent the epoch. Confirm against the gloas gossip condition before shipping. Pre-Gloas Ok(0) is a client invariant ("we only query committee 0"), not the protocol max. Any attestation_data caller that starts passing a non-zero committee will reject a valid answer and burn the vote the same way.

  2. The service regression test does not reach the service check. MockBeaconNode::attestation_data now calls validate_attestation_data and returns Err before the service sees the data (mock.rs). an_index_invalid_at_the_slots_fork_is_refused_and_records_nothing still passes if the service-side call is deleted. The trait comment says a public BeaconNodeApi can be wrong and the service must refuse before recording. Use a stub that returns index 1 without validating, and assert nothing was signed or recorded.

  3. Keystore status text now overclaims rollback (http_api/keystores.rs, (true, true) arm). The previous message said the keystore was rewritten even when rolled_back succeeded; this diff only changes the string to "keystore and definitions entry are back as they were" and the test only checks that string. If rolled_back restores the definitions entry and not the keystore bytes and password, operators will not look for a rewritten key. Assert the on-disk keystore and password match the pre-import files in that test.

  4. SlashingProtectionUnusable docs are broader than ensure_usable. The variant text (error.rs) says a lock held past the wait also makes every duty fail until the duty loop exits. ensure_usable only checks is_poisoned(), so a timeout is still per-duty and the next slot can succeed. Callers that treat the whole variant as fatal will restart on a slow lock. Keep the permanent-exit wording on the poison path only.

  5. holds_impossible_source can forgive the wrong failure (tests/interchange_vectors.rs). return Ok(()) ends the whole case when the file merely contains an impossible source, even if import failed earlier for another reason. Epochs are read only as strings, so a numeric epoch will not match. Safer than the old flag-only skip, but a step that fails for a parse error and also contains one bad attestation will go green. Skip only when the import error is the impossible-source refusal.

ensure_usable once a slot, the "this access" vs "earlier access" panic wording, and failing over an out-of-range index before signing are sound. panic_message covers &str and String. No change needed there.


Automated review by Grok (xAI) · grok-4.7 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 DeepSeek Code Review

Overall this looks solid. The attestation index validation is correctly layered in the node implementations, so FallbackBeaconNode can fail over before slashing protection records a target, and the slashing-protection poison handling is a real safety improvement.

A few minor points:

  • crates/validator/src/beacon_node/mod.rs, in validate_attestation_data’s new error message:

    "attestation data for slot {slot} carries index ..., and a {} attestation's is at most ..."

    is missing the word “index”: it should be something like

    "... and a {fork} attestation's index is at most {highest_index}"

    This is purely cosmetic but affects operator logs.

  • crates/validator/src/beacon_node/mod.rs, test a_nonzero_index_is_refused_before_gloas uses:

    ForkName::ALL
        .iter()
        .copied()
        .filter(|fork| *fork < ForkName::Gloas)

    This relies on enum ordering and the contents of ALL. If Lean is included and ordered before Gloas, the test will incorrectly assert that a Lean index is valid. Prefer an explicit list of Beacon Chain forks or a helper that matches the same forks as highest_attestation_index rather than relying on Ord.

  • crates/validator/src/beacon_node/mod.rs, highest_attestation_index returns Error::InconsistentResponse for ForkName::Lean. That makes a local caller/programming error look like an inconsistent node response and will cause failover to try every node. Consider a dedicated “unsupported fork” error or debug assertion; failover cannot fix this case anyway.

  • crates/validator/src/lib.rs, ensure_usable is checked once per slot at the boundary. That is fine for the intended supervisor-restart behavior, but a panic occurring mid-slot will not stop the client until the next slot. This is documented, so likely intentional.

No consensus-layer correctness issues found in this diff; the changes are validator-client validation and slashing-protection lifecycle improvements.


Automated review by DeepSeek · deepseek-v4-pro · custom prompt

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nothing blocking. The exit path in lib.rs is the one I'd fix before merging; the rest are smaller.

// Kubernetes the restart the refusals ask for. Asked once a slot, at
// the boundary, which is soon enough: in between, the record refuses
// every duty on its own.
if let Err(err) = slashing_protection::ensure_usable(&protection) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Returning here drops the runtime, and with it any keymanager request in flight. An import that has already written its keystore files and enabled definitions entry is then parked on slashing_protection::blocking(...).await (always a spawn_blocking, so a real yield) and is dropped there, so undo_import never runs. The next start finds a loaded key with no record and refuses the whole client, and a supervisor restarts it into the same refusal every time.

The window is narrow (a poisoned record plus an import in flight at the boundary), and a crash in the same spot already leaves this state by design. But this exit is deliberate, so it can be graceful: signal shutdown from where the poisoning is observed (e.g. a CancellationToken cancelled in blocking on the panic) and let the keymanager finish or undo before run returns. That would also stop the client at once rather than up to a slot later.

)),
// The undo failed as well, so what this request wrote
// over the key's files and entry may still be there.
(true, false) => KeyStatus::error(format!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

undo_import saves the definitions first and only then restores the keystore and password files, so rolled_back being Err doesn't mean the entry stayed. If a put_back failed, the entry is back, and it's the keystore and password that are still the new ones, which this message doesn't say. Same for the (false, false) arm: if the entry was removed but the files weren't, they're orphans and nothing is refused at the next start. Having undo_import report which step failed would let both messages say what's actually on disk.

// say, a duplicated pubkey or a surround, with that case's blocks
// and attestations never attempted.
(false, true)
if step.contains_slashable_data && holds_impossible_source(&step.interchange) =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This forgives any refusal of an interchange that happens to contain an impossible source, not a refusal for it. If the importer started refusing something else (a duplicated pubkey, a surround) in one of the single_validator_source_greater_than_target* cases, the case would still return Ok with its blocks and attestations never tried, which is the masking this change sets out to remove. Matching the error as well (it says "is at or after its target epoch", slashing_protection/file.rs:178) would close it.

/// still signed. `context()` is mainnet's schedule, where slot 96 is long
/// before gloas, so 1 is not an index a vote there can carry.
#[tokio::test]
async fn an_index_invalid_at_the_slots_fork_is_refused_and_records_nothing() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This error comes from MockBeaconNode::attestation_data, which runs validate_attestation_data itself (mock.rs:602), not from the re-check in attest at line 138. Deleting that line, or passing it the wrong fork, keeps every test green. Covering it needs a BeaconNodeApi impl that hands the data back unchecked.

ForkName::Gloas => Ok(1),
// Not a Beacon Chain fork: `Config::fork_at_epoch` never names it, so
// a caller that does is wrong, and nothing is signed for it.
ForkName::Lean => Err(crate::error::Error::InconsistentResponse(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can't happen today, but if it did: this fires after the HTTP request, as InconsistentResponse, which is retryable and blamed on the node, so failover tries every node and reports them all as failed. A local bug would read as a beacon-node outage every slot. Checking the fork before the request, or unreachable! here, would report it as what it is.

propose(&mut protection, 1, 97).expect("propose");
drop(protection);

let read_only = std::fs::Permissions::from_mode(0o500);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing the running_as_root() skip that a_failed_write_refuses_and_changes_nothing uses (line 1211). Under root the 0o500 mode restricts nothing, so in a root container this passes even if export starts writing to the directory.

/// restart. A poisoned lock is a bug, never a disk, and it stays poisoned
/// for the life of the process, so every duty is refused until it is
/// restarted. Never retryable.
/// for the life of the process, so every duty is refused from then on

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"Every duty is refused" is broader than what happens: only attestations and proposals consult the record. Sync committee messages and contributions, aggregates and PTC votes keep signing until the next boundary, then stop with the process. Same wording in ensure_usable and docs/cli.md.

/// it, and the `Err` is one failover moves past to a node that may answer
/// properly. Lodestar's validator client refuses the same before signing
/// (`validateAttestationDuty`).
pub fn validate_attestation_data(slot: Slot, fork: ForkName, data: &AttestationData) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: fork comes from the caller and is never checked against slot, so the index bound is only as right as the caller's fork. Every call site derives it correctly today, but the new failover test already passes ForkName::Electra for slot 96, which is phase0 on mainnet. A debug_assert against the schedule, or deriving the fork here, would remove the footgun.

/// forgives along with it.
fn holds_impossible_source(interchange: &serde_json::Value) -> bool {
let epoch =
|value: &serde_json::Value| value.as_str().and_then(|text| text.parse::<u64>().ok());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: only quoted epochs are read here, while the importer also takes bare integers (quoted_or_bare). The vectors quote them per EIP-3076, so this is theoretical, but the restated rule covers less than the one it mirrors.

/// A panic's message, when it carried one: `panic!` with a literal gives a
/// `&str` and with arguments a `String`. Anything else was raised with
/// `panic_any` and has no text to show.
fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: the same downcast as guarded in crates/net/p2p/src/beacon/verdict.rs (&str, then String, then a fallback), with a different fallback string.

This branch has not been deployed

No deployments
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.

2 participants