From e867f9fc8ff927ef432cc3f6ffcf1261ef2608d7 Mon Sep 17 00:00:00 2001 From: Patrice Tisserand Date: Wed, 2 Oct 2024 02:35:26 +0200 Subject: [PATCH 1/4] feat: implemented headers message --- src/network/protocol/messages/headers.zig | 115 ++++++++++++++++++++++ src/network/protocol/messages/lib.zig | 7 ++ 2 files changed, 122 insertions(+) create mode 100644 src/network/protocol/messages/headers.zig diff --git a/src/network/protocol/messages/headers.zig b/src/network/protocol/messages/headers.zig new file mode 100644 index 0000000..e68dcb6 --- /dev/null +++ b/src/network/protocol/messages/headers.zig @@ -0,0 +1,115 @@ +const std = @import("std"); +const protocol = @import("../lib.zig"); +const genericChecksum = @import("lib.zig").genericChecksum; + +const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; + +const BlockHeader = @import("../../../types/lib.zig").BlockHeader; + +/// HeadersMessage represents the "headers" message +/// +/// https://developer.bitcoin.org/reference/p2p_networking.html#headers +pub const HeadersMessage = struct { + headers: []BlockHeader, + + const Self = @This(); + + pub inline fn name() *const [12]u8 { + return protocol.CommandNames.HEADERS ++ [_]u8{0} ** 5; + } + + pub fn checksum(self: HeadersMessage) [4]u8 { + return genericChecksum(self); + } + + pub fn deinit(self: *HeadersMessage, allocator: std.mem.Allocator) void { + allocator.free(self.headers); + } + + /// Serialize the message as bytes and write them to the Writer. + /// + /// `w` should be a valid `Writer`. + pub fn serializeToWriter(self: *const Self, 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'."); + if (!std.meta.hasFn(@TypeOf(w), "writeByte")) @compileError("Expects r to have fn 'writeByte'."); + } + if (self.headers.len != 0) { + try CompactSizeUint.new(self.headers.len).encodeToWriter(w); + + for (self.headers) |header| { + try header.serializeToWriter(w); + try w.writeByte(0); + } + } + } + + /// Serialize a message as bytes and write them to the buffer. + /// + /// buffer.len must be >= than self.hintSerializedLen() + pub fn serializeToSlice(self: *const Self, buffer: []u8) !void { + var fbs = std.io.fixedBufferStream(buffer); + try self.serializeToWriter(fbs.writer()); + } + + /// Serialize a message as bytes and return them. + pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { + const serialized_len = self.hintSerializedLen(); + if (serialized_len != 0) { + const ret = try allocator.alloc(u8, serialized_len); + errdefer allocator.free(ret); + + try self.serializeToSlice(ret); + + return ret; + } else { + return &.{}; + } + } + + pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { + 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'."); + if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); + } + + var headers_message: Self = undefined; + + const headers_count = try CompactSizeUint.decodeReader(r); + + headers_message.headers = try allocator.alloc(BlockHeader, headers_count.value()); + errdefer allocator.free(headers_message.headers); + + var i: usize = 0; + while (i < headers_count.value()) : (i += 1) { + const header = try BlockHeader.deserializeReader(allocator, r); + headers_message.headers[i] = header; + _ = r.readByte(); + } + + return headers_message; + } + + /// Deserialize bytes into a `HeaderMessage` + pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { + var fbs = std.io.fixedBufferStream(bytes); + return try Self.deserializeReader(allocator, fbs.reader()); + } + + pub fn hintSerializedLen(self: Self) usize { + if (self.headers.len != 0) { + const headers_number_length = CompactSizeUint.new(self.headers.len).hint_encoded_len(); + var headers_length: usize = 0; + for (self.headers) |_| { + headers_length += BlockHeader.serializedLen(); + headers_length += 1; // 0 transactions + } + return headers_number_length + headers_length; + } else { + return 0; + } + } +}; diff --git a/src/network/protocol/messages/lib.zig b/src/network/protocol/messages/lib.zig index 5a27d62..a35b23c 100644 --- a/src/network/protocol/messages/lib.zig +++ b/src/network/protocol/messages/lib.zig @@ -17,6 +17,7 @@ const Sha256 = std.crypto.hash.sha2.Sha256; pub const NotFoundMessage = @import("notfound.zig").NotFoundMessage; pub const SendHeadersMessage = @import("sendheaders.zig").SendHeadersMessage; pub const FilterLoadMessage = @import("filterload.zig").FilterLoadMessage; +pub const HeadersMessage = @import("headers.zig").HeadersMessage; pub const InventoryVector = struct { type: u32, @@ -65,6 +66,7 @@ pub const MessageTypes = enum { notfound, sendheaders, filterload, + headers, }; pub const Message = union(MessageTypes) { @@ -84,6 +86,7 @@ pub const Message = union(MessageTypes) { notfound: NotFoundMessage, sendheaders: SendHeadersMessage, filterload: FilterLoadMessage, + headers: HeadersMessage, pub fn name(self: Message) *const [12]u8 { return switch (self) { @@ -103,6 +106,7 @@ pub const Message = union(MessageTypes) { .notfound => |m| @TypeOf(m).name(), .sendheaders => |m| @TypeOf(m).name(), .filterload => |m| @TypeOf(m).name(), + .headers => |m| @TypeOf(m).name(), }; } @@ -124,6 +128,7 @@ pub const Message = union(MessageTypes) { .notfound => {}, .sendheaders => {}, .filterload => {}, + .headers => |*m| m.deinit(allocator), } } @@ -145,6 +150,7 @@ pub const Message = union(MessageTypes) { .notfound => |*m| m.checksum(), .sendheaders => |*m| m.checksum(), .filterload => |*m| m.checksum(), + .headers => |*m| m.checksum(), }; } @@ -166,6 +172,7 @@ pub const Message = union(MessageTypes) { .notfound => |m| m.hintSerializedLen(), .sendheaders => |m| m.hintSerializedLen(), .filterload => |*m| m.hintSerializedLen(), + .headers => |*m| m.hintSerializedLen(), }; } }; From fedba9d2d626d13f91c171688404e1b5ac4a476d Mon Sep 17 00:00:00 2001 From: Patrice Tisserand Date: Wed, 2 Oct 2024 16:34:33 +0200 Subject: [PATCH 2/4] fix code style and add missing test for headers message --- src/network/protocol/messages/headers.zig | 80 +++++++++++++++-------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/src/network/protocol/messages/headers.zig b/src/network/protocol/messages/headers.zig index e68dcb6..0b046a1 100644 --- a/src/network/protocol/messages/headers.zig +++ b/src/network/protocol/messages/headers.zig @@ -35,13 +35,11 @@ pub const HeadersMessage = struct { if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll'."); if (!std.meta.hasFn(@TypeOf(w), "writeByte")) @compileError("Expects r to have fn 'writeByte'."); } - if (self.headers.len != 0) { - try CompactSizeUint.new(self.headers.len).encodeToWriter(w); + try CompactSizeUint.new(self.headers.len).encodeToWriter(w); - for (self.headers) |header| { - try header.serializeToWriter(w); - try w.writeByte(0); - } + for (self.headers) |header| { + try header.serializeToWriter(w); + try w.writeByte(0); } } @@ -76,21 +74,17 @@ pub const HeadersMessage = struct { if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); } - var headers_message: Self = undefined; - const headers_count = try CompactSizeUint.decodeReader(r); - headers_message.headers = try allocator.alloc(BlockHeader, headers_count.value()); - errdefer allocator.free(headers_message.headers); + var headers = try allocator.alloc(BlockHeader, headers_count.value()); + errdefer allocator.free(headers); - var i: usize = 0; - while (i < headers_count.value()) : (i += 1) { - const header = try BlockHeader.deserializeReader(allocator, r); - headers_message.headers[i] = header; - _ = r.readByte(); + for (0..headers_count.value()) |i| { + headers[i] = try BlockHeader.deserializeReader(r); + _ = try r.readByte(); } - return headers_message; + return Self{ .headers = headers }; } /// Deserialize bytes into a `HeaderMessage` @@ -100,16 +94,48 @@ pub const HeadersMessage = struct { } pub fn hintSerializedLen(self: Self) usize { - if (self.headers.len != 0) { - const headers_number_length = CompactSizeUint.new(self.headers.len).hint_encoded_len(); - var headers_length: usize = 0; - for (self.headers) |_| { - headers_length += BlockHeader.serializedLen(); - headers_length += 1; // 0 transactions - } - return headers_number_length + headers_length; - } else { - return 0; - } + const headers_number_length = CompactSizeUint.new(self.headers.len).hint_encoded_len(); + const headers_length = self.headers.len * (BlockHeader.serializedLen() + 1); + return headers_number_length + headers_length; } }; + +// TESTS + +test "ok_fullflow_headers_message" { + const allocator = std.testing.allocator; + + { + // payload example from https://developer.bitcoin.org/reference/p2p_networking.html#headers + const payload = [_]u8{ + 0x01, // header count + // block header + 0x02, 0x00, 0x00, 0x00, // block version: 2 + 0xb6, 0xff, 0x0b, 0x1b, + 0x16, 0x80, 0xa2, 0x86, + 0x2a, 0x30, 0xca, 0x44, + 0xd3, 0x46, 0xd9, 0xe8, + 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash of previous block + 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, + 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, // merkle root + 0x24, 0xd9, 0x5a, 0x54, // unix time (1415239972) + 0x30, 0xc3, 0x1b, 0x18, // bits + 0xfe, 0x9f, 0x08, 0x64, // nonce + // end of block header + 0x00, // transaction count + }; + + var deserialized_msg = try HeadersMessage.deserializeSlice(allocator, &payload); + defer deserialized_msg.deinit(allocator); + + try std.testing.expectEqual(1, deserialized_msg.headers.len); + try std.testing.expectEqual(2, deserialized_msg.headers[0].version); + try std.testing.expectEqual(1415239972, deserialized_msg.headers[0].timestamp); + try std.testing.expectEqual(1678286846, deserialized_msg.headers[0].nonce); + + const serialized_payload = try deserialized_msg.serialize(allocator); + defer allocator.free(serialized_payload); + + try std.testing.expect(std.mem.eql(u8, &payload, serialized_payload)); + } +} From 3ab5fa6e42c500b4818798d3b05373231e0832cc Mon Sep 17 00:00:00 2001 From: Patrice Tisserand Date: Thu, 3 Oct 2024 01:42:08 +0200 Subject: [PATCH 3/4] add `eql` method for `BlockHeader` --- src/types/block_header.zig | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/types/block_header.zig b/src/types/block_header.zig index 0199cdd..ec12506 100644 --- a/src/types/block_header.zig +++ b/src/types/block_header.zig @@ -38,3 +38,31 @@ pub fn deserializeReader(r: anytype) !Self { pub fn serializedLen() usize { return 80; } + +pub fn eql(self: *const Self, other: *const Self) bool { + if (self.version != other.version) { + return false; + } + + if (!std.mem.eql(u8, &self.prev_block, &other.prev_block)) { + return false; + } + + if (!std.mem.eql(u8, &self.merkle_root, &other.merkle_root)) { + return false; + } + + if (self.timestamp != other.timestamp) { + return false; + } + + if (self.nbits != other.nbits) { + return false; + } + + if (self.nonce != other.nonce) { + return false; + } + + return true; +} From e95cf1ea6b15f53e0db982739eb1737d71a7692e Mon Sep 17 00:00:00 2001 From: Patrice Tisserand Date: Thu, 3 Oct 2024 01:48:15 +0200 Subject: [PATCH 4/4] remove useless comptime check and use `BlockHeader::eql` instead of field by field compare. --- src/network/protocol/messages/headers.zig | 45 +++++++++++++++-------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/network/protocol/messages/headers.zig b/src/network/protocol/messages/headers.zig index 0b046a1..b945123 100644 --- a/src/network/protocol/messages/headers.zig +++ b/src/network/protocol/messages/headers.zig @@ -31,8 +31,6 @@ pub const HeadersMessage = struct { /// `w` should be a valid `Writer`. pub fn serializeToWriter(self: *const Self, 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'."); if (!std.meta.hasFn(@TypeOf(w), "writeByte")) @compileError("Expects r to have fn 'writeByte'."); } try CompactSizeUint.new(self.headers.len).encodeToWriter(w); @@ -68,9 +66,6 @@ pub const HeadersMessage = struct { pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 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'."); if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); } @@ -111,13 +106,14 @@ test "ok_fullflow_headers_message" { 0x01, // header count // block header 0x02, 0x00, 0x00, 0x00, // block version: 2 - 0xb6, 0xff, 0x0b, 0x1b, - 0x16, 0x80, 0xa2, 0x86, - 0x2a, 0x30, 0xca, 0x44, - 0xd3, 0x46, 0xd9, 0xe8, - 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash of previous block - 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, - 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, // merkle root + 0xb6, 0xff, 0x0b, 0x1b, 0x16, 0x80, 0xa2, 0x86, // hash of previous block + 0x2a, 0x30, 0xca, 0x44, 0xd3, 0x46, 0xd9, 0xe8, // hash of previous block + 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, // hash of previous block + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash of previous block + 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, // merkle root + 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, // merkle root + 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, // merkle root + 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, // merkle root 0x24, 0xd9, 0x5a, 0x54, // unix time (1415239972) 0x30, 0xc3, 0x1b, 0x18, // bits 0xfe, 0x9f, 0x08, 0x64, // nonce @@ -128,11 +124,28 @@ test "ok_fullflow_headers_message" { var deserialized_msg = try HeadersMessage.deserializeSlice(allocator, &payload); defer deserialized_msg.deinit(allocator); + const expected_block_header = BlockHeader{ + .version = 2, + .prev_block = [_]u8{ + 0xb6, 0xff, 0x0b, 0x1b, 0x16, 0x80, 0xa2, 0x86, + 0x2a, 0x30, 0xca, 0x44, 0xd3, 0x46, 0xd9, 0xe8, + 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + .merkle_root = [_]u8{ + 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, + 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, + 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, + 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, + }, + .timestamp = 1415239972, + .nbits = 404472624, + .nonce = 1678286846, + }; + try std.testing.expectEqual(1, deserialized_msg.headers.len); - try std.testing.expectEqual(2, deserialized_msg.headers[0].version); - try std.testing.expectEqual(1415239972, deserialized_msg.headers[0].timestamp); - try std.testing.expectEqual(1678286846, deserialized_msg.headers[0].nonce); - + try std.testing.expect(expected_block_header.eql(&deserialized_msg.headers[0])); + const serialized_payload = try deserialized_msg.serialize(allocator); defer allocator.free(serialized_payload);