FAILURE MAP
← Case archive

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.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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