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
173 changes: 173 additions & 0 deletions src/network/protocol/messages/addr.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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;
}
};

/// 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(@intCast(self.ip_addresses.len)).encodeToWriter(w);
oxlime marked this conversation as resolved.
Show resolved Hide resolved
try CompactSizeUint.new(self.ip_addresses.len).encodeToWriter(w);

// Serialize each IP address
for (self.ip_addresses) |*ip_address| {
try w.writeInt(u32, ip_address.time, .little);
try w.writeInt(u64, ip_address.services, .little);
try w.writeAll(std.mem.asBytes(&ip_address.ip));
try w.writeInt(u16, ip_address.port, .big);
}
oxlime marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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
}

var vm: AddrMessage = undefined;
const ip_address_count = try CompactSizeUint.decodeReader(r);

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

vm.ip_addresses = ip_addresses;

for (vm.ip_addresses) |*ip_address| {
ip_address.time = try r.readInt(u32, .little);
ip_address.services = try r.readInt(u64, .little);
try r.readNoEof(&ip_address.ip);
ip_address.port = try r.readInt(u16, .big);
}
oxlime marked this conversation as resolved.
Show resolved Hide resolved

return vm;
}

/// 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 @@ -6,6 +6,7 @@ pub const GetaddrMessage = @import("getaddr.zig").GetaddrMessage;
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 FeeFilterMessage = @import("feefilter.zig").FeeFilterMessage;
pub const SendCmpctMessage = @import("sendcmpct.zig").SendCmpctMessage;
pub const FilterClearMessage = @import("filterclear.zig").FilterClearMessage;
Expand All @@ -18,6 +19,7 @@ pub const MessageTypes = enum {
getblocks,
ping,
pong,
addr,
sendcmpct,
feefilter,
filterclear,
Expand All @@ -31,6 +33,7 @@ pub const Message = union(MessageTypes) {
getblocks: GetblocksMessage,
ping: PingMessage,
pong: PongMessage,
addr: AddrMessage,
sendcmpct: SendCmpctMessage,
feefilter: FeeFilterMessage,
filterclear: FilterClearMessage,
Expand All @@ -44,6 +47,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(),
.sendcmpct => |m| @TypeOf(m).name(),
.feefilter => |m| @TypeOf(m).name(),
.filterclear => |m| @TypeOf(m).name(),
Expand All @@ -59,6 +63,7 @@ pub const Message = union(MessageTypes) {
.getblocks => |m| m.deinit(allocator),
.ping => {},
.pong => {},
.addr => |m| m.deinit(allocator),
.sendcmpct => {},
.feefilter => {},
.filterclear => {},
Expand All @@ -74,6 +79,7 @@ pub const Message = union(MessageTypes) {
.getblocks => |m| m.checksum(),
.ping => |m| m.checksum(),
.pong => |m| m.checksum(),
.addr => |m| m.checksum(),
oxlime marked this conversation as resolved.
Show resolved Hide resolved
.sendcmpct => |m| m.checksum(),
.feefilter => |m| m.checksum(),
.filterclear => |m| m.checksum(),
Expand All @@ -89,6 +95,7 @@ pub const Message = union(MessageTypes) {
.getblocks => |m| m.hintSerializedLen(),
.ping => |m| m.hintSerializedLen(),
.pong => |m| m.hintSerializedLen(),
.addr => |m| m.hintSerializedLen(),
oxlime marked this conversation as resolved.
Show resolved Hide resolved
.sendcmpct => |m| m.hintSerializedLen(),
.feefilter => |m| m.hintSerializedLen(),
.filterclear => |m| m.hintSerializedLen(),
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 @@ -122,6 +122,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.SendCmpctMessage.name()))
protocol.messages.Message{ .sendcmpct = try protocol.messages.SendCmpctMessage.deserializeReader(allocator, r) }
else if (std.mem.eql(u8, &command, protocol.messages.FilterClearMessage.name()))
Expand Down Expand Up @@ -338,6 +340,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,
};

const 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 "ko_receive_invalid_payload_length" {
const Config = @import("../../config/config.zig").Config;
const ArrayList = std.ArrayList;
Expand Down
Loading