Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <challenge_hex> explore
```

Commands:
```
discover, d Discover nodes via DNS seeds
Expand Down Expand Up @@ -91,6 +97,11 @@ Status:
./zig-out/bin/yam broadcast <tx_hex> [options]
```

Signet:
```
./zig-out/bin/yam --signet broadcast <tx_hex> [options]
```

Options:
- `--peers, -p <n>` - number of peers (default: 8)
- `--simultaneous, -s` - send to all peers at once (default: staggered)
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
31 changes: 27 additions & 4 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -104,16 +116,27 @@ fn printUsage() void {
\\Yam - Bitcoin P2P Network Tool
\\
\\USAGE:
\\ yam broadcast <tx_hex> [options] Broadcast a transaction
\\ yam explore Interactive network explorer (default)
\\ yam help Show this help
\\ yam <command> [options]
\\
\\OPTIONS:
\\ --signet [hex] Use signet network (optional custom challenge)
\\
\\COMMANDS:
\\ broadcast <tx_hex> 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.
\\
;
std.debug.print("{s}", .{usage});
}


fn printBroadcastUsage() void {
const usage =
\\USAGE:
Expand Down
159 changes: 159 additions & 0 deletions src/network.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Network.zig - Network configuration (mainnet/signet)
//
// Usage: yam --signet <challenge_hex> [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");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing discover command. Running yam --signet discover will incorrectly treat "discover" as a custom challenge hex, fail to parse it, and return an error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch thank you. This also demonstrated how invasive adding signet as a flag is. I changed it to an environment variable so it's less disruptive to overall logic / usage.


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