feat(raft-commit-safety): (lastLogTerm,lastLogIndex) vote+prevote freshness (tasks 1.3, 4.1-4.2, 5.1-5.2, 6.2)

logOk (raft.tla:285, closes G6): local_last_log_term = log_len>0 ? on_get_entry_term(log_len):0;
logOk = cand_last_log_term > local || (== && cand_last_log_index >= log_len). Lexicographic:
term dominates, tie decided by index. Shared raft_log_ok helper.

Vote (4.2): logOk deny inserted after stale-term/UpdateTerm/role-gate/one-vote-per-term and
BEFORE on_vote_eligible (denies even when the app gate would grant); persist-before-grant intact.
PreVote (5.2): identical logOk, fully side-effect-free (no term/vote/role/commit mutation, no
on_persist). Signatures extended (1.3) on raft_on_vote_request/pre_vote_request; all 39 lib call
sites + sim routing updated; sim captures the candidate's send-time last-log tuple (raft_sim_rpc_t).
Empty-log call sites pass (0,0) → logOk true → existing outcomes preserved. 9 freshness tests.

NOTE: controller cluster.c vote callers still use the old signatures and will NOT compile — an
accepted, design-documented revalidation trigger; the controller adapter plumbing is owned by the
downstream ha-term-safety spec (to be implemented next). Library suite ASAN green (239 tests).
This commit is contained in:
Rongsong Shen 2026-06-22 00:14:26 +00:00
parent 195f660f9a
commit 56485e2fa3
5 changed files with 449 additions and 62 deletions

View file

@ -53,6 +53,14 @@ static int sim_send_vote_request(void *ctx, int peer_idx, uint64_t term)
rpc->src = node->idx;
rpc->dst = peer_idx;
rpc->term = term;
/* Capture the CANDIDATE's last-log tuple at SEND time from its own in-memory
* log (the library holds no entry bytes): last_log_index == log_count,
* last_log_term == term of the tail entry (0 when the log is empty). This
* feeds the (lastLogTerm, lastLogIndex) freshness gate at delivery. */
rpc->last_log_index = (uint64_t)node->log_count;
rpc->last_log_term = (node->log_count > 0)
? node->log[node->log_count - 1].term
: 0;
return 0;
}
@ -319,9 +327,12 @@ void raft_sim_deliver_pending(raft_sim_cluster_t *c)
if (c->nodes[src].partition || c->nodes[dst].partition)
continue;
/* Deliver vote request to dst */
/* Deliver vote request to dst, passing the candidate's send-time last-log
* tuple so dst evaluates the (lastLogTerm, lastLogIndex) freshness gate. */
uint64_t reply_term = 0;
int granted = raft_on_vote_request(&c->nodes[dst].state, src, term,
rpc->last_log_term,
rpc->last_log_index,
&reply_term);
/* Route reply back to src */