Simulations

This page is generated: cargo run --release --example spore_sim -- markdown > docs/SIMULATIONS.md. CI fails if it is stale, so every number below came out of the run that wrote it.

examples/spore_sim.rs drives the real crate — Node::on_rx and Node::send, exactly as a bridge does — over a seeded event queue. What it measures is SPORE, not a model of SPORE. Every node is built with Node::from_seed, every coin comes from one seeded LCG, and ties break on a monotonic sequence number, so a run is reproducible from its seed and a metric that moves means the protocol moved.

The scenario that matters most is the one that fails. mixed-mtu is reported rather than asserted because it is a bug, and failing the build on a known bug teaches people to ignore the build. If it ever flips to delivered, that is a result.

Why generated at all: a hand-written page is a claim about numbers nobody re-checked. That is not hypothetical here — "one repair symbol takes 10% loss to 90%" sat in three documents for months when the measured figure was 66%, because the scenario behind it is reported rather than asserted and nothing compared the prose to a run.

At a glance

scenario result what it measures
line 1 of 1 (100%) Control: five nodes in a line, one MTU, no loss
mixed-mtu 0 of 1 (0%) A Wi-Fi island bridged to a LoRa hop with no per-hop splitting — the case the old design silently could not serve, kept as a red line rather than deleted
mixed-mtu-clamped 0 of 1 (0%) The same, with every node clamped to its own narrowest link: the fix that does not work, because the sender owns none of the narrow hop
mixed-mtu-linkfrag 1 of 1 (100%) The same topology with link fragmentation on
mtu-staircase 1 of 1 (100%) One envelope down a staircase of MTUs — 1400, 237, 54, 1400, 137 — with no node told what the rest of the path is made of
lossy-mesh-0pct 99 of 99 (100%) A hundred nodes in a random graph
lossy-mesh-10pct 99 of 99 (100%) A hundred nodes in a random graph
partition 1 of 1 (100%) Two clusters joined by a single node, with loss
file-multihop 1 of 1 (100%) A file three hops from its only seeder, with no chunk in any store between
fetch-abandoned 1 of 1 (100%) A fetcher that stops wanting a file, three ways: it vanishes, it cancels, or its link drops
push-2chunk-budget2 1 of 1 (100%) A publisher with four neighbours, one of whom wants the file
push-10chunk-budget2 1 of 1 (100%) A publisher with four neighbours, one of whom wants the file
linkfrag-loss-0pct 200 of 200 (100%) A 900-byte envelope over a 237-byte radio, 200 trials
linkfrag-loss-10pct 108 of 200 (54%) A 900-byte envelope over a 237-byte radio, 200 trials
linkfrag-5pct-repair1 167 of 200 (83%) The same link with erasure repair symbols added, to measure what they actually buy
linkfrag-10pct-repair1 132 of 200 (66%) The same link with erasure repair symbols added, to measure what they actually buy
linkfrag-20pct-repair1 94 of 200 (47%) The same link with erasure repair symbols added, to measure what they actually buy
linkfrag-10pct-repair2 175 of 200 (87%) The same link with erasure repair symbols added, to measure what they actually buy
hop-limit-17 1 of 1 (100%) A line of n nodes and one message end to end
hop-limit-19 0 of 1 (0%) A line of n nodes and one message end to end
malicious-want-depth8 8 of 24 (33%) One WANT forged with a deeper hop budget than policy allows, on a line longer than that policy
malicious-want-depth255 8 of 24 (33%) One WANT forged with a deeper hop budget than policy allows, on a line longer than that policy

line

Control: five nodes in a line, one MTU, no loss. If this fails, nothing else means anything.

5 nodes, 4 links · no link fragmentation

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n4(["node 4"])
  n0 ---|"1400 B, 10 ms"| n1
  n1 ---|"1400 B, 10 ms"| n2
  n2 ---|"1400 B, 10 ms"| n3
  n3 ---|"1400 B, 10 ms"| n4
measured
delivered 1 of 1
frames sent 4
bytes sent 524
dropped, frame too big for the link 0
dropped by link loss 0
first delivery 40 ms

control: 5-node line, uniform MTU

mixed-mtu

A Wi-Fi island bridged to a LoRa hop with no per-hop splitting — the case the old design silently could not serve, kept as a red line rather than deleted.

4 nodes, 3 links · no link fragmentation

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n0 ---|"1400 B, 5 ms"| n1
  n1 ---|"1400 B, 5 ms"| n2
  n2 ---|"237 B, 120 ms"| n3
measured
delivered 0 of 1
frames sent 1
bytes sent 4114
dropped, frame too big for the link 1
dropped by link loss 0

wifi island bridged to LoRa; M11-D must flip reached to 1

mixed-mtu-clamped

The same, with every node clamped to its own narrowest link: the fix that does not work, because the sender owns none of the narrow hop.

4 nodes, 3 links · no link fragmentation

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n0 ---|"1400 B, 5 ms"| n1
  n1 ---|"1400 B, 5 ms"| n2
  n2 ---|"237 B, 120 ms"| n3
