Capstone: End-to-End Proof of a Packet Router

Prove a two-by-two router end to end with one symbolic packet, catch a reordering queue, and see a fixed-priority arbiter pass every safety check and fail the bounded wait.

Lesson
F9.2
Time
40 min
Runs on
SymbiYosys
Check
4 questions

A two-by-two router used fixed priority at each output. When both inputs had a packet for the same output, input 0 always won. Per-source order held, so the simulation scoreboard never fired. The spec had a second sentence, "no offer waits more than one round", and nobody had written it as a property.

One Packet Carries the Proof

rtr22 and the Tracked PacketTwo inputs, a grant per output, a two-entry queue per output, two outputs. The tracked packet is drawn as it moves: accepted, queued, at the head, gone.
rtr22 and the tracked packetTwo inputs, a grant per output, a two-entry queue per output, two outputs. The tracked packet is drawn as it moves: accepted, queued, at the head, gone. input input 0 tvalid, tdata, tdest input input 1 tvalid, tdata, tdest logic grant per output round-robin whenboth want it state queue 0 two entries {tid,tdata} state queue 1 two entries {tid,tdata} output output 0 tvalid, tdata, tid output output 1 tvalid, tdata, tid 1 accepted2 queued3 its turn, 4 gone
Read the diagram as text
  • input 0: tvalid, tdata, tdest
  • input 1: tvalid, tdata, tdest
  • grant per output: round-robin when both want it
  • queue 0: two entries {tid, tdata}
  • queue 1: two entries {tid, tdata}
  • output 0: tvalid, tdata, tid
  • output 1: tvalid, tdata, tid
  • input 0 → grant per output: 1 accepted
  • input 1 → grant per output
  • grant per output → queue 0: 2 queued
  • grant per output → queue 1
  • queue 0 → output 0: 3 its turn, 4 gone
  • queue 1 → output 1

42 flop bits of router, 99 with the bench. tid names the input a packet came from (IHI 0051B 2.7).

Sentence of the specWhat the bench remembersCheck
Every accepted packet is still inside until it leavesaccepted? (f_in), packets of its flow ahead (f_ahead)a_inside: its queue holds more than f_ahead
Packets of one source to one output leave in order, unchangedits source (f_src), output (f_dest), payload (f_data)a_turn: at its turn, the packet leaving is it
Nothing leaves that was not sent there: no duplicate, no wrong outputpackets owed per flow (fl00 .. fl11)a_owed0, a_owed1

Four checks, one tracked packet. The solver picks its source once and the moment it is accepted, so every packet of every stream is the tracked one in some trace.

End to end means: what enters leaves once, at the right output, unchanged, in order within its flow. The bench never stores the stream. It follows one packet the solver picks, as F4.4 did for a FIFO, and counts the packets of its flow still ahead of it.

The tag is the moment of acceptance, never the payload. Payloads repeat; a tracker that follows "the first packet with data 5A" follows the wrong one as soon as two packets carry 5A.

The Proof

RunResult
Covers: a packet each way, both inputs at one output, a full queue, the tracked packet goneCover PASS, last cover reached at step 4, 0.6 s; deepest c_full
Safety set, abc pdrProve PASS (abc pdr), 0.8 s
Safety set, aiger rIC3Prove PASS (rIC3), 0.6 s
Safety set, k-induction, k = 8UNKNOWN at k = 8 (base case passed, induction step failed); a_owed0
Safety set with five queue invariants, k = 8Prove PASS (k = 8), 0.6 s

Reset assumed in step 0; the inputs keep TVALID and the payload until accepted (IHI 0051B 2.2). Nothing restricts the outputs' tready.

IC3 finds the invariant on its own. k-induction needs it written down: the queues agree with the flow counts, and the tracked packet sits where f_ahead says (F4.6).

A Queue That Reorders

BUG_REORDER: the newer packet first
01234clkfrees0_tvalidfrees0_tdestfrees0_tdata80018000frees1_tvalidfreem0_treadym0_tvalidm0_tidm0_tdata00F1E1F1f_inf_aheadits turn, a newer packet leavesreported step 4

