diff --git a/.kiro/specs/raft-commit-safety/tasks.md b/.kiro/specs/raft-commit-safety/tasks.md index f38b077..88b3ad0 100644 --- a/.kiro/specs/raft-commit-safety/tasks.md +++ b/.kiro/specs/raft-commit-safety/tasks.md @@ -23,7 +23,7 @@ - _Boundary: raft.h_ - _Depends: 1.1_ -- [ ] 1.3 Declare `raft_advance_commit` and extend the vote/PreVote signatures with last_log_term/last_log_index +- [x] 1.3 Declare `raft_advance_commit` and extend the vote/PreVote signatures with last_log_term/last_log_index - Declare `int raft_advance_commit(raft_state_t *s)` - Extend `raft_on_vote_request` and `raft_on_pre_vote_request` to accept `cand_last_log_term` and `cand_last_log_index` (the chosen "library owns logOk" plumbing — Option B in design.md) - Update the header doc comments to describe the new logOk gate and the commit-advance action @@ -74,7 +74,7 @@ ## 4. (lastLogTerm, lastLogIndex) vote freshness (raft.tla:285, closes G6) -- [ ] 4.1 RED: binding-vote freshness tests +- [x] 4.1 RED: binding-vote freshness tests - Test: candidate with stale `last_log_term` denied even when `last_log_index` is greater; candidate with fresher `last_log_term` granted with a smaller `last_log_index`; tie on term grants only when `cand_last_log_index >= log_len`; the voter's own last-log term/index come from `on_get_entry_term(log_len)`/`log_len` (term 0 when empty) - Test: `!logOk` denies even when `on_vote_eligible` would grant (logOk applied BEFORE the application gate); persist-before-grant unchanged on the grant path - Observable completion: tests compile and FAIL @@ -82,7 +82,7 @@ - _Boundary: test_raft.c_ - _Depends: 1.3_ -- [ ] 4.2 GREEN: implement the logOk gate in `raft_on_vote_request` +- [x] 4.2 GREEN: implement the logOk gate in `raft_on_vote_request` - Compute `local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0`; `logOk = (cand_last_log_term > local_last_log_term) || (cand_last_log_term == local_last_log_term && cand_last_log_index >= log_len)` - Insert the logOk deny BEFORE `on_vote_eligible`, after the existing stale-term/UpdateTerm/one-vote-per-term steps; leave persist-before-grant and all existing invariants in place - Observable completion: 4.1 tests pass; no regression in existing vote tests; the logOk block is clearly before `on_vote_eligible` in `raft.c` @@ -92,7 +92,7 @@ ## 5. (lastLogTerm, lastLogIndex) PreVote freshness (raft.tla:285, side-effect-free) -- [ ] 5.1 RED: PreVote freshness tests (identical rule, no side effects) +- [x] 5.1 RED: PreVote freshness tests (identical rule, no side effects) - Test: `raft_on_pre_vote_request` applies the SAME `(lastLogTerm, lastLogIndex)` logOk as the binding vote; a stale tuple is denied; the existing stale-term deny and `on_vote_eligible` gate remain in force - Test: across grant and deny, no mutation of `term`/`voted_for`/`voted_for_term`/`role`/`commit_index` and no `on_persist` call (PreVote invariant) - Observable completion: tests compile and FAIL @@ -100,7 +100,7 @@ - _Boundary: test_raft.c_ - _Depends: 1.3_ -- [ ] 5.2 GREEN: implement the logOk gate in `raft_on_pre_vote_request` +- [x] 5.2 GREEN: implement the logOk gate in `raft_on_pre_vote_request` - Apply the identical logOk predicate (factored to match 4.2) before/with the existing eligibility gate; mutate nothing; no `on_persist` - Observable completion: 5.1 tests pass; no regression in existing PreVote tests; the predicate matches the binding-vote path exactly - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ @@ -118,7 +118,7 @@ - _Boundary: raft_sim.c_ - _Depends: 3.2_ -- [ ] 6.2 Plumb `(last_log_term, last_log_index)` through the sim vote/PreVote routing +- [x] 6.2 Plumb `(last_log_term, last_log_index)` through the sim vote/PreVote routing - Update the sim's vote and PreVote delivery to read the candidate's last-log `(term, index)` from its in-memory log (`term at log_len` / `log_len`) and pass them to the extended `raft_on_vote_request` / `raft_on_pre_vote_request` - Observable completion: existing election sim tests still pass with the extended signatures; a node with a stale last-log term is denied a vote it would previously have won on LSN alone - _Requirements: 6.5, 3.2, 4.2_ diff --git a/src/lib/raft/raft.c b/src/lib/raft/raft.c index b89348e..2e1f2de 100644 --- a/src/lib/raft/raft.c +++ b/src/lib/raft/raft.c @@ -148,7 +148,25 @@ int raft_start_election(raft_state_t *s) return RAFT_OK; } +/* (lastLogTerm, lastLogIndex) freshness predicate (raft.tla + * HandleRequestVoteRequest logOk, line 285, closes G6). The voter's own last-log + * tuple is (on_get_entry_term(log_len), log_len) — term 0 when the log is empty, + * matching the on_get_entry_term 0-means-absent sentinel. The library owns this + * decision (design "Option B: library owns logOk") and applies it identically on + * the binding vote and the PreVote eligibility paths. */ +static int raft_log_ok(const raft_state_t *s, uint64_t cand_last_log_term, + uint64_t cand_last_log_index) +{ + uint64_t local_last_log_term = + (s->log_len > 0) ? s->cbs->on_get_entry_term(s->ctx, s->log_len) : 0; + return (cand_last_log_term > local_last_log_term) || + (cand_last_log_term == local_last_log_term && + cand_last_log_index >= s->log_len); +} + int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term, + uint64_t cand_last_log_term, + uint64_t cand_last_log_index, uint64_t *reply_term) { /* Step 1: stale term — deny immediately, no state change */ @@ -208,7 +226,18 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term, /* same candidate — fall through to re-persist and re-grant */ } - /* Step 5: application-level policy gate (freshness, log lsn, etc.) */ + /* Step 5: (lastLogTerm, lastLogIndex) freshness gate (raft.tla + * HandleRequestVoteRequest logOk, 285, closes G6). This precedes the + * application gate: a !logOk denies regardless of on_vote_eligible (Req 3.4), + * without weakening the stale-term / one-vote-per-term / persist-before-grant + * invariants above (Req 3.5). */ + if (!raft_log_ok(s, cand_last_log_term, cand_last_log_index)) { + if (s->cbs->on_trace) + s->cbs->on_trace(s->ctx, "vote_denied_log_stale", NULL); + return RAFT_DENY; + } + + /* Step 6: application-level policy gate (freshness, log lsn, etc.) */ if (s->cbs->on_vote_eligible != NULL) { if (s->cbs->on_vote_eligible(s->ctx, from_idx, term) == 0) { if (s->cbs->on_trace) @@ -217,7 +246,7 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term, } } - /* Step 6: persist before granting — durability guarantee */ + /* Step 7: persist before granting — durability guarantee */ if (s->cbs->on_persist(s->ctx, term, from_idx) != 0) { /* Persist failed — deny without updating voted_for */ return RAFT_DENY; @@ -231,15 +260,18 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term, return RAFT_GRANT; } -int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term) +int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term, + uint64_t cand_last_log_term, + uint64_t cand_last_log_index) { /* PreVote is a side-effect-free straw poll: it answers "would you vote for * me in cand_term?" WITHOUT changing any persistent state. Unlike * raft_on_vote_request, this function never adopts cand_term (even when * higher — that adoption is the very thing PreVote exists to prevent), never * touches term / voted_for / voted_for_term / role, and never calls - * on_persist. The dispatcher applies the freshness floor before this call, - * exactly as on the real-vote path. */ + * on_persist. The library applies the SAME (lastLogTerm, lastLogIndex) + * logOk freshness gate as the real-vote path (via raft_log_ok), so a stale + * candidate is rejected here exactly as it would be in a binding vote. */ /* Step 1: stale term — deny (mirrors raft_on_vote_request Step 1). A * candidate proposing a term we already passed could never win a real @@ -251,10 +283,21 @@ int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term) return RAFT_DENY; } - /* Step 2: application-level eligibility gate — reuse the EXISTING + /* Step 2: (lastLogTerm, lastLogIndex) freshness gate — the IDENTICAL logOk + * as the binding vote path (raft.tla 285, closes G6 for PreVote, Req 4.1). + * The library owns this predicate (the dispatcher no longer applies freshness + * before this call). Side-effect-free: a deny mutates nothing and never + * persists (Req 4.4). */ + if (!raft_log_ok(s, cand_last_log_term, cand_last_log_index)) { + if (s->cbs->on_trace) + s->cbs->on_trace(s->ctx, "pre_vote_denied_log_stale", NULL); + return RAFT_DENY; + } + + /* Step 3: application-level eligibility gate — reuse the EXISTING * on_vote_eligible callback (primary-deny / fresh-heartbeat stickiness / - * catch-up deny), identical to raft_on_vote_request Step 5. If NULL, every - * Raft-eligible candidate is granted. */ + * catch-up deny), identical to raft_on_vote_request's application gate. If + * NULL, every Raft-eligible candidate is granted. */ if (s->cbs->on_vote_eligible != NULL) { if (s->cbs->on_vote_eligible(s->ctx, from_idx, cand_term) == 0) { if (s->cbs->on_trace) diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index 3774852..36c51a2 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -244,12 +244,29 @@ int raft_start_election(raft_state_t *s); /* * raft_on_vote_request: process an incoming RequestVote RPC. - * Applies Raft invariants (term comparison, one-vote-per-term). - * If invariants pass, calls on_vote_eligible to allow application policy. + * Applies Raft invariants (term comparison, one-vote-per-term), then the + * (lastLogTerm, lastLogIndex) freshness gate (raft.tla HandleRequestVoteRequest + * logOk, line 285, closes G6), then the application policy gate. + * + * The library owns the logOk decision (design "Option B: library owns logOk"): + * local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0 + * logOk = (cand_last_log_term > local_last_log_term) + * || (cand_last_log_term == local_last_log_term + * && cand_last_log_index >= log_len) + * The candidate advertises (cand_last_log_term, cand_last_log_index) in its wire + * RequestVote; the caller decodes and passes them here. + * + * Step order: stale-term deny → UpdateTerm/step-down → role gate → + * one-vote-per-term → logOk gate (NEW; precedes the application gate, so a + * !logOk denies regardless of on_vote_eligible) → on_vote_eligible → + * persist-before-grant. + * * Fires on_persist before granting. * Returns RAFT_GRANT (1) or RAFT_DENY (0); sets *reply_term to current term. */ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term, + uint64_t cand_last_log_term, + uint64_t cand_last_log_index, uint64_t *reply_term); /* @@ -301,13 +318,17 @@ int raft_begin_pre_vote(raft_state_t *s); /* * raft_on_pre_vote_request: answer an incoming PreVote probe. - * Denies if cand_term < s->term; otherwise returns on_vote_eligible(ctx, - * from_idx, cand_term) (granting when the gate is NULL). Never touches term, - * voted_for, role, or fires on_persist. Freshness is applied by the - * dispatcher before this call (same contract as the real vote path). + * Denies if cand_term < s->term; then applies the IDENTICAL (lastLogTerm, + * lastLogIndex) logOk as raft_on_vote_request (the library owns the predicate — + * it is NO LONGER applied by the dispatcher before this call), then consults + * on_vote_eligible (granting when the gate is NULL). Mutates NOTHING — never + * touches term, voted_for, voted_for_term, role, or commit_index, and never + * fires on_persist — preserving the PreVote side-effect-free invariant. * Returns 1 = grant, 0 = deny. */ -int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term); +int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term, + uint64_t cand_last_log_term, + uint64_t cand_last_log_index); /* * raft_on_pre_vote_reply: tally a reply to our PreVote. @@ -423,11 +444,17 @@ void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap); /* Upper bound on buffered AppendEntries awaiting delivery (mirrors pending[]). */ #define RAFT_SIM_MAX_AE 64 -/* Buffered RPC entry: one pending vote-request to route */ +/* Buffered RPC entry: one pending vote-request to route. + * last_log_term/last_log_index capture the CANDIDATE's advertised last-log tuple + * at SEND time (mirroring raft_sim_ae_t.leader_term) so the (lastLogTerm, + * lastLogIndex) freshness gate (raft-commit-safety) is evaluated against the + * candidate's send-epoch log, not a value re-read at delivery. */ typedef struct { int src; int dst; uint64_t term; + uint64_t last_log_term; /* candidate's last-log term at send (0 if empty) */ + uint64_t last_log_index; /* candidate's last-log index at send (== log_count) */ } raft_sim_rpc_t; /* One in-memory log entry: term stamp + fixed-size payload bytes. diff --git a/src/lib/raft/raft_sim.c b/src/lib/raft/raft_sim.c index 15d0d03..8f5a45b 100644 --- a/src/lib/raft/raft_sim.c +++ b/src/lib/raft/raft_sim.c @@ -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 */ diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 5d76a44..7937381 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -130,6 +130,13 @@ typedef struct { int vote_eligible_return_value; /* configurable: 1=grant, 0=deny; default 1 */ int vote_eligible_call_count; + /* on_get_entry_term mock (raft-commit-safety freshness, task 4.1/5.1). + * The voter's local last-log term is returned for index == get_entry_term_log_len; + * any other index returns 0 (absent). Default 0 keeps empty-log scenarios at the + * pre-existing local_last_log_term == 0. */ + uint64_t local_last_log_term; /* term reported at index == get_entry_term_log_len */ + uint64_t get_entry_term_log_len; /* the index treated as the local last log */ + /* on_trace mock */ int trace_call_count; char last_trace_event[64]; @@ -241,6 +248,19 @@ static int mock_vote_eligible(void *ctx, int from_idx, uint64_t term) return g_mock.vote_eligible_return_value; } +/* Configurable local-log term reporter for freshness tests (task 4.1/5.1). + * Returns g_mock.local_last_log_term for the configured last-log index, 0 + * otherwise. Defaults (both 0) reproduce the previous always-0 behaviour, so + * empty-log call sites are unaffected. */ +static uint64_t mock_get_entry_term(void *ctx, uint64_t index) +{ + (void)ctx; + if (g_mock.get_entry_term_log_len != 0 && + index == g_mock.get_entry_term_log_len) + return g_mock.local_last_log_term; + return 0; +} + static void mock_trace(void *ctx, const char *event, const char *detail) { (void)ctx; (void)detail; @@ -940,7 +960,7 @@ static void test_vote_req_stale_term(void) TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); - int rc = raft_on_vote_request(&s, 1, 3, &reply_term); + int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -981,7 +1001,7 @@ static void test_vote_req_higher_term_updates_state(void) s.voted_for = 0; /* voted for self */ s.voted_for_term = 2; - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); /* Term upgrade must happen: term updated, role stepped down */ TEST_ASSERT_EQUAL_UINT64(5, s.term); @@ -1021,7 +1041,7 @@ static void test_vote_req_same_term_already_voted_different(void) s.voted_for = 1; s.voted_for_term = 5; - int rc = raft_on_vote_request(&s, 2, 5, &reply_term); + int rc = raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1058,7 +1078,7 @@ static void test_vote_req_same_term_idempotent_same_candidate(void) s.voted_for = 1; s.voted_for_term = 5; - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1089,7 +1109,7 @@ static void test_vote_req_leader_denies(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.role = RAFT_LEADER; - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1124,7 +1144,7 @@ static void test_vote_req_on_vote_eligible_denies(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1161,7 +1181,7 @@ static void test_vote_req_on_vote_eligible_grants(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1198,7 +1218,7 @@ static void test_vote_req_on_vote_eligible_null(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1228,7 +1248,7 @@ static void test_vote_req_persist_fails(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -1257,14 +1277,14 @@ static void test_vote_req_reply_term_always_set(void) reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 7)); reply_term = 0; - raft_on_vote_request(&s, 1, 3, &reply_term); + raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal current term=7 */ /* Path 2: term upgrade + grant */ reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 3)); reply_term = 0; - raft_on_vote_request(&s, 1, 7, &reply_term); + raft_on_vote_request(&s, 1, 7, 0, 0, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal upgraded term=7 */ /* Path 3: already voted for different — deny */ @@ -1272,7 +1292,7 @@ static void test_vote_req_reply_term_always_set(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); s.voted_for = 1; s.voted_for_term = 5; reply_term = 0; - raft_on_vote_request(&s, 2, 5, &reply_term); + raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */ /* Path 4: persist fails — deny */ @@ -1281,7 +1301,7 @@ static void test_vote_req_reply_term_always_set(void) g_mock.persist_return_value = -1; TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); reply_term = 0; - raft_on_vote_request(&s, 1, 5, &reply_term); + raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */ } @@ -4098,7 +4118,7 @@ static void test_proto_A3_late_election_denied(void) /* Now node 2 tries to request a vote from node 1 at the same term=1 */ uint64_t reply_term = 0; - int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, &reply_term); + int rc = raft_on_vote_request(&c.nodes[1].state, 2, 1, 0, 0, &reply_term); /* Node 1 must deny — already voted for node 0 this term */ TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); @@ -4153,7 +4173,7 @@ static void test_proto_A5_leader_denies_vote_request(void) /* Send a vote request to the leader at the same term */ uint64_t reply_term = 0; - int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, &reply_term); + int rc = raft_on_vote_request(&c.nodes[0].state, 1, term_before, 0, 0, &reply_term); /* Must be denied */ TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); @@ -4187,7 +4207,7 @@ static void test_proto_C1_eligible_denies(void) s.term = 1; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 1, &reply_term); + int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); @@ -4214,7 +4234,7 @@ static void test_proto_C2_eligible_grants(void) s.term = 1; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 1, &reply_term); + int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_INT(1, s.voted_for); @@ -4240,7 +4260,7 @@ static void test_proto_C3_eligible_null_grants(void) s.term = 1; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 1, &reply_term); + int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); /* vote_eligible not called (NULL) */ @@ -4267,7 +4287,7 @@ static void test_proto_C4_stale_term_no_eligible_call(void) s.term = 5; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 3, &reply_term); + int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count); @@ -4295,7 +4315,7 @@ static void test_proto_C5_already_voted_no_eligible_call(void) s.voted_for = 1; s.voted_for_term = 3; - int rc = raft_on_vote_request(&s, 2, 3, &reply_term); + int rc = raft_on_vote_request(&s, 2, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count); @@ -4395,7 +4415,7 @@ static void test_proto_H6_restored_vote_blocks_second_vote(void) TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg)); /* Different candidate (idx=2) requests a vote in the SAME term 5 */ - int rc = raft_on_vote_request(&s, 2, 5, &reply_term); + int rc = raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -4405,7 +4425,7 @@ static void test_proto_H6_restored_vote_blocks_second_vote(void) TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); /* Same candidate (idx=1) retransmitting must still be re-granted */ - rc = raft_on_vote_request(&s, 1, 5, &reply_term); + rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); } @@ -4445,7 +4465,7 @@ static void test_proto_H3_persist_before_vote_recorded(void) s.term = 1; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 1, &reply_term); + int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term); /* Must grant */ TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); @@ -4481,7 +4501,7 @@ static void test_proto_H4_persist_fail_denies(void) s.term = 1; s.voted_for = -1; - int rc = raft_on_vote_request(&s, 1, 1, &reply_term); + int rc = raft_on_vote_request(&s, 1, 1, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(-1, s.voted_for); @@ -4602,7 +4622,7 @@ static void test_proto_B2_higher_term_updates_state(void) s.voted_for = 1; /* node voted for itself in an earlier election */ s.voted_for_term = 1; /* that vote was in term 1 */ - int rc = raft_on_vote_request(&s, 0, 5, &reply_term); + int rc = raft_on_vote_request(&s, 0, 5, 0, 0, &reply_term); /* Term must be upgraded to 5 */ TEST_ASSERT_EQUAL_UINT64(5, s.term); @@ -4632,7 +4652,7 @@ static void test_proto_B3_stale_term_denied(void) reset_mock(); TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_vote_request(&s, 0, 3, &reply_term); + int rc = raft_on_vote_request(&s, 0, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(5, reply_term); @@ -4660,7 +4680,7 @@ static void test_proto_B4_different_candidate_denied(void) s.voted_for = 1; s.voted_for_term = 3; - int rc = raft_on_vote_request(&s, 2, 3, &reply_term); + int rc = raft_on_vote_request(&s, 2, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_UINT64(3, reply_term); @@ -4691,7 +4711,7 @@ static void test_proto_B5_idempotent_same_candidate(void) s.voted_for = 1; s.voted_for_term = 3; - int rc = raft_on_vote_request(&s, 1, 3, &reply_term); + int rc = raft_on_vote_request(&s, 1, 3, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_UINT64(3, reply_term); @@ -5055,7 +5075,7 @@ static void test_proto_F2_voted_during_delay_skip(void) TEST_ASSERT_EQUAL_INT(-1, s.voted_for); /* During the delay, a vote request arrives from node 0 at term=1 */ - int rc = raft_on_vote_request(&s, 0, 1, &reply_term); + int rc = raft_on_vote_request(&s, 0, 1, 0, 0, &reply_term); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); TEST_ASSERT_EQUAL_INT(0, s.voted_for); @@ -5220,6 +5240,279 @@ static void test_proto_G2_partition_minority_no_quorum(void) #endif /* XCE_TESTING */ +/* ---------- (lastLogTerm,lastLogIndex) vote freshness tests (task 4.1) ---------- + * + * raft.tla HandleRequestVoteRequest logOk (285), closes G6. The library now + * computes logOk itself from the candidate's advertised (cand_last_log_term, + * cand_last_log_index) against its own (on_get_entry_term(log_len), log_len), + * BEFORE the application gate. */ + +/* Configure the voter's local log: last index = log_len, last term = last_term. */ +static void set_voter_log(raft_state_t *s, uint64_t log_len, uint64_t last_term) +{ + s->log_len = log_len; + g_mock.get_entry_term_log_len = log_len; + g_mock.local_last_log_term = last_term; +} + +/* + * test_vote_req_freshness_stale_term_denied_higher_index + * Voter log = (term 5, index 3). Candidate advertises a STALE last-log term (4) + * but a HIGHER index (10). logOk false -> deny, even though the app gate grants. + * (Req 3.1, 3.3, 3.4.) + */ +static void test_vote_req_freshness_stale_term_denied_higher_index(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + cbs.on_trace = mock_trace; + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 3, 5); + + int rc = raft_on_vote_request(&s, 1, 5, 4, 10, &reply_term); + + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_UINT64(5, reply_term); + TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); + TEST_ASSERT_EQUAL_INT(-1, s.voted_for); +} + +/* + * test_vote_req_freshness_fresher_term_granted_smaller_index + * Voter log = (term 5, index 10). Candidate (term 6 fresher, index 2 smaller). + * logOk = (6 > 5) -> grant. (Req 3.1, 3.6.) + */ +static void test_vote_req_freshness_fresher_term_granted_smaller_index(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + cbs.on_trace = mock_trace; + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 10, 5); + + int rc = raft_on_vote_request(&s, 1, 5, 6, 2, &reply_term); + + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); + TEST_ASSERT_EQUAL_INT(1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term); + TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); +} + +/* + * test_vote_req_freshness_tie_term_index_boundary + * Voter log = (term 5, index 4). On a term TIE, grant requires cand index >= + * voter log_len. index 3 (<4) -> deny; index 4 (==4) -> grant. (Req 3.1.) + */ +static void test_vote_req_freshness_tie_term_index_boundary(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + + reset_mock(); + g_mock.vote_eligible_return_value = 1; + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 4, 5); + int rc = raft_on_vote_request(&s, 1, 5, 5, 3, &reply_term); /* idx 3 < 4 */ + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); + + reset_mock(); + g_mock.vote_eligible_return_value = 1; + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 4, 5); + rc = raft_on_vote_request(&s, 1, 5, 5, 4, &reply_term); /* idx 4 == 4 */ + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); +} + +/* + * test_vote_req_freshness_empty_voter_log_term_zero + * Empty voter log (log_len 0): local last-log term 0. Candidate (0,0) satisfies + * logOk = (0>0)||(0==0 && 0>=0) = true -> grant. (Req 3.1.) + */ +static void test_vote_req_freshness_empty_voter_log_term_zero(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_INT(1, g_mock.persist_count); +} + +/* + * test_vote_req_freshness_logok_precedes_app_gate + * !logOk denies even when on_vote_eligible WOULD grant, and the app gate is not + * consulted (logOk strictly precedes it). (Req 3.4.) + */ +static void test_vote_req_freshness_logok_precedes_app_gate(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + uint64_t reply_term = 0; + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 8, 7); + + int rc = raft_on_vote_request(&s, 1, 5, 7, 1, &reply_term); + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_INT(0, g_mock.vote_eligible_call_count); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + +/* ---------- (lastLogTerm,lastLogIndex) PreVote freshness tests (task 5.1) ---------- + * + * The PreVote path applies the IDENTICAL logOk, and mutates NOTHING. (Req 4.1-4.5.) */ + +/* + * test_pre_vote_req_freshness_stale_tuple_denied + * Voter (term 5, index 3). PreVote tuple (term 4 stale, index 10) -> deny, no + * mutation, no persist. (Req 4.1, 4.3.) + */ +static void test_pre_vote_req_freshness_stale_tuple_denied(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 3, 5); + + int rc = raft_on_pre_vote_request(&s, 1, 5, 4, 10); + + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_UINT64(5, s.term); + TEST_ASSERT_EQUAL_INT(-1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); + TEST_ASSERT_EQUAL_UINT64(0, s.commit_index); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + +/* + * test_pre_vote_req_freshness_fresher_tuple_granted + * Voter (term 5, index 10). PreVote (term 6 fresher, index 2) -> grant, still + * side-effect-free. (Req 4.1.) + */ +static void test_pre_vote_req_freshness_fresher_tuple_granted(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 10, 5); + + int rc = raft_on_pre_vote_request(&s, 1, 5, 6, 2); + + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); + TEST_ASSERT_EQUAL_INT(-1, s.voted_for); +} + +/* + * test_pre_vote_req_freshness_no_mutation_grant_and_deny + * Across BOTH a grant and a deny, no change to term/voted_for/voted_for_term/ + * role/commit_index and no on_persist. (Req 4.4.) + */ +static void test_pre_vote_req_freshness_no_mutation_grant_and_deny(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + + reset_mock(); + g_mock.vote_eligible_return_value = 1; + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + s.commit_index = 2; + set_voter_log(&s, 4, 5); + TEST_ASSERT_EQUAL_INT(RAFT_GRANT, raft_on_pre_vote_request(&s, 1, 5, 5, 9)); + TEST_ASSERT_EQUAL_UINT64(5, s.term); + TEST_ASSERT_EQUAL_INT(-1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); + TEST_ASSERT_EQUAL_UINT64(2, s.commit_index); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); + + reset_mock(); + g_mock.vote_eligible_return_value = 1; + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + s.commit_index = 2; + set_voter_log(&s, 4, 5); + TEST_ASSERT_EQUAL_INT(RAFT_DENY, raft_on_pre_vote_request(&s, 1, 5, 5, 1)); + TEST_ASSERT_EQUAL_UINT64(5, s.term); + TEST_ASSERT_EQUAL_INT(-1, s.voted_for); + TEST_ASSERT_EQUAL_UINT64(0, s.voted_for_term); + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role); + TEST_ASSERT_EQUAL_UINT64(2, s.commit_index); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + +/* + * test_pre_vote_req_freshness_stale_term_still_denied + * The pre-existing stale-term deny stays in force even with a fresh tuple: + * cand_term < s.term denies. (Req 4.5.) + */ +static void test_pre_vote_req_freshness_stale_term_still_denied(void) +{ + raft_state_t s; + raft_callbacks_t cbs = make_mock_cbs(); + + cbs.on_vote_eligible = mock_vote_eligible; + cbs.on_get_entry_term = mock_get_entry_term; + reset_mock(); + g_mock.vote_eligible_return_value = 1; + + TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); + set_voter_log(&s, 3, 5); + + int rc = raft_on_pre_vote_request(&s, 1, 4, 99, 99); + TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); + TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count); +} + /* ---------- raft_on_pre_vote_request tests (task 2.1) ---------- */ /* @@ -5249,7 +5542,7 @@ static void test_pre_vote_req_grant_higher_term_eligible(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_pre_vote_request(&s, 1, 6); + int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); @@ -5283,7 +5576,7 @@ static void test_pre_vote_req_grant_equal_term_eligible(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_pre_vote_request(&s, 1, 5); /* equal term */ + int rc = raft_on_pre_vote_request(&s, 1, 5, 0, 0); /* equal term */ TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); @@ -5312,7 +5605,7 @@ static void test_pre_vote_req_deny_stale_term(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_pre_vote_request(&s, 1, 4); /* stale */ + int rc = raft_on_pre_vote_request(&s, 1, 4, 0, 0); /* stale */ TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); @@ -5346,7 +5639,7 @@ static void test_pre_vote_req_deny_eligibility_rejects(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_pre_vote_request(&s, 1, 6); + int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0); TEST_ASSERT_EQUAL_INT(RAFT_DENY, rc); TEST_ASSERT_EQUAL_INT(1, g_mock.vote_eligible_call_count); @@ -5375,7 +5668,7 @@ static void test_pre_vote_req_null_eligible_grants(void) TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5)); - int rc = raft_on_pre_vote_request(&s, 1, 6); + int rc = raft_on_pre_vote_request(&s, 1, 6, 0, 0); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); @@ -5409,7 +5702,7 @@ static void test_pre_vote_req_grant_preserves_existing_vote(void) s.voted_for_term = 5; /* A different candidate (peer 2) probes a PreVote for term 6. */ - int rc = raft_on_pre_vote_request(&s, 2, 6); + int rc = raft_on_pre_vote_request(&s, 2, 6, 0, 0); TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc); @@ -5940,14 +6233,14 @@ static void test_invariant_prevote_sequence_pure_no_persist(void) TEST_ASSERT_EQUAL_INT(0, raft_begin_pre_vote(&s)); /* (b) answer an incoming PreVote request that PASSES eligibility — grant */ - TEST_ASSERT_EQUAL_INT(1, raft_on_pre_vote_request(&s, 2, 6 /* higher */)); + TEST_ASSERT_EQUAL_INT(1, raft_on_pre_vote_request(&s, 2, 6 /* higher */, 0, 0)); /* (c) answer a STALE-term incoming request — deny */ - TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 3 /* stale */)); + TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 3 /* stale */, 0, 0)); /* (d) answer an incoming request the eligibility gate REJECTS — deny */ g_mock.vote_eligible_return_value = 0; - TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 6)); + TEST_ASSERT_EQUAL_INT(0, raft_on_pre_vote_request(&s, 2, 6, 0, 0)); g_mock.vote_eligible_return_value = 1; /* (e) tally replies: a grant (drives to WON quorum on 3-node), then a @@ -6103,7 +6396,7 @@ static void test_invariant_real_vote_higher_term_adopt_unchanged(void) s.voted_for_term = 2; reset_mock(); - int rc = raft_on_vote_request(&s, 1, 5, &reply_term); + int rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term); /* Higher-term adopt + leader step-down, exactly as bug-339/340 require. */ TEST_ASSERT_EQUAL_UINT64(5, s.term); @@ -6229,6 +6522,19 @@ void test_raft(void) RUN_TEST(test_vote_req_persist_fails); RUN_TEST(test_vote_req_reply_term_always_set); + /* (lastLogTerm,lastLogIndex) binding-vote freshness (task 4.1, Req 3.1-3.6) */ + RUN_TEST(test_vote_req_freshness_stale_term_denied_higher_index); + RUN_TEST(test_vote_req_freshness_fresher_term_granted_smaller_index); + RUN_TEST(test_vote_req_freshness_tie_term_index_boundary); + RUN_TEST(test_vote_req_freshness_empty_voter_log_term_zero); + RUN_TEST(test_vote_req_freshness_logok_precedes_app_gate); + + /* (lastLogTerm,lastLogIndex) PreVote freshness (task 5.1, Req 4.1-4.5) */ + RUN_TEST(test_pre_vote_req_freshness_stale_tuple_denied); + RUN_TEST(test_pre_vote_req_freshness_fresher_tuple_granted); + RUN_TEST(test_pre_vote_req_freshness_no_mutation_grant_and_deny); + RUN_TEST(test_pre_vote_req_freshness_stale_term_still_denied); + /* raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election (task 3.5) */ RUN_TEST(test_election_timeout_undecided); RUN_TEST(test_election_timeout_already_decided);