measured
delivered 0 of 1
frames sent 1
bytes sent 4114
dropped, frame too big for the link 1
dropped by link loss 0

each node clamped to its own narrowest link; sender still owns none of the narrow hop

mixed-mtu-linkfrag

The same topology with link fragmentation on. The M11-D acceptance test.

4 nodes, 3 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n0 ---|"1400 B, 5 ms"| n1
  n1 ---|"1400 B, 5 ms"| n2
  n2 ---|"237 B, 120 ms"| n3
measured
delivered 1 of 1
frames sent 24
bytes sent 12489
dropped, frame too big for the link 0
dropped by link loss 0
first delivery 130 ms

same topology and message, link fragmentation on

mtu-staircase

One envelope down a staircase of MTUs — 1400, 237, 54, 1400, 137 — with no node told what the rest of the path is made of. Delivery must not depend on the order the links come in.

6 nodes, 5 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n4(["node 4"])
  n5(["node 5"])
  n0 ---|"1400 B, 5 ms"| n1
  n1 ---|"237 B, 5 ms"| n2
  n2 ---|"54 B, 5 ms"| n3
  n3 ---|"1400 B, 5 ms"| n4
  n4 ---|"137 B, 5 ms"| n5
measured
delivered 1 of 1
frames sent 37
bytes sent 5294
dropped, frame too big for the link 0
dropped by link loss 0
first delivery 25 ms

crossed five links of five different widths, splitting and reassembling at each

lossy-mesh-0pct

A hundred nodes in a random graph. Does a damped flood still reach everyone when links drop frames?

100 nodes, 198 links · no link fragmentation

Not drawn: 100 nodes in a random graph, 0 of 198 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 99 of 99
frames sent 297
bytes sent 39798
dropped, frame too big for the link 0
dropped by link loss 0

100 nodes, no loss

lossy-mesh-10pct

A hundred nodes in a random graph. Does a damped flood still reach everyone when links drop frames?

100 nodes, 197 links · no link fragmentation

Not drawn: 100 nodes in a random graph, 197 of 197 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 99 of 99
frames sent 295
bytes sent 39530
dropped, frame too big for the link 0
dropped by link loss 20

100 nodes, 10% loss

partition

Two clusters joined by a single node, with loss. Does a partition heal through one bridge, and what does it cost?

11 nodes, 10 links · no link fragmentation

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n4(["node 4"])
  n5(["node 5"])
  n6(["node 6"])
  n7(["node 7"])
  n8(["node 8"])
  n9(["node 9"])
  n10(["node 10"])
  n0 ---|"1400 B, 5% loss, 8 ms"| n1
  n1 ---|"1400 B, 5% loss, 8 ms"| n2
  n2 ---|"1400 B, 5% loss, 8 ms"| n3
  n3 ---|"1400 B, 5% loss, 8 ms"| n4
  n4 ---|"1400 B, 5% loss, 40 ms"| n5
  n5 ---|"1400 B, 5% loss, 40 ms"| n6
  n6 ---|"1400 B, 5% loss, 8 ms"| n7
  n7 ---|"1400 B, 5% loss, 8 ms"| n8
  n8 ---|"1400 B, 5% loss, 8 ms"| n9
  n9 ---|"1400 B, 5% loss, 8 ms"| n10
measured
delivered 1 of 1
frames sent 10
bytes sent 1290
dropped, frame too big for the link 0
dropped by link loss 0
first delivery 144 ms

two 5-node clusters joined by one bridge, 5% loss

file-multihop

A file three hops from its only seeder, with no chunk in any store between. The recursive-pull acceptance test.

4 nodes, 3 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n0 ---|"1400 B, 10 ms"| n1
  n1 ---|"1400 B, 10 ms"| n2
  n2 ---|"1400 B, 10 ms"| n3
measured
delivered 1 of 1
frames sent 13
bytes sent 12269
dropped, frame too big for the link 0
dropped by link loss 0

a file crossed three hops; the middle nodes cached what they carried

fetch-abandoned

A fetcher that stops wanting a file, three ways: it vanishes, it cancels, or its link drops. Measures what each leaves behind.

4 nodes, 3 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n0 ---|"1400 B, 100% loss, 10 ms"| n1
  n1 ---|"1400 B, 10 ms"| n2
  n2 ---|"1400 B, 10 ms"| n3
measured
delivered 1 of 1
frames sent 0
bytes sent 0
dropped, frame too big for the link 0
dropped by link loss 0

interests still open across the relays — vanished: 30, cancelled: 0, link dropped: 0

push-2chunk-budget2

A publisher with four neighbours, one of whom wants the file. Weighs what a push saves the fetcher against what it costs everyone else.

5 nodes, 4 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n4(["node 4"])
  n0 ---|"1400 B, 10 ms"| n1
  n0 ---|"1400 B, 10 ms"| n2
  n0 ---|"1400 B, 10 ms"| n3
  n0 ---|"1400 B, 10 ms"| n4
measured
delivered 1 of 1
frames sent 28
bytes sent 33776
dropped, frame too big for the link 0
dropped by link loss 0