Measured, bmc depth 20: FAIL at reported step 4 on a_turn.

With two entries from the same input, the faulty queue sends the newer first. At the tracked packet's turn the packet leaving carries other data, and a_turn fails at reported step 4.

The Sentence Safety Cannot Say

The Starvation Counterexample a Liveness Engine Returns (Drawing)A prefix from reset, then a loop the design can repeat forever: input 0 always wins, input 1's offer is never taken, and the downstream is ready inside the loop.
The starvation counterexample a liveness engine returns (drawing)A prefix from reset, then a loop the design can repeat forever: input 0 always wins, input 1's offer is never taken, and the downstream is ready inside the loop. start reset step 0 state input 1 offers both want output 0 loop input 0 wins a packet queued loop output 0 ready fairness is kept forever
Read the diagram as text
  • reset: step 0
  • input 1 offers: both want output 0
  • input 0 wins: a packet queued
  • output 0 ready: fairness is kept
  • reset → input 1 offers
  • input 1 offers → input 0 wins
  • input 0 wins → output 0 ready
  • output 0 ready → input 0 wins: forever

Drawn, not measured: this site's engine does not yet run liveness jobs. The bounded wait below is what ships.

"Every offer is eventually taken" is liveness. It needs a fairness assumption: each output's tready is high infinitely often. Without it the downstream may stall forever and nothing is owed (F3.7).

With fixed priority the safety set still passes: Prove PASS (abc pdr), 0.8 s. Nothing is dropped, duplicated or reordered. The bug is a wait with no end, and only a liveness or bounded-wait property sees it.

The Bounded Wait That Ships

BUG_FIXED_PRIO: input 1 waits while input 0 streams
012345678clkfrees0_tvalidfrees0_tdests0_treadyfrees1_tvalidfrees1_tdests1_treadyfreem0_treadywait101234560input 1 starts waitingwait > 5reported step 8

Measured, bmc depth 20: FAIL at reported step 8 on a_wait. 4 input-0 packets are accepted while input 1 waits.

The stand-in: if tready is never low more than two cycles in a row, every offer is accepted within 5 cycles. The bound is measured: a cover reaches a wait of 5 but not 6, and the proof of the bound passes (Prove PASS (abc pdr), 0.5 s). Fixed priority fails it.

The assumption is named as a stand-in for fairness. A reviewer accepts it, or asks for the liveness run.

Asked in Interviews

QuestionAnswer
How do you prove a router end to end?One symbolic packet: picked at acceptance, counted through its flow, checked unchanged at its turn, plus a check that nothing leaves unowed.
What does a safety set miss?Starvation. A fixed-priority arbiter drops and reorders nothing, so every safety check passes; "every offer is eventually taken" fails.
How would you ship it today?A bounded wait under a named stand-in for fairness, with the bound from a measured cover.

Try It

The lab is the router with the tracker: Prove PASS (rIC3), 0.6 s. The twins add the covers, the invariants for k-induction, the reordering queue and the bounded wait on both arbiters.

The whole task. The twins change the mode, the engine and the bench switches.
[options]
mode prove
[engines]
aiger rIC3
[script]
read_verilog -formal -sv design.sv
prep -top rtr22
[files]
design.sv

The checks as SVA

assert property (@(posedge clk) disable iff (rst)
  f_in |-> f_room > f_ahead);
assert property (@(posedge clk) disable iff (rst)
  f_in && f_lv && f_ahead == 0 |-> f_out == f_data);

No drop; order and payload at its turn.

The liveness obligation (not run here)

assume property (@(posedge clk) s_eventually m0_tready);
assume property (@(posedge clk) s_eventually m1_tready);
assert property (@(posedge clk)
  s1_tvalid |-> s_eventually s1_tready);

One obligation per live run, and a companion s_eventually 1'b0 that must fail.

  1. Write the tracker

    From the table: what must the bench remember when the packet is accepted, and when it leaves?

  2. Covers

    Run the covers; the tracked packet leaves at step 4.

  3. Prove

    Run rIC3, then k-induction with and without the invariants.

  4. The twins

    Run the reordering queue, then the bounded wait on both arbiters.

