FA-096 / Distributed coordination / Open access
Election votes prefer a longer but older log · case 01
A candidate with more entries from an older term receives a vote over the voter's newer history.
ROOT CAUSE
Log length is compared without prioritizing the last log term, and vote state is not scoped to election term.
VERIFIED REPAIR
Advance term and clear prior-term vote first; compare (last log term,last log index) lexicographically before granting.
Unsuccessful approach: Fixing log comparison while keeping a vote across newer election terms prevents legitimate elections.
Case contract
State is [current term,voted candidate or None], local log is [last term,last index], and requests are [election term,candidate,last log term,last index]. Newer terms clear the vote even if the candidate is rejected. Grant only in current term to an up-to-date log and an unclaimed or same vote. Return [term,voted candidate,grant decisions]. This is a voting-rule model, not a consensus implementation.
Why this case matters
Models two interacting durable election invariants: freshness by log term and one candidate per election term, including term advancement on rejected requests.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(state, local_log, requests):
term, voted = state
grants = []
for request_term, candidate, last_term, last_index in requests:
if request_term > term:
term, voted = request_term, None
allowed = request_term == term and last_index >= local_log[1] and voted in (None, candidate)
if allowed:
voted = candidate
grants.append(allowed)
return [term, voted, grants]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
t = N+10
check('longer old-term log rejected', solve([t, None], [N+2, N], [[t, 'a', N+1, 100*N]]), [t, None, [False]])
check('new term clears old vote', solve([t, 'old'], [N, N], [[t+1, 'new', N, N]]), [t+1, 'new', [True]])
check('newer log term beats length', solve([t, None], [N, 100*N], [[t, 'a', N+1, 1]]), [t, 'a', [True]])
check('one candidate per term', solve([t, None], [N, N], [[t, 'a', N, N], [t, 'b', N, N]]), [t, 'a', [True, False]])
check('same candidate retransmission', solve([t, 'a'], [N, N], [[t, 'a', N, N]]), [t, 'a', [True]])
check('old election term ignored', solve([t, None], [N, N], [[t-1, 'a', N+2, N]]), [t, None, [False]])
check('rejected new term still advances', solve([t, 'old'], [N+2, N], [[t+1, 'a', N, N]]), [t+1, None, [False]])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| longer old-term log rejected | [11, 'a', [True]] | [11, None, [False]] | Failed |
| new term clears old vote | [12, 'new', [True]] | [12, 'new', [True]] | Passed |
| newer log term beats length | [11, None, [False]] | [11, 'a', [True]] | Failed |
| one candidate per term | [11, 'a', [True, False]] | [11, 'a', [True, False]] | Passed |
| same candidate retransmission | [11, 'a', [True]] | [11, 'a', [True]] | Passed |
| old election term ignored | [11, None, [False]] | [11, None, [False]] | Passed |
| rejected new term still advances | [12, 'a', [True]] | [12, None, [False]] | Failed |
SHA-256 / 16a4485ab58a73a1a2166fe677231f88812ee7887d7cbbe2dba40c74d1264231
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(state, local_log, requests):
term, voted = state
grants = []
for request_term, candidate, last_term, last_index in requests:
term = max(term, request_term)
allowed = request_term == term and (last_term, last_index) >= tuple(local_log) and voted in (None, candidate)
if allowed:
voted = candidate
grants.append(allowed)
return [term, voted, grants]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
t = N+10
check('longer old-term log rejected', solve([t, None], [N+2, N], [[t, 'a', N+1, 100*N]]), [t, None, [False]])
check('new term clears old vote', solve([t, 'old'], [N, N], [[t+1, 'new', N, N]]), [t+1, 'new', [True]])
check('newer log term beats length', solve([t, None], [N, 100*N], [[t, 'a', N+1, 1]]), [t, 'a', [True]])
check('one candidate per term', solve([t, None], [N, N], [[t, 'a', N, N], [t, 'b', N, N]]), [t, 'a', [True, False]])
check('same candidate retransmission', solve([t, 'a'], [N, N], [[t, 'a', N, N]]), [t, 'a', [True]])
check('old election term ignored', solve([t, None], [N, N], [[t-1, 'a', N+2, N]]), [t, None, [False]])
check('rejected new term still advances', solve([t, 'old'], [N+2, N], [[t+1, 'a', N, N]]), [t+1, None, [False]])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| longer old-term log rejected | [11, None, [False]] | [11, None, [False]] | Passed |
| new term clears old vote | [12, 'old', [False]] | [12, 'new', [True]] | Failed |
| newer log term beats length | [11, 'a', [True]] | [11, 'a', [True]] | Passed |
| one candidate per term | [11, 'a', [True, False]] | [11, 'a', [True, False]] | Passed |
| same candidate retransmission | [11, 'a', [True]] | [11, 'a', [True]] | Passed |
| old election term ignored | [11, None, [False]] | [11, None, [False]] | Passed |
| rejected new term still advances | [12, 'old', [False]] | [12, None, [False]] | Failed |
SHA-256 / 34c0e9c1c9d9e800124c3603845c03d9a65bda6762996c0ee2668a30b79bdacf
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(state, local_log, requests):
term, voted = state
grants = []
for request_term, candidate, last_term, last_index in requests:
if request_term > term:
term, voted = request_term, None
allowed = request_term == term and (last_term, last_index) >= tuple(local_log) and voted in (None, candidate)
if allowed:
voted = candidate
grants.append(allowed)
return [term, voted, grants]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
t = N+10
check('longer old-term log rejected', solve([t, None], [N+2, N], [[t, 'a', N+1, 100*N]]), [t, None, [False]])
check('new term clears old vote', solve([t, 'old'], [N, N], [[t+1, 'new', N, N]]), [t+1, 'new', [True]])
check('newer log term beats length', solve([t, None], [N, 100*N], [[t, 'a', N+1, 1]]), [t, 'a', [True]])
check('one candidate per term', solve([t, None], [N, N], [[t, 'a', N, N], [t, 'b', N, N]]), [t, 'a', [True, False]])
check('same candidate retransmission', solve([t, 'a'], [N, N], [[t, 'a', N, N]]), [t, 'a', [True]])
check('old election term ignored', solve([t, None], [N, N], [[t-1, 'a', N+2, N]]), [t, None, [False]])
check('rejected new term still advances', solve([t, 'old'], [N+2, N], [[t+1, 'a', N, N]]), [t+1, None, [False]])
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| longer old-term log rejected | [11, None, [False]] | [11, None, [False]] | Passed |
| new term clears old vote | [12, 'new', [True]] | [12, 'new', [True]] | Passed |
| newer log term beats length | [11, 'a', [True]] | [11, 'a', [True]] | Passed |
| one candidate per term | [11, 'a', [True, False]] | [11, 'a', [True, False]] | Passed |
| same candidate retransmission | [11, 'a', [True]] | [11, 'a', [True]] | Passed |
| old election term ignored | [11, None, [False]] | [11, None, [False]] | Passed |
| rejected new term still advances | [12, None, [False]] | [12, None, [False]] | Passed |
SHA-256 / b2cfee3b3e4f5c92611279b43ba6619c8fd4481fd2600c236519bcc448d3f558
Verification & scope
This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:36:50.454387+00:00.
Case digest / c5dc01051c335042f4298886033411be8f74a4550649d3435ef314119c363588