0 round trips for the one fetcher; 25248 B sitting in three neighbours who never asked

push-10chunk-budget2

A publisher with four neighbours, one of whom wants the file. Weighs what a push saves the fetcher against what it costs everyone else.

5 nodes, 4 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n2(["node 2"])
  n3(["node 3"])
  n4(["node 4"])
  n0 ---|"1400 B, 10 ms"| n1
  n0 ---|"1400 B, 10 ms"| n2
  n0 ---|"1400 B, 10 ms"| n3
  n0 ---|"1400 B, 10 ms"| n4
measured
delivered 1 of 1
frames sent 36
bytes sent 42790
dropped, frame too big for the link 0
dropped by link loss 0

2 round trips for the one fetcher; 942 B sitting in three neighbours who never asked

linkfrag-loss-0pct

A 900-byte envelope over a 237-byte radio, 200 trials. Five pieces, all of which must arrive, so a little frame loss costs a lot of envelopes.

2 nodes, 1 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 20 ms"| n1
measured
delivered 200 of 200
frames sent 1000
bytes sent 208400
dropped, frame too big for the link 0
dropped by link loss 0

900 B over a 237-byte link, no loss

linkfrag-loss-10pct

A 900-byte envelope over a 237-byte radio, 200 trials. Five pieces, all of which must arrive, so a little frame loss costs a lot of envelopes.

2 nodes, 1 links · link fragmentation on

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 10% loss, 20 ms"| n1
measured
delivered 108 of 200
frames sent 1000
bytes sent 208400
dropped, frame too big for the link 0
dropped by link loss 103

900 B over a 237-byte link, 10% frame loss

linkfrag-5pct-repair1

The same link with erasure repair symbols added, to measure what they actually buy.

2 nodes, 1 links · link fragmentation on · 1 forced repair symbols

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 5% loss, 20 ms"| n1
measured
delivered 167 of 200
frames sent 1200
bytes sent 209800
dropped, frame too big for the link 0
dropped by link loss 55

900 B over a 237-byte link, erasure-coded repair

linkfrag-10pct-repair1

The same link with erasure repair symbols added, to measure what they actually buy.

2 nodes, 1 links · link fragmentation on · 1 forced repair symbols

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 10% loss, 20 ms"| n1
measured
delivered 132 of 200
frames sent 1200
bytes sent 209800
dropped, frame too big for the link 0
dropped by link loss 122

900 B over a 237-byte link, erasure-coded repair

linkfrag-20pct-repair1

The same link with erasure repair symbols added, to measure what they actually buy.

2 nodes, 1 links · link fragmentation on · 1 forced repair symbols

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 20% loss, 20 ms"| n1
measured
delivered 94 of 200
frames sent 1200
bytes sent 209800
dropped, frame too big for the link 0
dropped by link loss 225

900 B over a 237-byte link, erasure-coded repair

linkfrag-10pct-repair2

The same link with erasure repair symbols added, to measure what they actually buy.

2 nodes, 1 links · link fragmentation on · 2 forced repair symbols

flowchart LR
  n0(["node 0"])
  n1(["node 1"])
  n0 ---|"237 B, 10% loss, 20 ms"| n1
measured
delivered 175 of 200
frames sent 1400
bytes sent 211200
dropped, frame too big for the link 0
dropped by link loss 146

900 B over a 237-byte link, erasure-coded repair

hop-limit-17

A line of n nodes and one message end to end. Measures how far flooding alone reaches.

17 nodes, 16 links · no link fragmentation

Not drawn: 17 nodes in a random graph, 0 of 16 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 1 of 1
frames sent 16
bytes sent 1936
dropped, frame too big for the link 0
dropped by link loss 0
first delivery 32 ms

reached the far end

hop-limit-19

A line of n nodes and one message end to end. Measures how far flooding alone reaches.

19 nodes, 18 links · no link fragmentation

Not drawn: 19 nodes in a random graph, 0 of 18 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 0 of 1
frames sent 17
bytes sent 2057
dropped, frame too big for the link 0
dropped by link loss 0

hop budget exhausted before the far end — by flooding. pull would still get there

malicious-want-depth8

One WANT forged with a deeper hop budget than policy allows, on a line longer than that policy. Measures how far a stranger can make a mesh hunt.

24 nodes, 23 links · no link fragmentation

Not drawn: 24 nodes in a random graph, 0 of 23 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 8 of 24
frames sent 9
bytes sent 315
dropped, frame too big for the link 0
dropped by link loss 0

one WANT claiming depth 8: 8 of 24 nodes now hold an interest nobody can serve

malicious-want-depth255

One WANT forged with a deeper hop budget than policy allows, on a line longer than that policy. Measures how far a stranger can make a mesh hunt.

24 nodes, 23 links · no link fragmentation

Not drawn: 24 nodes in a random graph, 0 of 23 links dropping frames. The shape is not the point — that it is arbitrary is.

measured
delivered 8 of 24
frames sent 9
bytes sent 315
dropped, frame too big for the link 0
dropped by link loss 0

one WANT claiming depth 255: 8 of 24 nodes now hold an interest nobody can serve