Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/addr #135

Merged
merged 22 commits into from
Oct 7, 2024
180 changes: 180 additions & 0 deletions src/network/protocol/messages/addr.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
const std = @import("std");
const protocol = @import("../lib.zig");

const Endian = std.builtin.Endian;
const Sha256 = std.crypto.hash.sha2.Sha256;

const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint;

pub const NetworkIPAddr = struct {
oxlime marked this conversation as resolved.
Show resolved Hide resolved
time: u32, // Unix epoch time
services: u64, // Services offered by the node
ip: [16]u8, // IPv6 address (including IPv4-mapped)
port: u16, // Port number

// NetworkIPAddr eql
pub fn eql(self: *const NetworkIPAddr, other: *const NetworkIPAddr) bool {
return self.time == other.time and self.services == other.services and std.mem.eql(u8, &self.ip, &other.ip) and self.port == other.port;
}

pub fn serializeWriter(self: *const NetworkIPAddr, writer: anytype) !void {
oxlime marked this conversation as resolved.
Show resolved Hide resolved
try writer.writeInt(u32, self.time, .little);
try writer.writeInt(u64, self.services, .little);
try writer.writeAll(&self.ip);
try writer.writeInt(u16, self.port, .big);
}

pub fn deserializeReader(reader: anytype) !NetworkIPAddr {
return NetworkIPAddr{
.time = try reader.readInt(u32, .little),
.services = try reader.readInt(u64, .little),
.ip = try reader.readBytesNoEof(16),
.port = try reader.readInt(u16, .big),
};
}
};

/// AddrMessage represents the "addr" message
///
/// https://developer.bitcoin.org/reference/p2p_networking.html#addr
pub const AddrMessage = struct {
ip_addresses: []NetworkIPAddr,

pub inline fn name() *const [12]u8 {
return protocol.CommandNames.ADDR ++ [_]u8{0} ** 8;
}

/// Returns the message checksum
///
/// Computed as `Sha256(Sha256(self.serialize()))[0..4]`
pub fn checksum(self: AddrMessage) [4]u8 {
var digest: [32]u8 = undefined;
var hasher = Sha256.init(.{});
const writer = hasher.writer();
self.serializeToWriter(writer) catch unreachable; // Sha256.write is infaible
hasher.final(&digest);

Sha256.hash(&digest, &digest, .{});
oxlime marked this conversation as resolved.
Show resolved Hide resolved

return digest[0..4].*;
}

/// Free the `user_agent` if there is one
pub fn deinit(self: AddrMessage, allocator: std.mem.Allocator) void {
allocator.free(self.ip_addresses);
}

/// Serialize the message as bytes and write them to the Writer.
///
/// `w` should be a valid `Writer`.
pub fn serializeToWriter(self: *const AddrMessage, w: anytype) !void {
comptime {
if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'.");
if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll'.");
oxlime marked this conversation as resolved.
Show resolved Hide resolved
}
try CompactSizeUint.new(self.ip_addresses.len).encodeToWriter(w);
for (self.ip_addresses) |*addr| {
try addr.serializeWriter(w);
}
}

/// Serialize a message as bytes and write them to the buffer.
///
/// buffer.len must be >= than self.hintSerializedLen()
pub fn serializeToSlice(self: *const AddrMessage, buffer: []u8) !void {
var fbs = std.io.fixedBufferStream(buffer);
const writer = fbs.writer();
try self.serializeToWriter(writer);
}

/// Serialize a message as bytes and return them.
pub fn serialize(self: *const AddrMessage, allocator: std.mem.Allocator) ![]u8 {
oxlime marked this conversation as resolved.
Show resolved Hide resolved
const serialized_len = self.hintSerializedLen();

const ret = try allocator.alloc(u8, serialized_len);
errdefer allocator.free(ret);

try self.serializeToSlice(ret);

return ret;
}

/// Deserialize a Reader bytes as a `AddrMessage`
pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !AddrMessage {
comptime {
if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'.");
if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'.");
if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'.");
oxlime marked this conversation as resolved.
Show resolved Hide resolved
}

const ip_address_count = try CompactSizeUint.decodeReader(r);

// Allocate space for IP addresses
const ip_addresses = try allocator.alloc(NetworkIPAddr, ip_address_count.value());
errdefer allocator.free(ip_addresses);

for (ip_addresses) |*ip_address| {
ip_address.* = try NetworkIPAddr.deserializeReader(r);
}

return AddrMessage{
.ip_addresses = ip_addresses,
};
}

/// Deserialize bytes into a `AddrMessage`
pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !AddrMessage {
oxlime marked this conversation as resolved.
Show resolved Hide resolved
var fbs = std.io.fixedBufferStream(bytes);
const reader = fbs.reader();
return try AddrMessage.deserializeReader(allocator, reader);
}

pub fn hintSerializedLen(self: AddrMessage) usize {
// 4 + 8 + 16 + 2
const fixed_length_per_ip = 30;
const count = CompactSizeUint.new(self.ip_addresses.len).hint_encoded_len();
return count + self.ip_addresses.len * fixed_length_per_ip;
}

pub fn eql(self: *const AddrMessage, other: *const AddrMessage) bool {
if (self.ip_addresses.len != other.ip_addresses.len) return false;

const count = @as(usize, self.ip_addresses.len);
for (0..count) |i| {
if (!self.ip_addresses[i].eql(&other.ip_addresses[i])) return false;
}

return true;
}
};

