db-16 — Observation

What does the simulator's output actually look like, and how do you read it by hand?

offset 0x00 :  44 53 45 36                 "DSE6"   (magic)
offset 0x04 :  78 00 00 00                 120      (event_count, u32 LE)

For --seed 42 --nodes 3 --rounds 20 the event count is 3 nodes × 20 rounds × 2 (send + recv) = 120.

A single Send event

Every Send is the start of a causal arc; every Recv is its endpoint. The first event in scenario A is a Send from node 0 at sim_time 0:

01                       kind = 1 = Send
00 00 00 00 00 00 00 00  sim_time   = 0
00 00 00 00              node       = 0    (sender)
?? 00 00 00              peer       = ?    (destination, computed from PRNG)
01 00 00 00 00 00 00 00  lamport    = 1    (Send rule: self += 1, then stamp)
01 00 00 00              vc_len     = 1
00 00 00 00 01 00 00 00 00 00 00 00   (node=0, counter=1)
01 00 00 00              payload_len = 1
??                       payload byte

Note the vector clock for a node that has only sent has a single entry (its own counter). Receivers' vector clocks grow as they merge incoming clocks.

A single Recv event

Recvs look identical except kind = 2 and peer is the source node:

02                       kind = 2 = Recv
?? ?? ?? ?? ?? ?? ?? ??  sim_time   = original send time + delay
01 00 00 00              node       = 1   (receiver)
00 00 00 00              peer       = 0   (sender of paired Send)
?? ?? ?? ?? ?? ?? ?? ??  lamport    = max(self_before, incoming) + 1
02 00 00 00              vc_len     = 2
00 00 00 00 01 00 00 00 00 00 00 00   merged entry for node 0
01 00 00 00 ?? 00 00 00 00 00 00 00   own counter, incremented
01 00 00 00              payload_len = 1
??                       payload byte (copied from send)

The number of VC entries grows as a node hears from new peers; in a 3-node, 20-round run each receiver will eventually have all 3 entries.

Hex walkthrough

./simctl --seed 42 --nodes 3 --rounds 20 | xxd | head

Read column-by-column:

00000000: 4453 4536 7800 0000  DSE6 . . . . . . . .      header
00000008: 01 00 00 00 00 00 00 00 00                      first Send: kind=1, sim_time=0
                                  00 00 00 00            node=0
00000014: ?? 00 00 00                                     peer
00000018: 01 00 00 00 00 00 00 00                         lamport=1
00000020: 01 00 00 00                                     vc_len=1
00000024: 00 00 00 00 01 00 00 00 00 00 00 00             vc entry (0 → 1)
00000030: 01 00 00 00                                     payload_len=1
00000034: ??                                              payload byte
00000035: 02 ...                                          next event (probably another Send at t=0)

The whole file for scenario A is 8156 bytes; scenario B is 45592 bytes.

What to learn from looking at it

  • Lamport values are non-decreasing within a node but may regress between nodes — that is healthy: nodes 0 and 1 can be ahead of node 2 if 2 hasn't sent or received yet.
  • The vector-clock entry for node i in node i's own events is strictly monotonic.
  • For any Send/Recv pair, the Recv's VC must dominate the Send's VC (> in VcOrd). This is exactly what check_causality asserts.
  • If you sort all events by sim_time you get a globally consistent "tape" — but events at the same sim_time are concurrent and have no inherent ordering between nodes. Deliveries are scheduled before sends within a tick by simulator policy, not by physics.

Cross-language reading

scripts/cross_test.sh prints the hex of the first 8 bytes (44534536 7800 0000 for scenario A). If three implementations agree on those 8 bytes but disagree on the rest, the suspect is almost always either (a) VC-entry order on the wire, or (b) heap tie-break by sender id.