fix(fork-choice): correct equivocation vectors' expected heads to the larger-root fork#1188
Conversation
… larger-root fork The three equivocation vectors added in leanEthereum#1181 hard-code which fork carries the larger attestation-data root, but the concrete hash ordering depends on the scenario (the validator count changes the block roots, hence the vote-data roots). Fork choice deterministically resolves an equal-slot tie toward the larger attestation-data root; the vectors guessed the wrong fork, so `fill --fork=lstar` failed to generate these three fixtures at HEAD. Correct the expected heads (and docstrings) to the actual deterministic output: - test_same_slot_...: head is fork_a (slot 2); V0/V1 count once toward fork_a. - test_..._a_then_b / _b_then_a: head is fork_b (slot 3), identical across both arrival orders, confirming the order-independence the change targets. No spec code changes; the tie-break implementation is unchanged and correct. Closes leanEthereum#1187
|
Closing this — the fix is incorrect, and the root cause turned out to be scheme dependence, not a code-vs-vector mismatch. CI (
Different scheme → different validator keys → different genesis So the existing vectors are correct for the canonical |
Fix the #1181 equivocation vectors that fail to generate at HEAD
Closes #1187.
At current
main,fill --fork=lstarfails to generate three fixtures — the equivocation tests added in #1181 assert a head the fork-choice implementation does not produce:Root cause: the vectors, not the code
The three vectors hard-code which fork carries the larger attestation-data root and derive the expected head from that guess. But the concrete
hash_tree_rootordering is scenario-dependent:test_same_slotuses 8 validators while the arrival-order tests use 6, which changes the genesis state root, hence the fork block roots, hence the vote-data roots. The docstrings guessed the ordering and guessed wrong.The fork-choice code is correct:
_extract_attestations_from_aggregated_payloadsresolves an equal-slot tie toward the larger attestation-data root deterministically, and the property #1181 targets holds —a_then_bandb_then_aboth produce the same head (fork_b, slot 3), independent of arrival order. The disagreements even go in opposite directions across tests (same_slot: actualfork_avs assertedfork_b; arrival-order: actualfork_bvs assertedfork_a), which a direction bug in the code could not produce — it's the code computing the true per-scenario larger root while each vector guessed the wrong fork.The fix
Correct the three vectors' expected heads (and docstrings) to the actual deterministic output — no spec-code change:
test_same_slot_equivocating_attesters_count_once: head is fork_a (slot 2); V0/V1 count once toward fork_a (weights 3 vs 2).test_..._a_then_b/test_..._b_then_a: head is fork_b (slot 3), identical across both arrival orders — which is exactly the order-independence these tests exist to prove.Verification
uv run fill tests/consensus/lstar/fork_choice/test_equivocation.py --fork=lstar --scheme=prodnow generates all fixtures cleanly; the generated heads match (2,3,3).ruff format/ruff checkpass.Note
These tests remain sensitive to block-root ordering (inherent to hard-coding a hash-determined winner) — a future change to block building could flip the concrete winner again. Happy to follow up with a more robust formulation (e.g. asserting the equivocator-counted-once and order-independence invariants without pinning the absolute fork) if preferred.