Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,23 @@ on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
branches: [main]

jobs:
lint-and-test:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Zig
uses: mlugg/setup-zig@v2.0.5
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0

- name: Format check
- name: Format
run: zig fmt --check .

- name: Test
run: zig build test
- name: Test (default options)
run: zig build test --summary all

- name: Test (experimental topic wire off)
run: zig build test --summary all -Dexperimental_topic_wire=false
47 changes: 47 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Release

on:
workflow_dispatch:
inputs:
tag:
description: "Git tag to publish (must match semver, e.g. v0.1.0)"
required: true
default: v0.1.0

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: mlugg/setup-zig@v2
with:
version: 0.16.0

- name: Verify release matches build.zig.zon
run: |
set -euo pipefail
want="${{ github.event.inputs.tag }}"
want="${want#v}"
got="$(grep -E '^\s*\.version\s*=' build.zig.zon | head -1 | sed -E 's/.*"(.*)".*/\1/')"
if [ "$want" != "$got" ]; then
echo "Tag version ($want) must match build.zig.zon version ($got). Bump build.zig.zon or pick another tag."
exit 1
fi

- name: Format and tests
run: |
zig fmt --check .
zig build test --summary all
zig build test --summary all -Dexperimental_topic_wire=false

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
zig-cache/
zig-out/
.zig-cache/
zig-pkg/
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The layout is modular; several areas are still stubs (`NotImplemented`) while pr
| Varint | `varint` | Unsigned LEB128 (`u64`), minimal encoding, strict decode |
| RLP | `rlp` | Strings and lists for devp2p payloads |
| Wire | `wire` | `MessageKind`, `varint`, `packet`, `message`, `message_crypto` |
| Message | `message` | Ordinary message RLP encode/decode (ping/pong/findnode/nodes/talk; REGTOPIC/TICKET/REGCONFIRMATION/TOPICQUERY are experimental per wire spec) |
| Message | `message` | Ordinary message RLP encode/decode; REGTOPIC/TICKET/REGCONFIRMATION/TOPICQUERY (0x07–0x0a) decode only when `-Dexperimental_topic_wire=true` (default). Encode helpers always exist for tooling. |
| Message crypto | `message_crypto` | AES-128-GCM for ordinary message ciphertext (spec section 2.3) |
| ENR | `enr` | EIP-778 textual `enr:` decode (base64url + RLP layout checks) |
| Handshake | `handshake` (alias `crypto`) | HKDF session keys, identity-proof SHA-256 |
Expand All @@ -38,6 +38,12 @@ zig build test
zig fmt --check .
```

Build option (non-final [topic advertisement](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) wire):

```sh
zig build test -Dexperimental_topic_wire=false
```

## Use as a dependency

Add `zig_discv5` as a path or URL dependency in your `build.zig.zon`, then import the module in `build.zig`:
Expand All @@ -53,7 +59,16 @@ Exact `dependency` shape depends on whether you consume the package from a git U

## Continuous integration

GitHub Actions runs `zig fmt --check .` and `zig build test` on pushes and pull requests.
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — on pushes and PRs to `main`: format check, `zig build test`, and the same tests with `-Dexperimental_topic_wire=false`.

## Releases

Manual release (creates a GitHub Release and tag at the current commit):

1. Ensure `version` in `build.zig.zon` matches the tag without the `v` prefix (e.g. tag `v0.1.0` ↔ version `0.1.0`).
2. Actions → **Release** → **Run workflow** → set tag (default `v0.1.0`).

Workflow: [`.github/workflows/release.yml`](.github/workflows/release.yml).

## Specification

Expand Down
10 changes: 10 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

const experimental_topic_wire = b.option(
bool,
"experimental_topic_wire",
"Decode non-final discv5 topic wire (0x07–0x0a). When false, those message types are rejected as unknown.",
) orelse true;

const build_options = b.addOptions();
build_options.addOption(bool, "experimental_topic_wire", experimental_topic_wire);

const zig_varint_mod = b.dependency("zig_varint", .{
.target = target,
.optimize = optimize,
Expand All @@ -16,6 +25,7 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
});
mod.addImport("zig_varint", zig_varint_mod);
mod.addImport("build_options", build_options.createModule());

const unit_tests = b.addTest(.{
.name = "zig-discv5-tests",
Expand Down
33 changes: 29 additions & 4 deletions src/message.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! RLP protocol messages (`message-type` byte + `message-data` list) per [discv5-wire](https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md).

const std = @import("std");
const build_options = @import("build_options");
const rlp = @import("rlp.zig");

pub const type_ping: u8 = 0x01;
Expand Down Expand Up @@ -157,10 +158,22 @@ pub fn decodePlaintext(plaintext: []const u8, allocator: std.mem.Allocator) (Dec
type_nodes => .{ .nodes = try decodeNodes(allocator, body) },
type_talkreq => .{ .talkreq = try decodeTalkRequest(body) },
type_talkresp => .{ .talkresp = try decodeTalkResponse(body) },
type_regtopic => .{ .regtopic = try decodeRegtopic(body) },
type_ticket => .{ .ticket = try decodeTicketResponse(body) },
type_regconfirmation => .{ .regconfirmation = try decodeRegconfirmation(body) },
type_topicquery => .{ .topicquery = try decodeTopicquery(body) },
type_regtopic => if (build_options.experimental_topic_wire)
.{ .regtopic = try decodeRegtopic(body) }
else
return error.UnknownKind,
type_ticket => if (build_options.experimental_topic_wire)
.{ .ticket = try decodeTicketResponse(body) }
else
return error.UnknownKind,
type_regconfirmation => if (build_options.experimental_topic_wire)
.{ .regconfirmation = try decodeRegconfirmation(body) }
else
return error.UnknownKind,
type_topicquery => if (build_options.experimental_topic_wire)
.{ .topicquery = try decodeTopicquery(body) }
else
return error.UnknownKind,
else => error.UnknownKind,
};
}
Expand Down Expand Up @@ -668,6 +681,8 @@ test "nodes talk roundtrip" {
}

test "topic wire roundtrip" {
if (!build_options.experimental_topic_wire) return error.SkipZigTest;

const alloc = std.testing.allocator;

const th = [_]u8{0xbb} ** 32;
Expand Down Expand Up @@ -697,3 +712,13 @@ test "topic wire roundtrip" {
defer dec_q.deinit(alloc);
try std.testing.expect(dec_q == .topicquery);
}

test "topic wire decode disabled rejects experimental kinds" {
if (build_options.experimental_topic_wire) return error.SkipZigTest;

const alloc = std.testing.allocator;
const th = [_]u8{0xbb} ** 32;
const enc_r = try encodeRegtopicPlaintext(alloc, &.{0x01}, &th, "enr-bytes", "");
defer alloc.free(enc_r);
try std.testing.expectError(error.UnknownKind, decodePlaintext(enc_r, alloc));
}
3 changes: 2 additions & 1 deletion src/topic.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Topic advertisement data structures per [discv5-theory](https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md)
//! (topic table, FIFO topic queues, registration throttling).
//!
//! Wire messages for topics (REGTOPIC, TICKET, …) are marked non-final in discv5-wire; see `message.zig`.
//! Wire messages for topics (REGTOPIC, TICKET, …) are non-final; inbound decode is gated by the
//! `experimental_topic_wire` build option in `message.zig` / `wire.zig`.

const std = @import("std");
const routing = @import("routing.zig");
Expand Down
15 changes: 10 additions & 5 deletions src/wire.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Wire encoding and message framing ([discv5-wire](https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md)).

const std = @import("std");
const build_options = @import("build_options");
const errors = @import("errors.zig");

pub const varint = @import("varint.zig");
Expand Down Expand Up @@ -35,10 +36,10 @@ pub const MessageKind = enum(u8) {
0x04 => .nodes,
0x05 => .talkreq,
0x06 => .talkresp,
0x07 => .regtopic,
0x08 => .ticket,
0x09 => .regconfirmation,
0x0a => .topicquery,
0x07 => if (build_options.experimental_topic_wire) .regtopic else null,
0x08 => if (build_options.experimental_topic_wire) .ticket else null,
0x09 => if (build_options.experimental_topic_wire) .regconfirmation else null,
0x0a => if (build_options.experimental_topic_wire) .topicquery else null,
else => null,
};
}
Expand All @@ -54,6 +55,10 @@ test "wire stub" {

test "message kind parse" {
try std.testing.expect(MessageKind.parse(0x01).? == .ping);
try std.testing.expect(MessageKind.parse(0x0a).? == .topicquery);
if (build_options.experimental_topic_wire) {
try std.testing.expect(MessageKind.parse(0x0a).? == .topicquery);
} else {
try std.testing.expect(MessageKind.parse(0x0a) == null);
}
try std.testing.expect(MessageKind.parse(0xff) == null);
}
Loading