From 009571a1ad56abf3bfa19137e37f9418dad245e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 10 Sep 2024 18:27:38 +0200 Subject: [PATCH] feat(protocol): VersionMessage with (de)serialization --- build.zig.zon | 4 +- src/config/config.zig | 15 +- src/core/mempool.zig | 1 - src/network/protocol.zig | 346 ++++++++++++++++++++++++++++++++++----- src/storage/storage.zig | 4 +- 5 files changed, 317 insertions(+), 53 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 9b60b86..89aa6ed 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,8 +19,8 @@ .hash = "1220f9e1eb744c8dc2750c1e6e1ceb1c2d521bedb161ddead1a6bb772032e576d74a", }, .@"bitcoin-primitives" = .{ - .url = "git+https://github.com/zig-bitcoin/bitcoin-primitives#17430ab361bb966b7cabb6c835adbe79c72276aa", - .hash = "12205840417b3b689acadf210bf1c815de4045381aa744d64e93e332bb49e9714d5f", + .url = "git+https://github.com/zig-bitcoin/bitcoin-primitives#4d179bb3027dbc35a99a56938c05008b62e4bf7e", + .hash = "1220a65f6105a79c9347449d2553e7abf965b3f61fa883478954d861e824631d5396", }, }, .paths = .{ diff --git a/src/config/config.zig b/src/config/config.zig index 311c4b6..79afffe 100644 --- a/src/config/config.zig +++ b/src/config/config.zig @@ -1,5 +1,13 @@ const std = @import("std"); +const dns_seed = [:0]const u8; + +const DNS_SEEDS = [3]dns_seed{ + "seed.bitcoin.sipa.be", + "seed.bitcoin.sprovoost.nl", + "seed.btc.petertodd.net", +}; + /// Global configuration for the node /// /// This is loaded from the `bitcoin.conf` file @@ -20,8 +28,6 @@ pub const Config = struct { /// Data directory datadir: [:0]const u8, - seednode: []const u8, - /// Load the configuration from a file /// /// # Arguments @@ -46,7 +52,6 @@ pub const Config = struct { .p2p_port = 8333, .testnet = false, .datadir = ".bitcoin", - .seednode = "", }; var buf: [1024]u8 = undefined; @@ -69,6 +74,10 @@ pub const Config = struct { return config; } + pub inline fn dnsSeeds() [3]dns_seed { + return DNS_SEEDS; + } + pub fn deinit(self: *Config) void { self.allocator.free(self.datadir); } diff --git a/src/core/mempool.zig b/src/core/mempool.zig index d039bea..4cf2eca 100644 --- a/src/core/mempool.zig +++ b/src/core/mempool.zig @@ -190,7 +190,6 @@ test "Mempool" { .p2p_port = 8333, .testnet = false, .datadir = "/tmp/btczee", - .seednode = "", }; var mempool = try Mempool.init(allocator, &config); defer mempool.deinit(); diff --git a/src/network/protocol.zig b/src/network/protocol.zig index 27604ea..1d8603a 100644 --- a/src/network/protocol.zig +++ b/src/network/protocol.zig @@ -1,16 +1,22 @@ const std = @import("std"); const net = std.net; +const native_endian = @import("builtin").target.cpu.arch.endian(); + +const Endian = std.builtin.Endian; + +const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; /// Protocol version pub const PROTOCOL_VERSION: u32 = 70015; /// Network services pub const ServiceFlags = struct { - pub const NODE_NETWORK: u64 = 1; - pub const NODE_GETUTXO: u64 = 2; - pub const NODE_BLOOM: u64 = 4; - pub const NODE_WITNESS: u64 = 8; - pub const NODE_NETWORK_LIMITED: u64 = 1024; + pub const NODE_NETWORK: u64 = 0x1; + pub const NODE_GETUTXO: u64 = 0x2; + pub const NODE_BLOOM: u64 = 0x4; + pub const NODE_WITNESS: u64 = 0x8; + pub const NODE_XTHIN: u64 = 0x10; + pub const NODE_NETWORK_LIMITED: u64 = 0x0400; }; /// Command string length @@ -19,41 +25,152 @@ pub const COMMAND_SIZE: usize = 12; /// Magic bytes for mainnet pub const MAGIC_BYTES: [4]u8 = .{ 0xF9, 0xBE, 0xB4, 0xD9 }; +/// An IpV6 address +pub const IpV6Address = struct { + ip: [8]u16, // represented in big endian + port: u16, // represented in system native endian +}; + /// NetworkAddress represents a network address pub const NetworkAddress = struct { services: u64, - ip: [16]u8, - port: u16, - - pub fn init(address: net.Address) NetworkAddress { - const result = NetworkAddress{ - .services = ServiceFlags.NODE_NETWORK, - .ip = [_]u8{0} ** 16, - .port = address.getPort(), - }; - // TODO: Handle untagged union properly (for IPv6) - - return result; - } + address: IpV6Address, }; /// VersionMessage represents the "version" message +/// +/// https://developer.bitcoin.org/reference/p2p_networking.html#version pub const VersionMessage = struct { version: i32, services: u64, timestamp: i64, addr_recv: NetworkAddress, - addr_from: NetworkAddress = .{ - .services = 0, - .ip = [_]u8{0} ** 16, - .port = 0, - }, - nonce: u64 = 0, - user_agent: []const u8 = "", - start_height: i32 = 0, - relay: bool = false, + addr_trans: NetworkAddress, + nonce: u64, + user_agent: ?[]const u8, + start_height: i32, + relay: ?bool, + + pub fn deinit(self: VersionMessage, allocator: std.mem.Allocator) void { + if (self.user_agent) |ua| { + allocator.free(ua); + } + } + + /// Serialize a message to bytes + /// + /// The caller is responsible for freeing the returned value. + pub fn serialize(self: VersionMessage, allocator: std.mem.Allocator) ![]u8 { + // 4 + 8 + 8 + (2 * (8 + 16 + 2) + 8 + 4) + const fixed_length = 84; + var user_agent_len: usize = undefined; + if (self.user_agent) |ua| { + user_agent_len = ua.len; + } else { + user_agent_len = 0; + } + const compact_user_agent_len = CompactSizeUint.new(user_agent_len); + const compact_user_agent_len_len = compact_user_agent_len.hint_encoded_len(); + const relay_len: usize = if (self.relay != null) 1 else 0; + const variable_length = compact_user_agent_len_len + user_agent_len + relay_len; + const len = fixed_length + variable_length; + + const res = try allocator.alloc(u8, len); + copyWithEndian(res[0..4], std.mem.asBytes(&self.version), .little); + copyWithEndian(res[4..12], std.mem.asBytes(&self.services), .little); + copyWithEndian(res[12..20], std.mem.asBytes(&self.timestamp), .little); + copyWithEndian(res[20..28], std.mem.asBytes(&self.addr_recv.services), .little); + @memcpy(res[28..44], std.mem.asBytes(&self.addr_recv.address.ip)); // ip is already repr as big endian + copyWithEndian(res[44..46], std.mem.asBytes(&self.addr_recv.address.port), .big); + copyWithEndian(res[46..54], std.mem.asBytes(&self.addr_trans.services), .little); + @memcpy(res[54..70], std.mem.asBytes(&self.addr_trans.address.ip)); // ip is already repr as big endian + copyWithEndian(res[70..72], std.mem.asBytes(&self.addr_trans.address.port), .big); + copyWithEndian(res[72..80], std.mem.asBytes(&self.nonce), .little); + compact_user_agent_len.encode_to(res[80..]); + if (user_agent_len != 0) { + @memcpy(res[80 + compact_user_agent_len_len .. 80 + compact_user_agent_len_len + user_agent_len], self.user_agent.?); + } + copyWithEndian(res[80 + compact_user_agent_len_len + user_agent_len .. 80 + compact_user_agent_len_len + user_agent_len + 4], std.mem.asBytes(&self.start_height), .little); + if (self.relay) |relay| { + copyWithEndian(res[80 + compact_user_agent_len_len + user_agent_len + 4 .. 80 + compact_user_agent_len_len + user_agent_len + 4 + 1], std.mem.asBytes(&relay), .little); + } + + return res; + } + + pub const DeserializeError = error{ + InputTooShort, + }; + + /// Deserialize bytes into a `VersionMessage` + /// + /// The caller is responsible for freeing the allocated memory in field `user_agent` by calling `VersionMessage.deinit();` + pub fn deserialize(allocator: std.mem.Allocator, bytes: []const u8) !VersionMessage { + var vm: VersionMessage = undefined; + + // No Version can be shorter than this + if (bytes.len < 85) { + return error.InputTooShort; + } + const compact_user_agent_len = try CompactSizeUint.decode(bytes[80..]); + const user_agent_len = compact_user_agent_len.value(); + const compact_user_agent_len_len = compact_user_agent_len.hint_encoded_len(); + + copyWithEndian(std.mem.asBytes(&vm.version), bytes[0..4], .little); + copyWithEndian(std.mem.asBytes(&vm.services), bytes[4..12], .little); + copyWithEndian(std.mem.asBytes(&vm.timestamp), bytes[12..20], .little); + copyWithEndian(std.mem.asBytes(&vm.addr_recv.services), bytes[20..28], .little); + @memcpy(std.mem.asBytes(&vm.addr_recv.address.ip), bytes[28..44]); // ip already in big endian + copyWithEndian(std.mem.asBytes(&vm.addr_recv.address.port), bytes[44..46], .big); + copyWithEndian(std.mem.asBytes(&vm.addr_trans.services), bytes[46..54], .little); + @memcpy(std.mem.asBytes(&vm.addr_trans.address.ip), bytes[54..70]); // ip already in big endian + copyWithEndian(std.mem.asBytes(&vm.addr_trans.address.port), bytes[70..72], .big); + copyWithEndian(std.mem.asBytes(&vm.nonce), bytes[72..80], .little); + if (user_agent_len != 0) { + const user_agent = try allocator.alloc(u8, user_agent_len); + @memcpy(user_agent, bytes[80 + compact_user_agent_len_len .. 80 + compact_user_agent_len_len + user_agent_len]); + vm.user_agent = user_agent; + } else { + vm.user_agent = null; + } + copyWithEndian(std.mem.asBytes(&vm.start_height), bytes[80 + compact_user_agent_len_len + user_agent_len .. 80 + compact_user_agent_len_len + user_agent_len + 4], .little); + if (bytes.len == 80 + compact_user_agent_len_len + user_agent_len + 4 + 1) { + copyWithEndian(std.mem.asBytes(&vm.relay.?), bytes[80 + compact_user_agent_len_len + user_agent_len + 4 .. 80 + compact_user_agent_len_len + user_agent_len + 4 + 1], .little); + } else { + vm.relay = null; + } + + return vm; + } + + pub fn hintSerializedLen(self: VersionMessage) usize { + // 4 + 8 + 8 + (2 * (8 + 16 + 2) + 8 + 4) + const fixed_length = 84; + var user_agent_len: usize = undefined; + if (self.user_agent) |ua| { + user_agent_len = ua.len; + } else { + user_agent_len = 0; + } + const compact_user_agent_len = CompactSizeUint.new(user_agent_len); + const compact_user_agent_len_len = compact_user_agent_len.hint_encoded_len(); + const relay_len: usize = if (self.relay != null) 1 else 0; + const variable_length = compact_user_agent_len_len + user_agent_len + relay_len; + return fixed_length + variable_length; + } }; +// Copy to dest and apply the specified endianness +// +// dest and src should not overlap +// dest.len should be == to src.len +fn copyWithEndian(dest: []u8, src: []const u8, endian: Endian) void { + @memcpy(dest, src); + if (native_endian != endian) { + std.mem.reverse(u8, dest[0..src.len]); + } +} + /// Header structure for all messages pub const MessageHeader = struct { magic: [4]u8, @@ -62,24 +179,6 @@ pub const MessageHeader = struct { checksum: u32, }; -/// Serialize a message to bytes -pub fn serializeMessage(allocator: std.mem.Allocator, command: []const u8, payload: anytype) ![]u8 { - _ = allocator; - _ = command; - _ = payload; - // In a real implementation, this would serialize the message - // For now, we'll just return a mock serialized message - return "serialized message"; -} - -/// Deserialize bytes to a message -pub fn deserializeMessage(allocator: std.mem.Allocator, bytes: []const u8) !void { - _ = allocator; - _ = bytes; - // In a real implementation, this would deserialize the message - // For now, we'll just do nothing -} - /// Calculate checksum for a message pub fn calculateChecksum(data: []const u8) u32 { _ = data; @@ -87,3 +186,160 @@ pub fn calculateChecksum(data: []const u8) u32 { // For now, we'll just return a mock checksum return 0x12345678; } + +// TESTS + +fn compareVersionMessage(lhs: VersionMessage, rhs: VersionMessage) bool { + // Normal fields + if (lhs.version != rhs.version // + or lhs.services != rhs.services // + or lhs.timestamp != rhs.timestamp // + or lhs.addr_recv.services != rhs.addr_recv.services // + or !std.mem.eql(u16, &lhs.addr_recv.address.ip, &rhs.addr_recv.address.ip) // + or lhs.addr_recv.address.port != rhs.addr_recv.address.port // + or lhs.addr_trans.services != rhs.addr_trans.services // + or !std.mem.eql(u16, &lhs.addr_trans.address.ip, &rhs.addr_trans.address.ip) // + or lhs.addr_trans.address.port != rhs.addr_trans.address.port // + or lhs.nonce != rhs.nonce) { + return false; + } + + // user_agent + if (lhs.user_agent) |lua| { + if (rhs.user_agent) |rua| { + if (!std.mem.eql(u8, lua, rua)) { + return false; + } + } else { + return false; + } + } else { + if (rhs.user_agent) |_| { + return false; + } + } + + // relay + if (lhs.relay) |ln| { + if (rhs.relay) |rn| { + if (ln != rn) { + return false; + } + } else { + return false; + } + } else { + if (rhs.relay) |_| { + return false; + } + } + + return true; +} + +test "ok_full_flow_VersionMessage" { + const allocator = std.testing.allocator; + + // No optional + { + const vm = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .addr_recv = NetworkAddress{ + .services = ServiceFlags.NODE_WITNESS, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 17, + }, + }, + .addr_trans = NetworkAddress{ + .services = ServiceFlags.NODE_BLOOM, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 19, + }, + }, + .nonce = 31, + .user_agent = null, + .start_height = 1000, + .relay = null, + }; + + const payload = try vm.serialize(allocator); + defer allocator.free(payload); + const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + defer deserialized_vm.deinit(allocator); + + try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + } + + // With relay + { + const vm = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .addr_recv = NetworkAddress{ + .services = ServiceFlags.NODE_WITNESS, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 17, + }, + }, + .addr_trans = NetworkAddress{ + .services = ServiceFlags.NODE_BLOOM, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 19, + }, + }, + .nonce = 31, + .user_agent = null, + .start_height = 1000, + .relay = true, + }; + + const payload = try vm.serialize(allocator); + defer allocator.free(payload); + const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + defer deserialized_vm.deinit(allocator); + + try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + } + + // With relay and user agent + { + const user_agent = [_]u8{0} ** 2046; + const vm = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .addr_recv = NetworkAddress{ + .services = ServiceFlags.NODE_WITNESS, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 17, + }, + }, + .addr_trans = NetworkAddress{ + .services = ServiceFlags.NODE_BLOOM, + .address = IpV6Address{ + .ip = [_]u16{13} ** 8, + .port = 19, + }, + }, + .nonce = 31, + .user_agent = &user_agent, + .start_height = 1000, + .relay = false, + }; + + const payload = try vm.serialize(allocator); + defer allocator.free(payload); + const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + defer deserialized_vm.deinit(allocator); + + try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + } +} diff --git a/src/storage/storage.zig b/src/storage/storage.zig index 07517f7..43a40ea 100644 --- a/src/storage/storage.zig +++ b/src/storage/storage.zig @@ -36,7 +36,7 @@ pub const Storage = struct { } /// Return a Transaction handle - pub fn init_transaction(self: Storage) !Transaction { + pub fn initTransaction(self: Storage) !Transaction { const txn = try lmdb.Transaction.init(self.env, .{ .mode = .ReadWrite }); return Transaction{ .txn = txn }; } @@ -52,7 +52,7 @@ pub const Transaction = struct { } /// Serialize and store a block in database - pub fn store_block(allocator: std.mem.Allocator, txn: Transaction, block: *Block) !void { + pub fn storeBlock(allocator: std.mem.Allocator, txn: Transaction, block: *Block) !void { const blocks = try txn.txn.database("blocks", .{ .create = true }); try blocks.set(&block.hash, try block.serizalize(allocator)); }