From e42ca830121a1750832e1ffbe28647cb0e002122 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Mon, 19 Jan 2026 15:34:47 -0600 Subject: [PATCH 1/2] add signet support --- README.md | 11 ++++ src/courier.zig | 2 +- src/explorer.zig | 6 +- src/main.zig | 31 +++++++-- src/network.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 4 +- src/scout.zig | 36 ++++++++++- 7 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 src/network.zig diff --git a/README.md b/README.md index ddb6b83..a9b6aea 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ WARNING: It has mostly been tested on MacOS. Windows has basic support. Linux su ./zig-out/bin/yam ``` +Signet (optional): +``` +./zig-out/bin/yam --signet explore +./zig-out/bin/yam --signet explore +``` + Commands: ``` discover, d Discover nodes via DNS seeds @@ -91,6 +97,11 @@ Status: ./zig-out/bin/yam broadcast [options] ``` +Signet: +``` +./zig-out/bin/yam --signet broadcast [options] +``` + Options: - `--peers, -p ` - number of peers (default: 8) - `--simultaneous, -s` - send to all peers at once (default: staggered) diff --git a/src/courier.zig b/src/courier.zig index e3d86bf..f4ba5c5 100644 --- a/src/courier.zig +++ b/src/courier.zig @@ -181,7 +181,7 @@ pub const Courier = struct { const header_ptr = std.mem.bytesAsValue(yam.MessageHeader, &header_buffer); const header = header_ptr.*; - if (header.magic != 0xD9B4BEF9) return error.InvalidMagic; + if (header.magic != yam.network.magic) return error.InvalidMagic; var payload: []u8 = &.{}; if (header.length > 0) { diff --git a/src/explorer.zig b/src/explorer.zig index 7f0dd8a..f8fddb1 100644 --- a/src/explorer.zig +++ b/src/explorer.zig @@ -504,11 +504,11 @@ pub const Explorer = struct { fn addNodeFromString(self: *Explorer, addr_str: []const u8) !usize { // Parse ip:port format - var port: u16 = 8333; // default Bitcoin port + var port: u16 = yam.network.default_port; var ip_str = addr_str; if (std.mem.lastIndexOfScalar(u8, addr_str, ':')) |colon_idx| { - port = std.fmt.parseInt(u16, addr_str[colon_idx + 1 ..], 10) catch 8333; + port = std.fmt.parseInt(u16, addr_str[colon_idx + 1 ..], 10) catch yam.network.default_port; ip_str = addr_str[0..colon_idx]; } @@ -1486,7 +1486,7 @@ pub const Explorer = struct { if (header_read < 24) return; const header = std.mem.bytesAsValue(yam.MessageHeader, &header_buf).*; - if (header.magic != 0xD9B4BEF9) return; + if (header.magic != yam.network.magic) return; var payload: [65536]u8 = undefined; const payload_len = @min(header.length, payload.len); diff --git a/src/main.zig b/src/main.zig index 5cd1be1..6918332 100644 --- a/src/main.zig +++ b/src/main.zig @@ -34,8 +34,20 @@ pub fn main() !void { // Skip program name _ = args_iter.next(); + // Check for --signet flag (must come before subcommand) + const signet_result = yam.network.parseSignetArgs(&args_iter) catch |err| { + std.debug.print("Error: Invalid signet challenge: {}\n", .{err}); + return; + }; + if (signet_result.enabled) { + std.debug.print("Signet mode: magic=0x{X:0>8} port={d}\n\n", .{ + yam.network.magic, + yam.network.default_port, + }); + } + // Get subcommand - const cmd_str = args_iter.next() orelse { + const cmd_str = signet_result.cmd_arg orelse { // No args = explore mode var explorer = try Explorer.init(allocator); defer explorer.deinit(); @@ -104,9 +116,19 @@ fn printUsage() void { \\Yam - Bitcoin P2P Network Tool \\ \\USAGE: - \\ yam broadcast [options] Broadcast a transaction - \\ yam explore Interactive network explorer (default) - \\ yam help Show this help + \\ yam [options] + \\ + \\OPTIONS: + \\ --signet [hex] Use signet network (optional custom challenge) + \\ + \\COMMANDS: + \\ broadcast Broadcast a transaction + \\ explore Interactive network explorer (default) + \\ help Show this help + \\ + \\EXAMPLES: + \\ yam Mainnet explorer + \\ yam broadcast 0100000001... Broadcast transaction \\ \\Run 'yam broadcast --help' for broadcast options. \\ @@ -114,6 +136,7 @@ fn printUsage() void { std.debug.print("{s}", .{usage}); } + fn printBroadcastUsage() void { const usage = \\USAGE: diff --git a/src/network.zig b/src/network.zig new file mode 100644 index 0000000..1cd405c --- /dev/null +++ b/src/network.zig @@ -0,0 +1,159 @@ +// Network.zig - Network configuration (mainnet/signet) +// +// Usage: yam --signet [command] +// +// These are "set once at startup" static variables - never modified after init. + +const std = @import("std"); + +// --------------------------------------------------------------------------- +// Static network configuration (set once at startup, never modified after) +// --------------------------------------------------------------------------- + +/// Public signet challenge (Bitcoin Core default) +pub const default_signet_challenge_hex = + "512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae"; + +/// Public signet DNS seeds (Bitcoin Core default) +pub const signet_dns_seeds = [_][]const u8{ + "seed.signet.bitcoin.sprovoost.nl", + "seed.signet.achownodes.xyz", +}; + +/// Network magic bytes (mainnet: 0xD9B4BEF9, signet: computed from challenge) +pub var magic: u32 = 0xD9B4BEF9; + +/// Default P2P port (mainnet: 8333, signet: 38333) +pub var default_port: u16 = 8333; + +/// Whether we're running in signet mode +pub var is_signet: bool = false; + +/// Whether to use DNS seeds for signet discovery +pub var has_signet_seeds: bool = false; + +// --------------------------------------------------------------------------- +// Runtime initialization (call from main before anything else) +// --------------------------------------------------------------------------- + +/// Initialize signet mode at runtime from --signet flag. +/// Must be called before any network operations. +pub fn initSignet(challenge_hex: []const u8) !void { + magic = try computeSignetMagic(challenge_hex); + default_port = 38333; + is_signet = true; + has_signet_seeds = false; +} + +/// Initialize default (public) signet. +pub fn initSignetDefault() !void { + try initSignet(default_signet_challenge_hex); + has_signet_seeds = true; +} + +pub const SignetParseResult = struct { + cmd_arg: ?[]const u8, + enabled: bool, +}; + +/// Parse leading --signet flag and initialize network settings. +/// Returns the command argument to use (if any). +pub fn parseSignetArgs(args_iter: anytype) !SignetParseResult { + const first_arg = args_iter.next(); + if (first_arg) |arg| { + if (std.mem.eql(u8, arg, "--signet")) { + const maybe_next = args_iter.next(); + if (maybe_next) |next| { + if (isCommandArg(next)) { + try initSignetDefault(); + return .{ .cmd_arg = next, .enabled = true }; + } + try initSignet(next); + return .{ .cmd_arg = args_iter.next(), .enabled = true }; + } + try initSignetDefault(); + return .{ .cmd_arg = args_iter.next(), .enabled = true }; + } + } + + return .{ .cmd_arg = first_arg, .enabled = false }; +} + +/// Compute signet magic from challenge hex. +/// Algorithm: first 4 bytes of SHA256d(CompactSize(len) ++ challenge_bytes) +fn computeSignetMagic(challenge_hex: []const u8) !u32 { + if (challenge_hex.len % 2 != 0) return error.InvalidHexLength; + const challenge_len: u64 = challenge_hex.len / 2; + + // Encode CompactSize length prefix (varint) + var prefix: [9]u8 = undefined; + const prefix_len = encodeCompactSize(&prefix, challenge_len); + + // SHA256d (double SHA256) with streaming input + var sha = std.crypto.hash.sha2.Sha256.init(.{}); + sha.update(prefix[0..prefix_len]); + + var i: usize = 0; + var one: [1]u8 = undefined; + while (i < challenge_hex.len) : (i += 2) { + one[0] = std.fmt.parseInt(u8, challenge_hex[i..][0..2], 16) catch + return error.InvalidHexChar; + sha.update(one[0..1]); + } + + var h1: [32]u8 = undefined; + sha.final(&h1); + + var h2: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(&h1, &h2, .{}); + + return std.mem.readInt(u32, h2[0..4], .little); +} + +fn encodeCompactSize(buf: *[9]u8, value: u64) usize { + if (value < 0xfd) { + buf[0] = @intCast(value); + return 1; + } else if (value <= 0xffff) { + buf[0] = 0xfd; + const v: u16 = @intCast(value); + buf[1] = @intCast(v & 0xff); + buf[2] = @intCast((v >> 8) & 0xff); + return 3; + } else if (value <= 0xffffffff) { + buf[0] = 0xfe; + const v: u32 = @intCast(value); + buf[1] = @intCast(v & 0xff); + buf[2] = @intCast((v >> 8) & 0xff); + buf[3] = @intCast((v >> 16) & 0xff); + buf[4] = @intCast((v >> 24) & 0xff); + return 5; + } else { + buf[0] = 0xff; + const v: u64 = value; + buf[1] = @intCast(v & 0xff); + buf[2] = @intCast((v >> 8) & 0xff); + buf[3] = @intCast((v >> 16) & 0xff); + buf[4] = @intCast((v >> 24) & 0xff); + buf[5] = @intCast((v >> 32) & 0xff); + buf[6] = @intCast((v >> 40) & 0xff); + buf[7] = @intCast((v >> 48) & 0xff); + buf[8] = @intCast((v >> 56) & 0xff); + return 9; + } +} + +fn isCommandArg(arg: []const u8) bool { + return std.mem.eql(u8, arg, "broadcast") or + std.mem.eql(u8, arg, "explore") or + std.mem.eql(u8, arg, "help") or + std.mem.eql(u8, arg, "--help") or + std.mem.eql(u8, arg, "-h"); +} + +test "compute signet magic from BIP-325 example" { + const challenge = + "512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be43051ae"; + const computed_magic = try computeSignetMagic(challenge); + try std.testing.expectEqual(@as(u32, 0xA553C67E), computed_magic); +} diff --git a/src/root.zig b/src/root.zig index 9ccc734..f27b20c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,15 +2,17 @@ // https://en.bitcoin.it/wiki/Protocol_documentation was used as the reference for this implementation. const std = @import("std"); +pub const network = @import("network.zig"); pub const MessageHeader = extern struct { - magic: u32 = 0xD9B4BEF9, + magic: u32, command: [12]u8, length: u32, checksum: u32, pub fn new(cmd: []const u8, payload_len: u32, payload_checksum: u32) MessageHeader { var header = MessageHeader{ + .magic = network.magic, .command = [_]u8{0} ** 12, .length = payload_len, .checksum = payload_checksum, diff --git a/src/scout.zig b/src/scout.zig index 5e2165f..405e05c 100644 --- a/src/scout.zig +++ b/src/scout.zig @@ -23,14 +23,44 @@ const fallback_peers = [_]struct { ip: []const u8, port: u16 }{ .{ .ip = "49.13.4.145", .port = 8333 }, }; -/// Discover peers via DNS seeds +/// Discover peers via DNS seeds (mainnet or public signet) +/// For custom signet returns empty list - use 'connect' command in explorer pub fn discoverPeers(allocator: std.mem.Allocator) !std.ArrayList(yam.PeerInfo) { var peers = std.ArrayList(yam.PeerInfo).empty; errdefer peers.deinit(allocator); + if (yam.network.is_signet) { + if (!yam.network.has_signet_seeds) { + std.debug.print("Signet mode: use 'connect ' to add peers\n", .{}); + return peers; + } + + for (yam.network.signet_dns_seeds) |seed| { + const addresses = std.net.getAddressList(allocator, seed, yam.network.default_port) catch |err| { + std.debug.print("DNS lookup failed for {s}: {}\n", .{ seed, err }); + continue; + }; + defer addresses.deinit(); + + for (addresses.addrs) |addr| { + // Only add IPv4 for now + if (addr.any.family == std.posix.AF.INET) { + try peers.append(allocator, .{ + .address = addr, + .services = 0, + .source = .dns_seed, + }); + } + } + } + + std.debug.print("Discovered {d} peers\n", .{peers.items.len}); + return peers; + } + // Try DNS seeds for (dns_seeds) |seed| { - const addresses = std.net.getAddressList(allocator, seed, 8333) catch |err| { + const addresses = std.net.getAddressList(allocator, seed, yam.network.default_port) catch |err| { std.debug.print("DNS lookup failed for {s}: {}\n", .{ seed, err }); continue; }; @@ -228,7 +258,7 @@ fn readMessage(stream: std.net.Stream, allocator: std.mem.Allocator) !struct { h const header_ptr = std.mem.bytesAsValue(yam.MessageHeader, &header_buffer); const header = header_ptr.*; - if (header.magic != 0xD9B4BEF9) return error.InvalidMagic; + if (header.magic != yam.network.magic) return error.InvalidMagic; var payload: []u8 = &.{}; if (header.length > 0) { From e327c68e7aaeefdf61c0a9b93d79a01782682944 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 22 Jan 2026 09:27:44 -0600 Subject: [PATCH 2/2] use env for configuring signet instead of flag --- README.md | 26 ++++++++++++++----------- src/main.zig | 27 ++++++++------------------ src/network.zig | 51 +++++++++++++++++-------------------------------- 3 files changed, 40 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index a9b6aea..a2bcf9b 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,6 @@ WARNING: It has mostly been tested on MacOS. Windows has basic support. Linux su ./zig-out/bin/yam ``` -Signet (optional): -``` -./zig-out/bin/yam --signet explore -./zig-out/bin/yam --signet explore -``` - Commands: ``` discover, d Discover nodes via DNS seeds @@ -97,11 +91,6 @@ Status: ./zig-out/bin/yam broadcast [options] ``` -Signet: -``` -./zig-out/bin/yam --signet broadcast [options] -``` - Options: - `--peers, -p ` - number of peers (default: 8) - `--simultaneous, -s` - send to all peers at once (default: staggered) @@ -119,6 +108,21 @@ Examples: ./zig-out/bin/yam broadcast 0100000001... --discover ``` +### Signet + +Set `YAM_SIGNET` environment variable to use signet instead of mainnet: + +``` +# Default public signet +YAM_SIGNET=1 ./zig-out/bin/yam + +# Custom signet (provide challenge hex) +YAM_SIGNET= ./zig-out/bin/yam + +# Works with any command +YAM_SIGNET=1 ./zig-out/bin/yam broadcast 0100000001... +``` + ## Export Formats **nodes.csv** diff --git a/src/main.zig b/src/main.zig index 6918332..8b6da14 100644 --- a/src/main.zig +++ b/src/main.zig @@ -34,12 +34,12 @@ pub fn main() !void { // Skip program name _ = args_iter.next(); - // Check for --signet flag (must come before subcommand) - const signet_result = yam.network.parseSignetArgs(&args_iter) catch |err| { - std.debug.print("Error: Invalid signet challenge: {}\n", .{err}); + // Initialize network from environment (YAM_SIGNET) + yam.network.initFromEnv(allocator) catch |err| { + std.debug.print("Error: Invalid YAM_SIGNET challenge: {}\n", .{err}); return; }; - if (signet_result.enabled) { + if (yam.network.is_signet) { std.debug.print("Signet mode: magic=0x{X:0>8} port={d}\n\n", .{ yam.network.magic, yam.network.default_port, @@ -47,7 +47,7 @@ pub fn main() !void { } // Get subcommand - const cmd_str = signet_result.cmd_arg orelse { + const cmd_str = args_iter.next() orelse { // No args = explore mode var explorer = try Explorer.init(allocator); defer explorer.deinit(); @@ -116,19 +116,9 @@ fn printUsage() void { \\Yam - Bitcoin P2P Network Tool \\ \\USAGE: - \\ yam [options] - \\ - \\OPTIONS: - \\ --signet [hex] Use signet network (optional custom challenge) - \\ - \\COMMANDS: - \\ broadcast Broadcast a transaction - \\ explore Interactive network explorer (default) - \\ help Show this help - \\ - \\EXAMPLES: - \\ yam Mainnet explorer - \\ yam broadcast 0100000001... Broadcast transaction + \\ yam broadcast [options] Broadcast a transaction + \\ yam explore Interactive network explorer (default) + \\ yam help Show this help \\ \\Run 'yam broadcast --help' for broadcast options. \\ @@ -136,7 +126,6 @@ fn printUsage() void { std.debug.print("{s}", .{usage}); } - fn printBroadcastUsage() void { const usage = \\USAGE: diff --git a/src/network.zig b/src/network.zig index 1cd405c..ba8f145 100644 --- a/src/network.zig +++ b/src/network.zig @@ -1,6 +1,7 @@ // Network.zig - Network configuration (mainnet/signet) // -// Usage: yam --signet [command] +// Signet mode: Set YAM_SIGNET=1 for default public signet, or +// YAM_SIGNET= for custom signet. // // These are "set once at startup" static variables - never modified after init. @@ -51,32 +52,22 @@ pub fn initSignetDefault() !void { has_signet_seeds = true; } -pub const SignetParseResult = struct { - cmd_arg: ?[]const u8, - enabled: bool, -}; - -/// Parse leading --signet flag and initialize network settings. -/// Returns the command argument to use (if any). -pub fn parseSignetArgs(args_iter: anytype) !SignetParseResult { - const first_arg = args_iter.next(); - if (first_arg) |arg| { - if (std.mem.eql(u8, arg, "--signet")) { - const maybe_next = args_iter.next(); - if (maybe_next) |next| { - if (isCommandArg(next)) { - try initSignetDefault(); - return .{ .cmd_arg = next, .enabled = true }; - } - try initSignet(next); - return .{ .cmd_arg = args_iter.next(), .enabled = true }; - } - try initSignetDefault(); - return .{ .cmd_arg = args_iter.next(), .enabled = true }; - } +/// Initialize network from YAM_SIGNET environment variable. +/// - Not set: mainnet (no-op) +/// - "1" or empty: default public signet +/// - Otherwise: custom signet challenge hex +pub fn initFromEnv(allocator: std.mem.Allocator) !void { + const val = std.process.getEnvVarOwned(allocator, "YAM_SIGNET") catch |err| switch (err) { + error.EnvironmentVariableNotFound => return, + else => return err, + }; + defer allocator.free(val); + + if (val.len == 0 or std.mem.eql(u8, val, "1")) { + try initSignetDefault(); + } else { + try initSignet(val); } - - return .{ .cmd_arg = first_arg, .enabled = false }; } /// Compute signet magic from challenge hex. @@ -143,14 +134,6 @@ fn encodeCompactSize(buf: *[9]u8, value: u64) usize { } } -fn isCommandArg(arg: []const u8) bool { - return std.mem.eql(u8, arg, "broadcast") or - std.mem.eql(u8, arg, "explore") or - std.mem.eql(u8, arg, "help") or - std.mem.eql(u8, arg, "--help") or - std.mem.eql(u8, arg, "-h"); -} - test "compute signet magic from BIP-325 example" { const challenge = "512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be43051ae";