// TESTS
test "ok_full_flow_AddrMessage" {
const test_allocator = std.testing.allocator;
{
const ip_addresses = try test_allocator.alloc(NetworkIPAddr, 1);
defer test_allocator.free(ip_addresses);

ip_addresses[0] = NetworkIPAddr{
.time = 1414012889,
.services = 1,
.ip = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 51 },
.port = 8080,
};
const am = AddrMessage{
.ip_addresses = ip_addresses[0..],
};

// Serialize
const payload = try am.serialize(test_allocator);
defer test_allocator.free(payload);

// Deserialize
const deserialized_am = try AddrMessage.deserializeSlice(test_allocator, payload);

// Test equality
try std.testing.expect(am.eql(&deserialized_am));

defer test_allocator.free(deserialized_am.ip_addresses);
}
}
7 changes: 7 additions & 0 deletions src/network/protocol/messages/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub const BlockMessage = @import("block.zig").BlockMessage;
pub const GetblocksMessage = @import("getblocks.zig").GetblocksMessage;
pub const PingMessage = @import("ping.zig").PingMessage;
pub const PongMessage = @import("pong.zig").PongMessage;
pub const AddrMessage = @import("addr.zig").AddrMessage;
pub const MerkleBlockMessage = @import("merkleblock.zig").MerkleBlockMessage;
pub const FeeFilterMessage = @import("feefilter.zig").FeeFilterMessage;
pub const SendCmpctMessage = @import("sendcmpct.zig").SendCmpctMessage;
Expand Down Expand Up @@ -56,6 +57,7 @@ pub const MessageTypes = enum {
getblocks,
ping,
pong,
addr,
merkleblock,
sendcmpct,
feefilter,
Expand All @@ -75,6 +77,7 @@ pub const Message = union(MessageTypes) {
getblocks: GetblocksMessage,
ping: PingMessage,
pong: PongMessage,
addr: AddrMessage,
merkleblock: MerkleBlockMessage,
sendcmpct: SendCmpctMessage,
feefilter: FeeFilterMessage,
Expand All @@ -94,6 +97,7 @@ pub const Message = union(MessageTypes) {
.getblocks => |m| @TypeOf(m).name(),
.ping => |m| @TypeOf(m).name(),
.pong => |m| @TypeOf(m).name(),
.addr => |m| @TypeOf(m).name(),
.merkleblock => |m| @TypeOf(m).name(),
.sendcmpct => |m| @TypeOf(m).name(),
.feefilter => |m| @TypeOf(m).name(),
Expand All @@ -115,6 +119,7 @@ pub const Message = union(MessageTypes) {
.getblocks => |*m| m.deinit(allocator),
.ping => {},
.pong => {},
.addr => |m| m.deinit(allocator),
.merkleblock => |*m| m.deinit(allocator),
.sendcmpct => {},
.feefilter => {},
Expand Down Expand Up @@ -145,6 +150,7 @@ pub const Message = union(MessageTypes) {
.notfound => |*m| m.checksum(),
.sendheaders => |*m| m.checksum(),
.filterload => |*m| m.checksum(),
.addr => |m| m.checksum(),
oxlime marked this conversation as resolved.
Show resolved Hide resolved
};
}

Expand All @@ -166,6 +172,7 @@ pub const Message = union(MessageTypes) {
.notfound => |m| m.hintSerializedLen(),
.sendheaders => |m| m.hintSerializedLen(),
.filterload => |*m| m.hintSerializedLen(),
.addr => |m| m.hintSerializedLen(),
oxlime marked this conversation as resolved.
Show resolved Hide resolved
};
}
};
Expand Down
42 changes: 42 additions & 0 deletions src/network/wire/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ pub fn receiveMessage(
protocol.messages.Message{ .ping = try protocol.messages.PingMessage.deserializeReader(allocator, r) }
else if (std.mem.eql(u8, &command, protocol.messages.PongMessage.name()))
protocol.messages.Message{ .pong = try protocol.messages.PongMessage.deserializeReader(allocator, r) }
else if (std.mem.eql(u8, &command, protocol.messages.AddrMessage.name()))
protocol.messages.Message{ .addr = try protocol.messages.AddrMessage.deserializeReader(allocator, r) }
else if (std.mem.eql(u8, &command, protocol.messages.MerkleBlockMessage.name()))
protocol.messages.Message{ .merkleblock = try protocol.messages.MerkleBlockMessage.deserializeReader(allocator, r) }
else if (std.mem.eql(u8, &command, protocol.messages.SendCmpctMessage.name()))
Expand Down Expand Up @@ -405,6 +407,46 @@ test "ok_send_pong_message" {
}
}

test "ok_send_addr_message" {
const Config = @import("../../config/config.zig").Config;
const NetworkIPAddr = @import("../protocol/messages/addr.zig").NetworkIPAddr;

const ArrayList = std.ArrayList;
const test_allocator = std.testing.allocator;
const AddrMessage = protocol.messages.AddrMessage;

var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator);
defer list.deinit();

const ip_addresses = try test_allocator.alloc(NetworkIPAddr, 1);
defer test_allocator.free(ip_addresses);

ip_addresses[0] = NetworkIPAddr{
.time = 1414012889,
.services = 1,
.ip = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 51 },
.port = 8080,
};

var message = AddrMessage{
.ip_addresses = ip_addresses,
};

var received_message = try write_and_read_message(
test_allocator,
&list,
Config.BitcoinNetworkId.MAINNET,
Config.PROTOCOL_VERSION,
message,
) orelse unreachable;
defer received_message.deinit(test_allocator);

switch (received_message) {
.addr => |rm| try std.testing.expect(message.eql(&rm)),
else => unreachable,
}
}

test "ok_send_sendheaders_message" {
const Config = @import("../../config/config.zig").Config;
const ArrayList = std.ArrayList;
Expand Down
Loading