diff --git a/README.md b/README.md index ddb6b83..a2bcf9b 100644 --- a/README.md +++ b/README.md @@ -108,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/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..8b6da14 100644 --- a/src/main.zig +++ b/src/main.zig @@ -34,6 +34,18 @@ pub fn main() !void { // Skip program name _ = args_iter.next(); + // Initialize network from environment (YAM_SIGNET) + yam.network.initFromEnv(allocator) catch |err| { + std.debug.print("Error: Invalid YAM_SIGNET challenge: {}\n", .{err}); + return; + }; + 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, + }); + } + // Get subcommand const cmd_str = args_iter.next() orelse { // No args = explore mode diff --git a/src/network.zig b/src/network.zig new file mode 100644 index 0000000..ba8f145 --- /dev/null +++ b/src/network.zig @@ -0,0 +1,142 @@ +// Network.zig - Network configuration (mainnet/signet) +// +// 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. + +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; +} + +/// 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); + } +} + +/// 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; + } +} + +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) {