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);