Lab 2: Verified Replicated State Machine (Verus MultiPaxos)
Introduction
In this lab you’ll work with a verified MultiPaxos (RSL) implementation in Verus (verified Rust), ported from Microsoft’s IronFleet (Dafny).
A replicated state machine ensures that all replicas agree on the same sequence of operations, even in the presence of failures. MultiPaxos achieves this through a ballot-based voting protocol. The fundamental safety property is:
If two quorums each decide a value for the same operation, those values must be identical.
Proving this property requires a sophisticated induction argument that tracks how values propagate across different ballots. In this lab, you will both prove this safety property and implement the protocol actions, all within Verus’s verification framework.
Objective
In this lab you’ll complete three parts:
- Part 1 — Protocol Safety Proof: Complete 10 core proof lemmas establishing the safety of MultiPaxos.
- Part 2 — Implementation + Refinement: Implement 21 protocol action functions and prove each refines its specification. Build the system, run it with 3 servers and 32 clients, and report throughput/latency.
- Part 3 — Performance Optimization (Bonus): Optimize the verified implementation to achieve > 20K req/s while maintaining full verification.
Your task: replace every admit() with actual proofs and implementations so that verus src/main.rs reports 0 errors, and the system runs end-to-end.
Some general tips:
- Start early. Although the amount of code isn’t large, getting Verus proofs to go through will be challenging.
- Read and understand the protocol specs in
src/protocol/RSL/before you start. Your proofs should follow the spec closely, since that’s what the verifier expects.
Getting Started
We supply you with Rust skeleton code. All functions you need to complete have admit() as their body.
Go to this link to join the lab assignment.
Similarly to lab 1, git clone your repo (except this time we already copied the code for you so you don’t need to do that). You can stay on the main branch. For submit, commit your code with a message starting with “submit” (lower case).
To verify your work:
verus src/main.rs 2>&1 | tail -1
# Target: "verification results:: XXX verified, 0 errors"
To build and run:
scons --verus-path="$VERUS_PATH" --no-verify
The Code
The codebase is organized into layers:
src/
services/RSL/ # Entry point (paxos_main), application state machine (counter)
protocol/RSL/ # Abstract spec: acceptor, proposer, learner, executor, election
common_proof/ # Safety proof lemmas <-- Part 1 edits here
refinement_proof/ # Refinement proofs (provided)
implementation/RSL/ # Concrete Rust implementation <-- Part 2 edits here
common/ # IO framework, collections, logic
verus_extra/ # Verus helper libraries
csharp/ # .NET IO framework (server/client), calls Rust via FFI
Most of the work for this lab should be in:
src/protocol/RSL/common_proof/(Part 1: proof lemmas)src/implementation/RSL/acceptorimpl.rs(Part 2: acceptor)src/implementation/RSL/electionimpl.rs(Part 2: election)src/implementation/RSL/proposerimpl.rs(Part 2: proposer)
You will also interact with the protocol spec files:
src/protocol/RSL/acceptor.rs(spec for acceptor actions)src/protocol/RSL/election.rs(spec for election actions)src/protocol/RSL/proposer.rs(spec for proposer actions)
For the lab, you should implement proof and protocol logic in the files listed above. Do not modify the provided helper functions, spec definitions, or IO/FFI infrastructure.
Refinement
Each implementation function has an ensures clause referencing a spec function:
pub fn CAcceptorProcess1a(&mut self, inp: CPacket) -> (sent: OutboundPackets)
requires old(self).valid(), inp.valid(), inp.msg is CMessage1a
ensures self.valid(), sent.valid(),
LAcceptorProcess1a(old(self)@, self@, inp@, sent@) // <-- refines spec
{
admit(); // TODO: replace
}
The spec defines what the function should do. You implement how in Rust with concrete types. Verus verifies the @ (view/abstraction) of your output matches the spec.
Available Helper Lemmas
These are provided (already proved) and available to call in your proofs:
Behavior & Constants:
lemma_ConstantsAllConsistent(b, c, i)— constants consistent at step ilemma_ReplicaConstantsAllConsistent(b, c, i, idx)— per-replica consistencylemma_AssumptionsMakeValidTransition(b, c, i)— step i->i+1 is valid
Packet Tracking:
lemma_PacketStaysInSentPackets(b, c, i, j, p)— once sent, always sentlemma_PacketSetStaysInSentPackets(b, c, i, j, s)— same for setslemma_ExtractSentPacketsFromIos(ios)— connects IOs to sent packets
Action Identification:
lemma_ActionThatSends2aIsMaybeNominateValueAndSend2a(s, s', p)-> (idx, ios)lemma_ActionThatSends1bIsProcess1a(s, s', p)-> (idx, ios)lemma_ActionThatSends2bIsProcess2a(s, s', p)-> (idx, ios)lemma_ActionThatChangesReplicaIsThatReplicasAction(b, c, i, idx)-> ios
Message Properties:
lemma_1bMessageImplicationsForCAcceptor(b, c, i, opn, p)— 1b reflects acceptor statelemma_2bMessageImplicationsForCAcceptor(b, c, i, p)— 2b reflects acceptor statelemma_VoteWithOpnImplies2aSent(b, c, i, idx, opn)— vote exists -> 2a was sentlemma_2aMessageHasValidBallot(b, c, i, p)— ballot has valid seqno/proposer_id
Quorum:
lemma_GetIndicesFromNodes(nodes, config)-> index setlemma_GetIndicesFromPackets(packets, config)-> index set
Learner State:
lemma_GetSent2bMessageFromLearnerState(b, c, i, idx, opn, sender)— extracts a sent 2blemma_Received2bMessageSendersAlwaysValidReplicas(b, c, i, idx, opn)— senders are valid
Required Functionality
Part 1: Protocol Safety Proof
Complete 10 proof lemmas in src/protocol/RSL/common_proof/ that establish the safety of MultiPaxos. The top-level safety theorem (lemma_ChosenQuorumsMatchValue) depends on all others:
lemma_ChosenQuorumsMatchValue [chosen.rs]
|
+-- lemma_ChosenQuorumAnd2aFromLaterBallotMatchValues [chosen.rs]
| | "If value v is chosen at ballot b, any 2a at higher ballot b'
| | has value v" (cross-ballot induction — THE HEART OF PAXOS)
| |
| +-- lemma_2aMessageHas1bQuorumPermittingIt [message2a.rs]
| +-- lemma_QuorumIndexOverlap [quorum.rs]
| +-- lemma_1bMessageWithoutOpnImplicationsFor2b [message1b.rs]
| +-- lemma_1bMessageWithOpnImplies2aSent [message1b.rs]
| +-- lemma_Vote1bMessageIsFromEarlierBallot [message1b.rs]
| +-- (recursive call with smaller ballot)
|
+-- lemma_2aMessagesFromSameBallotAndOperationMatch [message2a.rs]
| (calls lemma_2aMessagesFromSameBallotAndOperationMatchWLOG)
|
+-- lemma_2bMessageHasCorresponding2aMessage [message2b.rs]
Files to modify (all in src/protocol/RSL/common_proof/):
| File | Lemmas |
|---|---|
message2b.rs |
lemma_2bMessageHasCorresponding2aMessage |
message1b.rs |
lemma_Vote1bMessageIsFromEarlierBallot, lemma_1bMessageWithoutOpnImplicationsFor2b, lemma_1bMessageWithOpnImplies2aSent |
quorum.rs |
lemma_QuorumIndexOverlap |
message2a.rs |
lemma_2aMessagesFromSameBallotAndOperationMatch, lemma_2aMessagesFromSameBallotAndOperationMatchWLOG, lemma_2aMessageHas1bQuorumPermittingIt |
chosen.rs |
lemma_ChosenQuorumAnd2aFromLaterBallotMatchValues, lemma_ChosenQuorumsMatchValue |
You are recommended to do the lemmas in this order:
| # | Lemma | Key Technique |
|---|---|---|
| 1 | lemma_2bMessageHasCorresponding2aMessage |
Induction on behavior prefix i. Base: sentPackets empty. Inductive: packet old -> recurse; new -> lemma_ActionThatSends2bIsProcess2a. |
| 2 | lemma_Vote1bMessageIsFromEarlierBallot |
Similar induction. Use lemma_VotePrecedesMaxBal. |
| 3 | lemma_QuorumIndexOverlap |
Pigeonhole on finite sets. Use lemma_BoundedIntSetFinite + lemma_SetOfElementsOfRangeNoBiggerThanRange. |
| 4 | lemma_1bMessageWithOpnImplies2aSent |
Induction. When 1b newly sent, acceptor has vote -> lemma_VoteWithOpnImplies2aSent. |
| 5 | lemma_1bMessageWithoutOpnImplicationsFor2b |
4 cases: both old / 1b old + 2b new / 1b new + 2b old / both new (contradiction). |
| 6 | lemma_2aMessagesFromSameBallotAndOperationMatch |
Delegates to WLOG (Without Loss Of Generality) helper. |
| 7 | lemma_2aMessagesFromSameBallotAndOperationMatchWLOG |
Induction: both old -> recurse; one new -> trace to proposer, show uniqueness. |
| 8 | lemma_2aMessageHas1bQuorumPermittingIt |
Induction: trace 2a to send step, extract proposer’s received_1b_packets as quorum. |
| 9 | lemma_ChosenQuorumAnd2aFromLaterBallotMatchValues |
Core induction. Uses all above. decreases (ballot.seqno, ballot.proposer_id). |
| 10 | lemma_ChosenQuorumsMatchValue |
Case-split on q1.bal vs q2.bal, call sub-lemmas. |
Core proof intuition:
- Case b1 == b2: Both quorums voted at the same ballot. By
lemma_2aMessagesFromSameBallotAndOperationMatch, a proposer sends only one value per (ballot, opn). Values match. - Case b1 < b2 (WLOG): The proposer at b2 collected a 1b quorum Q_1b. Q_1b and Q1 overlap (pigeonhole). The overlap replica voted at b1 and sent 1b to b2. The 1b must report the vote (contradiction otherwise). The proposer at b2 picks highest-ballot vote b_max where b <= b_max < b2. If b_max == b, value matches directly. If b_max > b, recurse with b_max (terminates since b_max < b2).
Part 2: Implementation + Refinement
Implement 21 protocol action functions in src/implementation/RSL/ and prove each refines its specification.
acceptorimpl.rs — Acceptor:
| Function | Refines | Description |
|---|---|---|
CAcceptorTruncateLog |
LAcceptorTruncateLog |
Remove old votes below truncation point |
CAcceptorProcessHeartbeat |
LAcceptorProcessHeartbeat |
Update last_checkpointed_operation |
CAcceptorProcess1a |
LAcceptorProcess1a |
Compare ballot, send 1b with votes |
CAcceptorProcess2a |
LAcceptorProcess2a |
Update votes, send 2b broadcast |
Given helpers (don’t modify): CAcceptorInit, CRemoveVotesBeforeLogTruncationPoint, CAddVoteAndRemoveOldOnes, CIsLogTruncationPointValid
electionimpl.rs — Election:
| Function | Refines | Description |
|---|---|---|
CElectionStateReflectReceivedRequest |
ElectionStateReflectReceivedRequest |
Track client request in epoch |
CElectionStateCheckForViewTimeout |
ElectionStateCheckForViewTimeout |
Timeout + suspicion logic |
CElectionStateCheckForQuorumOfViewSuspicions |
ElectionStateCheckForQuorumOfViewSuspicions |
Quorum check + view advance |
CElectionStateProcessHeartbeat |
ElectionStateProcessHeartbeat |
Reset suspicion or suspect view |
Given helpers (don’t modify): CElectionStateInit, CComputeSuccessorView, CBoundRequestSequence, CRequestsMatch, CRequestSatisfiedBy, FindEarlierRequests, BoundCRequestHeaders, CRemoveAllSatisfiedRequestsInSequence, CRemoveExecutedRequestBatch, CElectionStateReflectExecutedRequestBatch
proposerimpl.rs — Proposer:
| Function | Refines | Description |
|---|---|---|
CIsAfterLogTruncationPoint |
LIsAfterLogTruncationPoint |
Check opn is after log truncation (HashSet loop) |
CSetOfMessage1b |
LSetOfMessage1b |
Check all packets in set are 1b (HashSet loop) |
CSetOfMessage1bAboutBallot |
LSetOfMessage1bAboutBallot |
Check all 1b packets match ballot (HashSet loop) |
CAllAcceptorsHadNoProposal |
LAllAcceptorsHadNoProposal |
Check no votes for opn in 1b set (HashSet + votes) |
CExistsAcceptorHasProposalLargeThanOpn |
LExistsAcceptorHasProposalLargeThanOpn |
Check existence of proposal > opn (HashSet loop) |
Cmax_balInS |
Lmax_balInS |
Check ballot is max in 1b set (HashSet + votes + ballot) |
CProposerProcessRequest |
LProposerProcessRequest |
Handle client request (HashMap get/insert + multi-branch) |
CProposerProcess1b |
LProposerProcess1b |
Add 1b packet to received set |
CProposerMaybeEnterNewViewAndSend1a |
LProposerMaybeEnterNewViewAndSend1a |
Start new ballot, broadcast 1a |
CProposerMaybeEnterPhase2 |
LProposerMaybeEnterPhase2 |
Check 1b quorum, enter phase 2 |
CProposerMaybeNominateValueAndSend2a |
LProposerMaybeNominateValueAndSend2a |
Decide whether to nominate |
CProposerNominateNewValueAndSend2a |
LProposerNominateNewValueAndSend2a |
Pick new value from queue, send 2a |
CProposerNominateOldValueAndSend2a |
LProposerNominateOldValueAndSend2a |
Highest-ballot value from 1b votes, send 2a |
Given helpers (don’t modify): CProposerInit, CProposerCanNominateUsingOperationNumber, CExistVotesHasProposalLargeThanOpn, CProposerProcessHeartbeat, CProposerCheckForViewTimeout, CProposerCheckForQuorumOfViewSuspicions, CProposerResetViewTimerDueToExecution
You are recommended to implement in this order:
| # | Function | Notes |
|---|---|---|
| 1 | CAcceptorTruncateLog |
Call CRemoveVotesBeforeLogTruncationPoint, update field |
| 2 | CAcceptorProcessHeartbeat |
Update checkpoint array |
| 3 | CAcceptorProcess1a |
Compare ballots, construct 1b |
| 4 | CAcceptorProcess2a |
Update votes, construct 2b broadcast |
| 5 | CSetOfMessage1b |
HashSet loop: check all elements are 1b |
| 6 | CSetOfMessage1bAboutBallot |
HashSet loop: check all 1b match ballot |
| 7 | CIsAfterLogTruncationPoint |
HashSet loop: check log truncation point |
| 8 | CAllAcceptorsHadNoProposal |
HashSet loop + votes HashMap lookup |
| 9 | CExistsAcceptorHasProposalLargeThanOpn |
HashSet loop: existence check |
| 10 | Cmax_balInS |
HashSet loop + votes + ballot comparison |
| 11 | CProposerProcessRequest |
HashMap get/insert, multi-branch logic |
| 12 | CProposerProcess1b |
Add packet to received set |
| 13 | CElectionStateReflectReceivedRequest |
Track request |
| 14 | CElectionStateCheckForViewTimeout |
Timeout + suspicion logic |
| 15 | CElectionStateCheckForQuorumOfViewSuspicions |
Quorum check + view advance |
| 16 | CElectionStateProcessHeartbeat |
Longest election function |
| 17 | CProposerMaybeEnterNewViewAndSend1a |
Start new ballot |
| 18 | CProposerMaybeEnterPhase2 |
Check 1b quorum |
| 19 | CProposerMaybeNominateValueAndSend2a |
Nomination dispatcher |
| 20 | CProposerNominateNewValueAndSend2a |
New value from queue |
| 21 | CProposerNominateOldValueAndSend2a |
Highest-ballot from votes (HashSet iteration + loop invariants) |
Key implementation patterns:
- Direct field mapping:
self.max_bal = bal_1a;maps tos'.max_bal == inp.msg->bal_1ain spec. - Broadcast construction: Use
CBroadcast::BuildBroadcastToEveryone(...)(already verified). - HashSet iteration: Convert with
hashset_to_vec(...), iterate withwhileloop + invariants. - Assert refinement: End each function with
assert(LAcceptorProcess1a(old(self)@, self@, inp@, sent@));. Break into conjuncts if Verus can’t prove it automatically.
Part 3 (Bonus): Performance Optimization
Baseline: ~5K req/s. Target: > 20K req/s. All code must remain fully verified (0 admits, 0 assumes, no new external_body on logic functions).
Main bottlenecks and strategies:
| Bottleneck | Strategy |
|---|---|
CAddVoteAndRemoveOldOnes creates new HashMap every call |
In-place HashMap mutation |
CRemoveVotesBeforeLogTruncationPoint same pattern |
In-place mutation |
CElectionStateReflectExecutedRequestBatch recursive Vec rebuild |
Loop-based single-pass filtering |
truncate_vec_crequest / concat_vec_crequest element-by-element clone |
Vec::split_off / Vec::append |
| Throughput | Bonus |
|---|---|
| > 10K req/s | +5% |
| > 20K req/s | +10% |
Compile and Test
Prerequisites:
- Verus (0.2026.02.04.175a879)
- rustc 1.80.1
- .NET 6.0 SDK
- scons (
pip install scons) - Python 3
Build:
# Verify + compile
scons --verus-path="$VERUS_PATH"
# Compile only (skip verification)
scons --verus-path="$VERUS_PATH" --no-verify
Run:
# Generate certificates (one-time)
dotnet bin/CreateIronServiceCerts.dll outputdir=certs name=MyCounter type=IronRSL \
addr1=127.0.0.1 port1=4001 addr2=127.0.0.1 port2=4002 addr3=127.0.0.1 port3=4003
# Start 3 servers (separate terminals or background)
dotnet bin/IronRSLServerUDP.dll certs/MyCounter.IronRSL.service.txt certs/MyCounter.IronRSL.server1.private.txt &
dotnet bin/IronRSLServerUDP.dll certs/MyCounter.IronRSL.service.txt certs/MyCounter.IronRSL.server2.private.txt &
dotnet bin/IronRSLServerUDP.dll certs/MyCounter.IronRSL.service.txt certs/MyCounter.IronRSL.server3.private.txt &
# Wait for leader election (~15s), then run benchmark
sleep 15
dotnet bin/IronRSLClientUDP.dll ip1=127.0.0.1 ip2=127.0.0.1 ip3=127.0.0.1 nthreads=32 duration=30
Verify no admits or assumes remain:
# No admits in proof layer
grep -rn 'admit()' src/protocol/RSL/common_proof/ | wc -l # Target: 0
# No admits in implementation
grep -rn 'admit()' src/implementation/RSL/acceptorimpl.rs \
src/implementation/RSL/electionimpl.rs \
src/implementation/RSL/proposerimpl.rs | wc -l # Target: 0
# No assumes anywhere
grep -rn '^\s*assume(' src/protocol/RSL/ src/implementation/RSL/ | wc -l # Target: 0
Submit
Commit your code to the repository. Include in your report:
- Verification output (
XXX verified, 0 errors) - Throughput and latency numbers from the 32-client, 30-second run
Grading
| Criteria | Weight |
|---|---|
| Part 1: Protocol Proof — 10 lemmas, each verified = 4.5% | 45% |
| Part 2: Implementation + Run — 21 functions, each verified = ~2%; build + benchmark = 3% | 45% |
| Part 3: Optimization (Bonus) — > 10K = +5%, > 20K = +10% | up to +10% |
DO NOT
Make sure you do not do any of the following; otherwise you will fail the labs, even if the code may pass verification.
- Modify provided helper functions, spec definitions, or IO/FFI infrastructure.
- Add
assume()or keepadmit()in any submitted code. - Add new
external_bodyannotations on logic functions (existing ones for IO/clone/hash are fine).