From 82bcb290a5af9bb9748020f03c1d8c0a4bdc8b44 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Sun, 10 May 2026 12:05:29 +0100 Subject: [PATCH] discv5: gate topic wire behind build flag, add CI and release - Add -Dexperimental_topic_wire (default true); when false, decodePlaintext rejects 0x07-0x0a and wire.MessageKind.parse hides those kinds. - Tests: skip topic roundtrip when off; assert UnknownKind when off. - CI: format + tests on main PR/push, including experimental_topic_wire=false. - Release: workflow_dispatch creates GitHub Release; verifies tag matches build.zig.zon version (default v0.1.0). - README: document option and workflows; gitignore zig-pkg/. Closes #24. --- .github/workflows/ci.yml | 20 +++++++-------- .github/workflows/release.yml | 47 +++++++++++++++++++++++++++++++++++ .gitignore | 1 + README.md | 19 ++++++++++++-- build.zig | 10 ++++++++ src/message.zig | 33 +++++++++++++++++++++--- src/topic.zig | 3 ++- src/wire.zig | 15 +++++++---- 8 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 814ca08..d18e560 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..456afed --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 9c8ce95..0a11329 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ zig-cache/ zig-out/ .zig-cache/ +zig-pkg/ diff --git a/README.md b/README.md index d42bbaf..63fdf18 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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`: @@ -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 diff --git a/build.zig b/build.zig index 6427f28..72aaaa3 100644 --- a/build.zig +++ b/build.zig @@ -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, @@ -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", diff --git a/src/message.zig b/src/message.zig index efe2c91..6d8982b 100644 --- a/src/message.zig +++ b/src/message.zig @@ -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; @@ -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, }; } @@ -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; @@ -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)); +} diff --git a/src/topic.zig b/src/topic.zig index 08c325b..995d9dd 100644 --- a/src/topic.zig +++ b/src/topic.zig @@ -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"); diff --git a/src/wire.zig b/src/wire.zig index 29882c5..2aa13fc 100644 --- a/src/wire.zig +++ b/src/wire.zig @@ -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"); @@ -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, }; } @@ -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); }