Skip to content
Open
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<challenge_hex> ./zig-out/bin/yam

# Works with any command
YAM_SIGNET=1 ./zig-out/bin/yam broadcast 0100000001...
```

## Export Formats

**nodes.csv**
Expand Down
2 changes: 1 addition & 1 deletion src/courier.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions src/explorer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 142 additions & 0 deletions src/network.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Network.zig - Network configuration (mainnet/signet)
//
// Signet mode: Set YAM_SIGNET=1 for default public signet, or
// YAM_SIGNET=<challenge_hex> 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);
}
4 changes: 3 additions & 1 deletion src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 33 additions & 3 deletions src/scout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ip:port>' 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;
};
Expand Down Expand Up @@ -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) {
Expand Down