Skip to content

Commit fc3ea7f

Browse files
authored
sim.rs_mesh: heap-backed adjacency and stats, 16-node stress ring (#21)
1 parent 3d8e2d4 commit fc3ea7f

4 files changed

Lines changed: 83 additions & 23 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,29 @@ Zig helpers for the wire formats of **[ethp2p](https://github.com/ethp2p/ethp2p)
2727
| RS emit planner (fair dispatch heap) | [`broadcast/rs/emit.go`](https://github.com/ethp2p/ethp2p/blob/main/broadcast/rs/emit.go) | `layer.emit_planner` |
2828
| RS parity encode (klauspost-default matrix) | [klauspost/reedsolomon](https://github.com/klauspost/reedsolomon) via ethp2p RS strategy | `layer.rs_encode`, `ReedSolomon`, `decodeMessage` |
2929
| RS unified strategy (per-session) | [`broadcast/rs/strategy.go`](https://github.com/ethp2p/ethp2p/blob/main/broadcast/rs/strategy.go) | `layer.rs_strategy` |
30-
| Abstract RS mesh (strategy-only; Go `TestNetwork` 0–1, 4-node ring, eight-node ring under stress) | [`sim/scenario_test.go`](https://github.com/ethp2p/ethp2p/blob/main/sim/scenario_test.go) | `sim.rs_mesh`, `zig build simtest` |
30+
| Abstract RS mesh (heap-backed adjacency + `PeerSessionStats`; cap `MaxMeshNodes`; Go `TestNetwork` 0–1, 4-node ring; 8- / 16-node rings under stress) | [`sim/scenario_test.go`](https://github.com/ethp2p/ethp2p/blob/main/sim/scenario_test.go) | `sim.rs_mesh`, `zig build simtest` |
3131
| Gossipsub sim publish bytes (same layout as Go `encodeGossipsubMessage`) + default topic | [`sim/strategy_gossipsub.go`](https://github.com/ethp2p/ethp2p/blob/main/sim/strategy_gossipsub.go) | `sim.gossipsub_transport` |
3232
| Abstract topic fanout + per-peer inboxes (no protobuf RPC) | same driver `Publish` / subscribe mesh | `sim.gossipsub_protocol` |
3333
| Encode + fanout helper | `GossipsubNode.Publish` + topic delivery | `sim.gossipsub_broadcast` |
3434
| App payload envelope entry point (re-export) | `encodeGossipsubMessage` | `broadcast.gossip` |
3535
| Gossipsim cross-checks (golden envelope, mesh fanout, `broadcast.gossip` vs transport) || `sim.gossipsub_interop` |
3636
| Gossipsub `ControlIHave` / `ControlIWant` protobuf bodies (subset of [libp2p `rpc.proto`](https://github.com/libp2p/go-libp2p-pubsub/blob/master/pb/rpc.proto)) | `ControlMessage` nested fields | `sim.gossipsub_rpc_pb`, `proto/gossipsub_rpc.proto` |
3737
| Gossipsub top-level `RPC` with `control` only (field 3) | length-delimited `RPC` shell for stream payloads | `sim.gossipsub_rpc_pb` (`encodeRpcEnvelopeControl`, `decodeRpcControlOnly`) |
38-
| **Still open** (see [issues](#pending-work)) | Full gossipsub `RPC`, libp2p/simnet host, RLNC, larger RS mesh graphs, optional channel-style event loop / `VerdictPending` for non-RS schemes ||
38+
| **Still open** (see [issues](#pending-work)) | Full gossipsub `RPC`, libp2p/simnet host, RLNC, optional channel-style event loop / `VerdictPending` for non-RS schemes ||
3939

4040
## Pending work
4141

42-
**On `main` today:** wire + layer RS strategy; `layer.dedup` / `layer.dedup_registry` / `layer.verify_queue` / `layer.verify_workers`; `broadcast.*` (engine, channel, `relay_async_verify`, verified + unverified relay ingest); abstract RS mesh (2-, 4-, 6-node; optional **stress** adds six-node budget and **eight-node ring**); gossipsim stack; gossipsub `ControlIHave` / `ControlIWant` plus **`RPC` control-only envelope** helpers in `sim.gossipsub_rpc_pb`. CI enforces `build.zig.zon` `minimum_zig_version` vs workflow `ZIG_VERSION`; `just check-zig-ci-align` matches locally. Default `zig build test` stays fast.
42+
**On `main` today:** wire + layer RS strategy; `layer.dedup` / `layer.dedup_registry` / `layer.verify_queue` / `layer.verify_workers`; `broadcast.*` (engine, channel, `relay_async_verify`, verified + unverified relay ingest); abstract RS mesh (**heap-backed**, 2-, 4-, 6-node default; **stress** adds higher six-node budget plus **8- and 16-node rings**); gossipsim stack; gossipsub `ControlIHave` / `ControlIWant` plus **`RPC` control-only envelope** helpers in `sim.gossipsub_rpc_pb`. CI enforces `build.zig.zon` `minimum_zig_version` vs workflow `ZIG_VERSION`; `just check-zig-ci-align` matches locally. Default `zig build test` stays fast.
4343

44-
**Suggested next:** [#13](https://github.com/ch4r10t33r/zig-ethp2p/issues/13)extend `sim/rs_mesh.zig` with larger topologies and budgets aligned with Go `sim/scenario_test.go` (builds on existing mesh tests without new transport).
44+
**Suggested next:** [#12](https://github.com/ch4r10t33r/zig-ethp2p/issues/12)full gossipsub `RPC`, libp2p streams, simnet/QUIC-style host.
4545

4646
**Tracked issues** (roadmap, not exhaustive):
4747

4848
| Issue | Status / topic |
4949
|-------|----------------|
5050
| [#11](https://github.com/ch4r10t33r/zig-ethp2p/issues/11) | **Closed** — engine dedup + async verify ingest (`relayIngestChunk*Engine`, `RelayAsyncVerifier.initBound`, `sessionDecodeClearEngineDedup`); RS verify stays sync (no `VerdictPending`) |
5151
| [#12](https://github.com/ch4r10t33r/zig-ethp2p/issues/12) | Open — full gossipsub `RPC`, libp2p streams, simnet/QUIC-style host |
52-
| [#13](https://github.com/ch4r10t33r/zig-ethp2p/issues/13) | OpenRS abstract mesh: larger graphs & Go scalability (**in progress**) |
52+
| [#13](https://github.com/ch4r10t33r/zig-ethp2p/issues/13) | **Closed**heap-backed `sim/rs_mesh` (`MaxMeshNodes`), 16-node ring stress (Go `TestNetwork` remains the default topology reference) |
5353
| [#14](https://github.com/ch4r10t33r/zig-ethp2p/issues/14) | Open — RLNC and additional EC `Scheme` types |
5454
| [#15](https://github.com/ch4r10t33r/zig-ethp2p/issues/15) | **Closed**`UPSTREAM.md` Zig toolchain note, CI `minimum_zig_version` check, `just check-zig-ci-align` |
5555

@@ -63,7 +63,7 @@ Zig helpers for the wire formats of **[ethp2p](https://github.com/ethp2p/ethp2p)
6363
zig build
6464
zig build test # wire, layer, broadcast, sim (default CI)
6565
zig build simtest # alias of `test` (mesh-focused name)
66-
zig build test-stress # same tests with `ZIG_ETHP2P_STRESS=1` (longer RS mesh case)
66+
zig build test-stress # `ZIG_ETHP2P_STRESS=1` (longer RS mesh + 8-/16-node ring cases)
6767
```
6868

6969
Add as a dependency and import the module `zig_ethp2p` (see `build.zig`).

UPSTREAM.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ When updating:
2626

2727
## Abstract mesh tests
2828

29-
`src/sim/rs_mesh.zig` runs the same RS **settings and graph topologies** as `sim/scenario_test.go` (`TestNetwork` RS / RS-ChunkLen) against `layer.RsStrategy` in-process (no libp2p, no Go simnet). `zig build test` and `zig build simtest` both execute them. With `ZIG_ETHP2P_STRESS=1` (see `zig build test-stress`), an additional 6-node case uses a higher round budget.
29+
`src/sim/rs_mesh.zig` runs the same RS **settings and graph topologies** as `sim/scenario_test.go` (`TestNetwork` RS / RS-ChunkLen) against `layer.RsStrategy` in-process (no libp2p, no Go simnet). Adjacency and per-peer stats are **heap-allocated** (`MaxMeshNodes` cap). `zig build test` and `zig build simtest` both execute them. With `ZIG_ETHP2P_STRESS=1` (see `zig build test-stress`), extra cases use higher round budgets and add **8- and 16-node** ring graphs beyond the Go file’s largest fixed topology.
3030

3131
## Specifications
3232

build.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ pub fn build(b: *std.Build) void {
3333
const run_stress = b.addRunArtifact(lib_tests);
3434
run_stress.setEnvironmentVariable("ZIG_ETHP2P_STRESS", "1");
3535
run_stress.has_side_effects = true;
36-
const stress_step = b.step("test-stress", "Run tests with ZIG_ETHP2P_STRESS=1 (longer RS mesh)");
36+
const stress_step = b.step("test-stress", "Run tests with ZIG_ETHP2P_STRESS=1 (longer RS mesh, 8-/16-node rings)");
3737
stress_step.dependOn(&run_stress.step);
3838
}

src/sim/rs_mesh.zig

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Abstract RS mesh (“simnet-style”) exercises `layer.RsStrategy` with the same RS settings and
22
//! topologies as ethp2p `sim/scenario_test.go` `TestNetwork`, without libp2p or Go simnet.
33
//!
4+
//! Graph structure and `PeerSessionStats` matrices are heap-backed (`MaxMeshNodes` upper bound).
45
//! This validates zig-ethp2p’s strategy layer under multi-hop chunk forwarding; it is not a UDP
56
//! timing simulation.
67

@@ -40,26 +41,50 @@ const MeshNode = struct {
4041
strat: RsStrategy = undefined,
4142
};
4243

44+
/// Upper bound on `MeshParams.node_count` for abstract mesh sims (heap use is O(n²) for stats + edges).
45+
pub const MaxMeshNodes = 256;
46+
4347
/// Run abstract RS broadcast until every non-origin node decodes `params.payload`, or `max_rounds`.
4448
pub fn runAbstractRsMesh(gpa: Allocator, params: MeshParams) !void {
4549
const n = params.node_count;
4650
std.debug.assert(n >= 2);
47-
std.debug.assert(n <= MaxNodes);
51+
if (n > MaxMeshNodes) return error.MeshTooLarge;
4852

4953
var nodes = try gpa.alloc(MeshNode, n);
5054
defer gpa.free(nodes);
5155

52-
var deg: [MaxNodes]u8 = .{0} ** MaxNodes;
53-
var neigh: [MaxNodes][MaxNodes]u8 = .{.{0} ** MaxNodes} ** MaxNodes;
56+
var deg = try gpa.alloc(usize, n);
57+
defer gpa.free(deg);
58+
@memset(deg, 0);
5459

5560
for (params.edges) |e| {
5661
std.debug.assert(e.a < n and e.b < n and e.a != e.b);
57-
neigh[e.a][deg[e.a]] = @intCast(e.b);
5862
deg[e.a] += 1;
59-
neigh[e.b][deg[e.b]] = @intCast(e.a);
6063
deg[e.b] += 1;
6164
}
6265

66+
var neigh = try gpa.alloc([]usize, n);
67+
defer {
68+
for (neigh) |row| gpa.free(row);
69+
gpa.free(neigh);
70+
}
71+
for (0..n) |i| {
72+
neigh[i] = try gpa.alloc(usize, deg[i]);
73+
}
74+
75+
var cursor = try gpa.alloc(usize, n);
76+
defer gpa.free(cursor);
77+
@memset(cursor, 0);
78+
@memset(deg, 0);
79+
80+
for (params.edges) |e| {
81+
neigh[e.a][cursor[e.a]] = e.b;
82+
cursor[e.a] += 1;
83+
neigh[e.b][cursor[e.b]] = e.a;
84+
cursor[e.b] += 1;
85+
}
86+
for (0..n) |i| deg[i] = neigh[i].len;
87+
6388
for (nodes, 0..) |*node, i| {
6489
node.id = nodeId(&node.id_buf, i);
6590
}
@@ -79,13 +104,18 @@ pub fn runAbstractRsMesh(gpa: Allocator, params: MeshParams) !void {
79104
nodes[built].strat = try RsStrategy.newRelay(gpa, params.cfg, &nodes[0].strat.preamble);
80105
}
81106

82-
var stats: [MaxNodes][MaxNodes]broadcast_types.PeerSessionStats = undefined;
83-
for (&stats) |*row| {
84-
for (row) |*cell| cell.* = .{ .peer_id = &.{} };
107+
var stats = try gpa.alloc([]broadcast_types.PeerSessionStats, n);
108+
defer {
109+
for (stats) |row| gpa.free(row);
110+
gpa.free(stats);
111+
}
112+
for (0..n) |i| {
113+
stats[i] = try gpa.alloc(broadcast_types.PeerSessionStats, n);
114+
for (stats[i]) |*cell| cell.* = .{ .peer_id = &.{} };
85115
}
86116

87117
for (nodes, 0..) |*dst, di| {
88-
var k: u8 = 0;
118+
var k: usize = 0;
89119
while (k < deg[di]) : (k += 1) {
90120
const j = neigh[di][k];
91121
try dst.strat.attachPeer(nodes[j].id, &stats[di][j]);
@@ -114,7 +144,7 @@ pub fn runAbstractRsMesh(gpa: Allocator, params: MeshParams) !void {
114144
}
115145
}
116146

117-
try exchangeRouting(gpa, nodes[0..n], neigh, deg);
147+
try exchangeRouting(gpa, nodes[0..n], neigh[0..n], deg[0..n]);
118148

119149
var all = true;
120150
for (1..n) |j| {
@@ -137,20 +167,19 @@ pub fn runAbstractRsMesh(gpa: Allocator, params: MeshParams) !void {
137167
}
138168
}
139169

140-
const MaxNodes = 8;
141-
142170
fn exchangeRouting(
143171
gpa: Allocator,
144172
nodes: []MeshNode,
145-
neigh: [MaxNodes][MaxNodes]u8,
146-
deg: [MaxNodes]u8,
173+
neigh: []const []const usize,
174+
deg: []const usize,
147175
) !void {
176+
std.debug.assert(neigh.len == nodes.len and deg.len == nodes.len);
148177
for (nodes, 0..) |*src, si| {
149178
const pr = try src.strat.pollRouting(gpa, false);
150179
if (!pr.emit) continue;
151180
const bm = pr.bitmap orelse continue;
152181
defer bm.deinit(gpa);
153-
var k: u8 = 0;
182+
var k: usize = 0;
154183
while (k < deg[si]) : (k += 1) {
155184
const j = neigh[si][k];
156185
const cancels = try nodes[j].strat.routingUpdate(src.id, bm);
@@ -322,3 +351,34 @@ test "abstract RS mesh eight nodes ring env stress (large-network scale)" {
322351
.max_rounds = 8_000_000,
323352
});
324353
}
354+
355+
fn ringEdges(comptime node_count: usize) [node_count]Edge {
356+
var out: [node_count]Edge = undefined;
357+
for (0..node_count) |i| {
358+
out[i] = .{ .a = i, .b = (i + 1) % node_count };
359+
}
360+
return out;
361+
}
362+
363+
// Heap-backed mesh supports rings beyond the old fixed `MaxNodes`; stress-only.
364+
test "abstract RS mesh sixteen nodes ring env stress" {
365+
const builtin = @import("builtin");
366+
if (builtin.os.tag == .windows) return;
367+
368+
const gpa = std.testing.allocator;
369+
const env = std.process.getEnvVarOwned(gpa, "ZIG_ETHP2P_STRESS") catch return;
370+
defer gpa.free(env);
371+
if (!std.mem.eql(u8, env, "1")) return;
372+
373+
const edges = comptime ringEdges(16);
374+
var payload: [10 * 1024]u8 = undefined;
375+
fillPayload(&payload);
376+
377+
try runAbstractRsMesh(gpa, .{
378+
.node_count = 16,
379+
.edges = &edges,
380+
.cfg = rsCfgDefault(),
381+
.payload = &payload,
382+
.max_rounds = 25_000_000,
383+
});
384+
}

0 commit comments

Comments
 (0)