design.sv

props.sv

Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.

Inputs are free unless constrained. Run to find a counterexample or check a proof.

Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.

SBY summary and log

Check Yourself

Predict the outputrtr22 with fixed priority, the safety set only (no bounded wait), mode prove, abc pdr, rst assumed high in step 0. The verdict:
module rtr22(input clk, rst,
  input s0_tvalid, input [7:0] s0_tdata, input s0_tdest, output s0_tready,
  input s1_tvalid, input [7:0] s1_tdata, input s1_tdest, output s1_tready,
  output m0_tvalid, output [7:0] m0_tdata, output m0_tid, input m0_tready,
  output m1_tvalid, output [7:0] m1_tdata, output m1_tid, input m1_tready);
  reg [8:0] q0_0 = 0, q0_1 = 0, q1_0 = 0, q1_1 = 0;   // {tid, tdata}; _0 is the head
  reg [1:0] n0 = 0, n1 = 0;                            // entries in each queue
  reg rr0 = 0, rr1 = 0;                                // 1: input 1 goes first at that output
  wire w00 = s0_tvalid && !s0_tdest, w01 = s0_tvalid && s0_tdest;   // input i wants output k
  wire w10 = s1_tvalid && !s1_tdest, w11 = s1_tvalid && s1_tdest;
  // output 0: grant, queue, pop
  wire g00 = w00;                       // bug: input 0 always wins
  wire g10 = w10 && !w00;
  wire p0 = (g00 || g10) && n0 != 2'd2;          // a packet enters queue 0
  wire [8:0] d0 = g00 ? {1'b0, s0_tdata} : {1'b1, s1_tdata};
  wire o0 = m0_tvalid && m0_tready;                  // a packet leaves output 0
  assign m0_tvalid = n0 != 2'd0;
  assign {m0_tid, m0_tdata} = q0_0;
  always @(posedge clk)
    if (rst) begin n0 <= 0; rr0 <= 0; end
    else begin
      if (o0) q0_0 <= q0_1;
      if (p0) begin if (n0 - o0 == 2'd0) q0_0 <= d0; else q0_1 <= d0; end
      if (p0) rr0 <= g00;                           // the other input goes first next time
      n0 <= n0 + p0 - o0;
    end
  // output 1: grant, queue, pop
  wire g01 = w01;                       // bug: input 0 always wins
  wire g11 = w11 && !w01;
  wire p1 = (g01 || g11) && n1 != 2'd2;          // a packet enters queue 1
  wire [8:0] d1 = g01 ? {1'b0, s0_tdata} : {1'b1, s1_tdata};
  wire o1 = m1_tvalid && m1_tready;                  // a packet leaves output 1
  assign m1_tvalid = n1 != 2'd0;
  assign {m1_tid, m1_tdata} = q1_0;
  always @(posedge clk)
    if (rst) begin n1 <= 0; rr1 <= 0; end
    else begin
      if (o1) q1_0 <= q1_1;
      if (p1) begin if (n1 - o1 == 2'd0) q1_0 <= d1; else q1_1 <= d1; end
      if (p1) rr1 <= g01;                           // the other input goes first next time
      n1 <= n1 + p1 - o1;
    end
  assign s0_tready = g00 && n0 != 2'd2 || g01 && n1 != 2'd2;
  assign s1_tready = g10 && n0 != 2'd2 || g11 && n1 != 2'd2;
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  wire a0 = s0_tvalid && s0_tready, a1 = s1_tvalid && s1_tready;   // a packet is accepted at input i
  // registered copies of the inputs, and the AXI-Stream source rules (IHI 0051B 2.2): TVALID holds, payload stable
  reg h0 = 0, h1 = 0, e0 = 0, e1 = 0;
  reg [7:0] t0 = 0, t1 = 0;
  always @(posedge clk) begin
    h0 <= !rst && s0_tvalid && !s0_tready; h1 <= !rst && s1_tvalid && !s1_tready;
    t0 <= s0_tdata; t1 <= s1_tdata; e0 <= s0_tdest; e1 <= s1_tdest;
  end
  always @(posedge clk) if (!rst) begin
    if (h0) assume(s0_tvalid && s0_tdata == t0 && s0_tdest == e0);
    if (h1) assume(s1_tvalid && s1_tdata == t1 && s1_tdest == e1);
  end
  // packets owed per flow (source i to output k): accepted, not yet left
  reg [1:0] fl00 = 0, fl01 = 0, fl10 = 0, fl11 = 0;
  always @(posedge clk)
    if (rst) begin fl00 <= 0; fl01 <= 0; fl10 <= 0; fl11 <= 0; end
    else begin
      fl00 <= fl00 + (a0 && !s0_tdest) - (o0 && !m0_tid);
      fl01 <= fl01 + (a0 && s0_tdest) - (o1 && !m1_tid);
      fl10 <= fl10 + (a1 && !s1_tdest) - (o0 && m0_tid);
      fl11 <= fl11 + (a1 && s1_tdest) - (o1 && m1_tid);
    end
  // the tracked packet: any accepted packet of input f_src, at a moment the solver picks
  wire f_src = $anyconst;
  wire f_pick = $anyseq;
  reg f_in = 0, f_done = 0, f_dest = 0;
  reg [7:0] f_data = 0;
  reg [1:0] f_ahead = 0;                                   // packets of its flow in front of it
  wire f_acc = f_src ? a1 : a0;
  wire f_tdest = f_src ? s1_tdest : s0_tdest;
  wire [1:0] f_flow = f_src ? (f_tdest ? fl11 : fl10) : (f_tdest ? fl01 : fl00);
  wire f_leave_now = f_tdest ? o1 && m1_tid == f_src : o0 && m0_tid == f_src;
  wire f_lv = f_dest ? o1 && m1_tid == f_src : o0 && m0_tid == f_src;   // a packet of its flow leaves
  wire [7:0] f_out = f_dest ? m1_tdata : m0_tdata;
  wire [1:0] f_room = f_dest ? n1 : n0;
  always @(posedge clk)
    if (rst) begin f_in <= 0; f_done <= 0; end
    else if (!f_in && !f_done && f_acc && f_pick) begin
      f_in <= 1; f_dest <= f_tdest; f_data <= f_src ? s1_tdata : s0_tdata; f_ahead <= f_flow - f_leave_now;
    end else if (f_in && f_lv) begin
      if (f_ahead == 2'd0) begin f_in <= 0; f_done <= 1; end
      else f_ahead <= f_ahead - 2'd1;
    end
  always @(posedge clk) if (!rst) begin
    if (f_in) a_inside: assert(f_room > f_ahead);                        // no drop: still inside
    if (f_in && f_lv && f_ahead == 2'd0) a_turn: assert(f_out == f_data); // its turn: it leaves, unchanged
    a_owed0: assert(!(o0 && (m0_tid ? fl10 : fl00) == 2'd0));            // nothing leaves output 0 unowed
    a_owed1: assert(!(o1 && (m1_tid ? fl11 : fl01) == 2'd0));            // nothing leaves output 1 unowed
  end
`endif
endmodule
ConceptWhich tracker line is wrong?
ConceptWhy does "every offer is eventually taken" need a fairness assumption on tready?
Read the waveformIn the fixed-priority trace, when does input 1 start waiting, and how many input-0 packets are accepted before a_wait fires?
BUG_FIXED_PRIO: input 1 waits while input 0 streams
012345678clkfrees0_tvalidfrees0_tdests0_treadyfrees1_tvalidfrees1_tdests1_treadyfreem0_treadywait101234560input 1 starts waitingwait > 5reported step 8

Measured, bmc depth 20: FAIL at reported step 8 on a_wait. 4 input-0 packets are accepted while input 1 waits.

Remember

  • End to end: one symbolic packet, tagged by the moment of acceptance, counted through its flow.
  • Order and payload are one check at its turn; an owed-count check catches duplicates and wrong outputs.
  • A fixed-priority arbiter passes every safety check; starvation is liveness.
  • Ship a bounded wait under a named stand-in for fairness, with the bound from a measured cover.