From 892930e6eb42333b41029ba1d2819f85acc3fb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= <34384633+tdelabro@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:51:52 +0200 Subject: [PATCH 1/4] feat(p2p): add wire package for messages io (#51) --- build.zig.zon | 4 +- src/lib.zig | 1 + src/network/protocol/lib.zig | 23 +- src/network/protocol/message.zig | 42 --- src/network/protocol/messages/lib.zig | 13 + src/network/protocol/messages/version.zig | 319 +++++++++++----------- src/network/wire/lib.zig | 248 +++++++++++++++++ 7 files changed, 421 insertions(+), 229 deletions(-) delete mode 100644 src/network/protocol/message.zig create mode 100644 src/network/wire/lib.zig diff --git a/build.zig.zon b/build.zig.zon index 89aa6ed..b31fca9 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#4d179bb3027dbc35a99a56938c05008b62e4bf7e", - .hash = "1220a65f6105a79c9347449d2553e7abf965b3f61fa883478954d861e824631d5396", + .url = "git+https://github.com/zig-bitcoin/bitcoin-primitives#3743701d398b35af80826b729b6eb12d3e8e8df9", + .hash = "12204e7aa2c42049440faf891e80cd7bc85f64b4aacdcda75e891ca52787f267342c", }, }, .paths = .{ diff --git a/src/lib.zig b/src/lib.zig index 4b1b77c..1e2827f 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -17,6 +17,7 @@ pub const wallet = @import("wallet/wallet.zig"); pub const miner = @import("miner/miner.zig"); pub const node = @import("node/node.zig"); pub const script = @import("script/lib.zig"); +pub const wire = @import("network/wire/lib.zig"); test { @import("std").testing.refAllDeclsRecursive(@This()); diff --git a/src/network/protocol/lib.zig b/src/network/protocol/lib.zig index f3ccd9a..0f4cfdf 100644 --- a/src/network/protocol/lib.zig +++ b/src/network/protocol/lib.zig @@ -1,17 +1,16 @@ -pub const message = @import("./message.zig"); pub const messages = @import("./messages/lib.zig"); -/// Protocol version -pub const PROTOCOL_VERSION: i32 = 70015; - /// Known network ids -pub const NetworkMagicBytes = struct { - pub const MAINNET: [4]u8 = 0xd9b4bef9; +pub const BitcoinNetworkId = struct { + pub const MAINNET: [4]u8 = .{ 0xd9, 0xb4, 0xbe, 0xf9 }; pub const REGTEST: [4]u8 = 0xdab5bffa; pub const TESTNET3: [4]u8 = 0x0709110b; pub const SIGNET: [4]u8 = 0x40cf030a; }; +/// Protocol version +pub const PROTOCOL_VERSION: i32 = 70015; + /// Network services pub const ServiceFlags = struct { pub const NODE_NETWORK: u64 = 0x1; @@ -22,18 +21,6 @@ pub const ServiceFlags = struct { pub const NODE_NETWORK_LIMITED: u64 = 0x0400; }; -/// 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, - address: IpV6Address, -}; - pub const CommandNames = struct { pub const VERSION = "version"; pub const VERACK = "verack"; diff --git a/src/network/protocol/message.zig b/src/network/protocol/message.zig deleted file mode 100644 index 7d0076f..0000000 --- a/src/network/protocol/message.zig +++ /dev/null @@ -1,42 +0,0 @@ -const std = @import("std"); -const net = std.net; -const Sha256 = std.crypto.hash.sha2.Sha256; - -const protocol = @import("lib.zig"); -const NetworkMagicBytes = protocol.NetworkMagicBytes; - -pub const Message = struct { - header: Header, - payload: []u8, -}; - -/// Header structure for all messages -/// -/// https://developer.bitcoin.org/reference/p2p_networking.html#message-headers -pub const Header = struct { - start_string: NetworkMagicBytes, - command_name: [12]u8, - payload_size: u32, - checksum: u32, - - pub fn new(network: NetworkMagicBytes, command_name: [12]u8, payload: []const u8) Header { - const header = .{ - .start_string = network, - .command_name = command_name, - .payload_size = 0, - .checksum = 0x5df6e0e2, - }; - - if (payload.len == 0) { - return header; - } - - const digest = [Sha256.digest_length]u8; - Sha256.hash(payload, &digest, .{}); - Sha256.hash(&digest, std.mem.asBytes(&header.checksum), .{}); - - header.payload_size = payload.len; - - return header; - } -}; diff --git a/src/network/protocol/messages/lib.zig b/src/network/protocol/messages/lib.zig index bbbef2f..097322a 100644 --- a/src/network/protocol/messages/lib.zig +++ b/src/network/protocol/messages/lib.zig @@ -1 +1,14 @@ +const std = @import("std"); pub const VersionMessage = @import("version.zig").VersionMessage; + +pub const MessageTypes = enum { Version }; + +pub const Message = union(MessageTypes) { + Version: VersionMessage, + + pub fn deinit(self: Message, allocator: std.mem.Allocator) void { + switch (self) { + .Version => |m| m.deinit(allocator), + } + } +}; diff --git a/src/network/protocol/messages/version.zig b/src/network/protocol/messages/version.zig index a242174..59876ad 100644 --- a/src/network/protocol/messages/version.zig +++ b/src/network/protocol/messages/version.zig @@ -2,12 +2,10 @@ const std = @import("std"); const native_endian = @import("builtin").target.cpu.arch.endian(); const protocol = @import("../lib.zig"); -const NetworkAddress = protocol.NetworkAddress; const ServiceFlags = protocol.ServiceFlags; -const IpV6Address = protocol.IpV6Address; -const Message = protocol.message.Message; const Endian = std.builtin.Endian; +const Sha256 = std.crypto.hash.sha2.Sha256; const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; @@ -15,187 +13,198 @@ const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; /// /// https://developer.bitcoin.org/reference/p2p_networking.html#version pub const VersionMessage = struct { - version: i32, - services: u64, + recv_ip: [16]u8, + trans_ip: [16]u8, timestamp: i64, - addr_recv: NetworkAddress, - addr_trans: NetworkAddress, + services: u64, nonce: u64, - user_agent: ?[]const u8, + recv_services: u64, + trans_services: u64, + version: i32, start_height: i32, + recv_port: u16, + trans_port: u16, + user_agent: ?[]const u8, relay: ?bool, - /// Will free the user_agent if present in the message + pub inline fn name() *const [12]u8 { + return protocol.CommandNames.VERSION ++ [_]u8{0} ** 5; + } + + /// Returns the message checksum + /// + /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` + pub fn checksum(self: VersionMessage) [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, .{}); + + return digest[0..4].*; + } + + /// Free the `user_agent` if there is one pub fn deinit(self: VersionMessage, allocator: std.mem.Allocator) void { if (self.user_agent) |ua| { allocator.free(ua); } } - /// Serialize a message to bytes + /// Serialize the message as bytes and write them to the Writer. /// - /// The caller is responsible for freeing the returned value. - pub fn serializeTo(self: VersionMessage, buffer: []u8) void { + /// `w` should be a valid `Writer`. + pub fn serializeToWriter(self: *const VersionMessage, 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'."); + } + const user_agent_len: usize = if (self.user_agent) |ua| ua.len else 0; const compact_user_agent_len = CompactSizeUint.new(user_agent_len); - const compact_user_agent_len_len = compact_user_agent_len.hint_encoded_len(); - copyWithEndian(buffer[0..4], std.mem.asBytes(&self.version), .little); - copyWithEndian(buffer[4..12], std.mem.asBytes(&self.services), .little); - copyWithEndian(buffer[12..20], std.mem.asBytes(&self.timestamp), .little); - copyWithEndian(buffer[20..28], std.mem.asBytes(&self.addr_recv.services), .little); - @memcpy(buffer[28..44], std.mem.asBytes(&self.addr_recv.address.ip)); // ip is already repr as big endian - copyWithEndian(buffer[44..46], std.mem.asBytes(&self.addr_recv.address.port), .big); - copyWithEndian(buffer[46..54], std.mem.asBytes(&self.addr_trans.services), .little); - @memcpy(buffer[54..70], std.mem.asBytes(&self.addr_trans.address.ip)); // ip is already repr as big endian - copyWithEndian(buffer[70..72], std.mem.asBytes(&self.addr_trans.address.port), .big); - copyWithEndian(buffer[72..80], std.mem.asBytes(&self.nonce), .little); - compact_user_agent_len.encode_to(buffer[80..]); + try w.writeInt(i32, self.version, .little); + try w.writeInt(u64, self.services, .little); + try w.writeInt(i64, self.timestamp, .little); + try w.writeInt(u64, self.recv_services, .little); + try w.writeAll(std.mem.asBytes(&self.recv_ip)); + try w.writeInt(u16, self.recv_port, .big); + try w.writeInt(u64, self.trans_services, .little); + try w.writeAll(std.mem.asBytes(&self.trans_ip)); + try w.writeInt(u16, self.trans_port, .big); + try w.writeInt(u64, self.nonce, .little); + try compact_user_agent_len.encodeToWriter(w); if (user_agent_len != 0) { - @memcpy(buffer[80 + compact_user_agent_len_len .. 80 + compact_user_agent_len_len + user_agent_len], self.user_agent.?); + try w.writeAll(self.user_agent.?); } - copyWithEndian(buffer[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(buffer[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); + try w.writeInt(i32, self.start_height, .little); + if (self.relay) |r| { + try w.writeAll(std.mem.asBytes(&r)); } } - /// Serialize a message to bytes + /// Serialize a message as bytes and write them to the buffer. /// - /// The caller is responsible for freeing the returned value. - pub fn serialize(self: VersionMessage, allocator: std.mem.Allocator) ![]u8 { + /// buffer.len must be >= than self.hintSerializedLen() + pub fn serializeToSlice(self: *const VersionMessage, 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 VersionMessage, allocator: std.mem.Allocator) ![]u8 { const serialized_len = self.hintSerializedLen(); - const res = try allocator.alloc(u8, serialized_len); + const ret = try allocator.alloc(u8, serialized_len); + errdefer allocator.free(ret); - self.serializeTo(res); + try self.serializeToSlice(ret); - return res; + return ret; } - pub const DeserializeError = error{ - InputTooShort, - }; + /// Deserialize a Reader bytes as a `VersionMessage` + pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !VersionMessage { + 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'."); + } - /// 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(); + vm.version = try r.readInt(i32, .little); + vm.services = try r.readInt(u64, .little); + vm.timestamp = try r.readInt(i64, .little); + vm.recv_services = try r.readInt(u64, .little); + try r.readNoEof(&vm.recv_ip); + vm.recv_port = try r.readInt(u16, .big); + vm.trans_services = try r.readInt(u64, .little); + try r.readNoEof(&vm.trans_ip); + vm.trans_port = try r.readInt(u16, .big); + vm.nonce = try r.readInt(u64, .little); + + const user_agent_len = (try CompactSizeUint.decodeReader(r)).value(); - 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]); + errdefer allocator.free(user_agent); + try r.readNoEof(user_agent); 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; - } + vm.start_height = try r.readInt(i32, .little); + vm.relay = if (r.readByte() catch null) |v| v != 0 else null; return vm; } + /// Deserialize bytes into a `VersionMessage` + pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !VersionMessage { + var fbs = std.io.fixedBufferStream(bytes); + const reader = fbs.reader(); + return try VersionMessage.deserializeReader(allocator, reader); + } + pub fn hintSerializedLen(self: VersionMessage) usize { // 4 + 8 + 8 + (2 * (8 + 16 + 2) + 8 + 4) const fixed_length = 84; - const user_agent_len: usize = if (self.user_agent) |ua| - ua.len - else - 0; + const user_agent_len: usize = if (self.user_agent) |ua| ua.len else 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]); - } -} - -// 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) |_| { + pub fn eql(self: *const VersionMessage, other: *const VersionMessage) bool { + // Normal fields + if (self.version != other.version // + or self.services != other.services // + or self.timestamp != other.timestamp // + or self.recv_services != other.recv_services // + or !std.mem.eql(u8, &self.recv_ip, &other.recv_ip) // + or self.recv_port != other.recv_port // + or self.trans_services != other.trans_services // + or !std.mem.eql(u8, &self.trans_ip, &other.trans_ip) // + or self.trans_port != other.trans_port // + or self.nonce != other.nonce) { return false; } - } - // relay - if (lhs.relay) |ln| { - if (rhs.relay) |rn| { - if (ln != rn) { + // user_agent + if (self.user_agent) |lua| { + if (other.user_agent) |rua| { + if (!std.mem.eql(u8, lua, rua)) { + return false; + } + } else { return false; } } else { - return false; + if (other.user_agent) |_| { + return false; + } } - } else { - if (rhs.relay) |_| { + + // relay + if (self.relay != other.relay) { return false; } + + return true; } +}; - return true; -} +// TESTS test "ok_full_flow_VersionMessage" { const allocator = std.testing.allocator; @@ -206,20 +215,12 @@ test "ok_full_flow_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, - }, - }, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, .nonce = 31, .user_agent = null, .start_height = 1000, @@ -228,10 +229,10 @@ test "ok_full_flow_VersionMessage" { const payload = try vm.serialize(allocator); defer allocator.free(payload); - const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + const deserialized_vm = try VersionMessage.deserializeSlice(allocator, payload); defer deserialized_vm.deinit(allocator); - try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + try std.testing.expect(vm.eql(&deserialized_vm)); } // With relay @@ -240,20 +241,12 @@ test "ok_full_flow_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, - }, - }, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, .nonce = 31, .user_agent = null, .start_height = 1000, @@ -262,10 +255,10 @@ test "ok_full_flow_VersionMessage" { const payload = try vm.serialize(allocator); defer allocator.free(payload); - const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + const deserialized_vm = try VersionMessage.deserializeSlice(allocator, payload); defer deserialized_vm.deinit(allocator); - try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + try std.testing.expect(vm.eql(&deserialized_vm)); } // With relay and user agent @@ -275,20 +268,12 @@ test "ok_full_flow_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, - }, - }, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, .nonce = 31, .user_agent = &user_agent, .start_height = 1000, @@ -297,9 +282,9 @@ test "ok_full_flow_VersionMessage" { const payload = try vm.serialize(allocator); defer allocator.free(payload); - const deserialized_vm = try VersionMessage.deserialize(allocator, payload); + const deserialized_vm = try VersionMessage.deserializeSlice(allocator, payload); defer deserialized_vm.deinit(allocator); - try std.testing.expect(compareVersionMessage(vm, deserialized_vm)); + try std.testing.expect(vm.eql(&deserialized_vm)); } } diff --git a/src/network/wire/lib.zig b/src/network/wire/lib.zig new file mode 100644 index 0000000..e1fd910 --- /dev/null +++ b/src/network/wire/lib.zig @@ -0,0 +1,248 @@ +//! The logic to read/write bitcoin messages from/to any Zig Reader/Writer. +//! +//! Bitcoin messages are always prefixed by the following header: +//! * network_id: [4]u8 +//! * command: [12]u8 +//! * payload_len: u32 +//! * checksum: [4]u8 +//! +//! `command` tells how to read the payload. +//! Error detection is done by checking received messages against their `payload_len` and `checksum`. + +const std = @import("std"); +const protocol = @import("../protocol/lib.zig"); + +const Stream = std.net.Stream; +const io = std.io; +const Sha256 = std.crypto.hash.sha2.Sha256; + +/// Return the checksum of a slice +/// +/// Use it on serialized messages to compute the header's value +fn computePayloadChecksum(payload: []u8) [4]u8 { + var digest: [32]u8 = undefined; + Sha256.hash(payload, &digest, .{}); + Sha256.hash(&digest, &digest, .{}); + + return digest[0..4].*; +} + +/// Send a message through the wire. +/// +/// Prefix it with the appropriate header. +pub fn sendMessage(allocator: std.mem.Allocator, w: anytype, protocol_version: i32, network_id: [4]u8, message: anytype) !void { + comptime { + if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'readAll'."); + if (!std.meta.hasFn(@TypeOf(message), "name")) @compileError("Expects message to have fn 'name'."); + if (!std.meta.hasFn(@TypeOf(message), "serialize")) @compileError("Expects message to have fn 'serialize'."); + } + + // Not used right now. + // As we add more messages, we will need to create multiple dedicated + // methods like this one to handle different messages in different + // way depending on the version of the protocol used + _ = protocol_version; + + const command = comptime @TypeOf(message).name(); + + const payload: []u8 = try message.serialize(allocator); + defer allocator.free(payload); + const checksum = computePayloadChecksum(payload); + + // I believe it's safe. No payload will be longer than u32.MAX + const payload_len: u32 = @intCast(payload.len); + + try w.writeAll(&network_id); + try w.writeAll(command); + try w.writeAll(std.mem.asBytes(&payload_len)); + try w.writeAll(std.mem.asBytes(&checksum)); + try w.writeAll(payload); +} + +pub const ReceiveMessageError = error{ InvalidCommand, InvaliPayloadLen, InvalidChecksum }; + +/// Read a message from the wire. +/// +/// Will fail if the header content does not match the payload. +pub fn receiveMessage(allocator: std.mem.Allocator, r: anytype) !protocol.messages.Message { + comptime { + if (!std.meta.hasFn(@TypeOf(r), "readBytesNoEof")) @compileError("Expects r to have fn 'readBytesNoEof'."); + } + + // Read header + _ = try r.readBytesNoEof(4); // Network id + const command = try r.readBytesNoEof(12); + const payload_len = try r.readInt(u32, .little); + const checksum = try r.readBytesNoEof(4); + + // Read payload + const message = if (std.mem.eql(u8, &command, protocol.messages.VersionMessage.name())) + try protocol.messages.VersionMessage.deserializeReader(allocator, r) + else + return error.InvalidCommand; + errdefer message.deinit(allocator); + + if (!std.mem.eql(u8, &message.checksum(), &checksum)) { + return error.InvalidChecksum; + } + if (message.hintSerializedLen() != payload_len) { + return error.InvaliPayloadLen; + } + + return protocol.messages.Message{ .Version = message }; +} + +// TESTS + +test "ok_send_message" { + const ArrayList = std.ArrayList; + const test_allocator = std.testing.allocator; + const VersionMessage = protocol.messages.VersionMessage; + const ServiceFlags = protocol.ServiceFlags; + + var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator); + defer list.deinit(); + + const user_agent = [_]u8{0} ** 2023; + const message = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, + .nonce = 31, + .user_agent = &user_agent, + .start_height = 1000, + .relay = false, + }; + + const writer = list.writer(); + try sendMessage(test_allocator, writer, protocol.PROTOCOL_VERSION, protocol.BitcoinNetworkId.MAINNET, message); + var fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(list.items); + const reader = fbs.reader(); + + const received_message = try receiveMessage(test_allocator, reader); + defer received_message.deinit(test_allocator); + + switch (received_message) { + .Version => |rm| try std.testing.expect(message.eql(&rm)), + } +} + +test "ko_receive_invalid_payload_length" { + const ArrayList = std.ArrayList; + const test_allocator = std.testing.allocator; + const VersionMessage = protocol.messages.VersionMessage; + const ServiceFlags = protocol.ServiceFlags; + + var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator); + defer list.deinit(); + + const user_agent = [_]u8{0} ** 2; + const message = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, + .nonce = 31, + .user_agent = &user_agent, + .start_height = 1000, + .relay = false, + }; + + const writer = list.writer(); + try sendMessage(test_allocator, writer, protocol.PROTOCOL_VERSION, protocol.BitcoinNetworkId.MAINNET, message); + + // Corrupt header payload length + @memset(list.items[16..20], 42); + + var fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(list.items); + const reader = fbs.reader(); + + try std.testing.expectError(error.InvaliPayloadLen, receiveMessage(test_allocator, reader)); +} + +test "ko_receive_invalid_checksum" { + const ArrayList = std.ArrayList; + const test_allocator = std.testing.allocator; + const VersionMessage = protocol.messages.VersionMessage; + const ServiceFlags = protocol.ServiceFlags; + + var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator); + defer list.deinit(); + + const user_agent = [_]u8{0} ** 2; + const message = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, + .nonce = 31, + .user_agent = &user_agent, + .start_height = 1000, + .relay = false, + }; + + const writer = list.writer(); + try sendMessage(test_allocator, writer, protocol.PROTOCOL_VERSION, protocol.BitcoinNetworkId.MAINNET, message); + + // Corrupt header checksum + @memset(list.items[20..24], 42); + + var fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(list.items); + const reader = fbs.reader(); + + try std.testing.expectError(error.InvalidChecksum, receiveMessage(test_allocator, reader)); +} + +test "ko_receive_invalid_command" { + const ArrayList = std.ArrayList; + const test_allocator = std.testing.allocator; + const VersionMessage = protocol.messages.VersionMessage; + const ServiceFlags = protocol.ServiceFlags; + + var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator); + defer list.deinit(); + + const user_agent = [_]u8{0} ** 2; + const message = VersionMessage{ + .version = 42, + .services = ServiceFlags.NODE_NETWORK, + .timestamp = 43, + .recv_services = ServiceFlags.NODE_WITNESS, + .trans_services = ServiceFlags.NODE_BLOOM, + .recv_ip = [_]u8{13} ** 16, + .trans_ip = [_]u8{12} ** 16, + .recv_port = 33, + .trans_port = 22, + .nonce = 31, + .user_agent = &user_agent, + .start_height = 1000, + .relay = false, + }; + + const writer = list.writer(); + try sendMessage(test_allocator, writer, protocol.PROTOCOL_VERSION, protocol.BitcoinNetworkId.MAINNET, message); + + // Corrupt header command + @memcpy(list.items[4..16], "whoissatoshi"); + + var fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(list.items); + const reader = fbs.reader(); + + try std.testing.expectError(error.InvalidCommand, receiveMessage(test_allocator, reader)); +} From ea0f61ab37ee759cc9bc97bed1019272905ad32a Mon Sep 17 00:00:00 2001 From: Nathan GD <53536851+gdnathan@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:56:35 +0200 Subject: [PATCH 2/4] feat(p2p/messages): add verack message (#103) --- src/network/protocol/messages/lib.zig | 18 ++++++- src/network/protocol/messages/verack.zig | 63 ++++++++++++++++++++++++ src/network/wire/lib.zig | 35 +++++++++++-- 3 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/network/protocol/messages/verack.zig diff --git a/src/network/protocol/messages/lib.zig b/src/network/protocol/messages/lib.zig index 097322a..dd8dd39 100644 --- a/src/network/protocol/messages/lib.zig +++ b/src/network/protocol/messages/lib.zig @@ -1,14 +1,30 @@ const std = @import("std"); pub const VersionMessage = @import("version.zig").VersionMessage; +pub const VerackMessage = @import("verack.zig").VerackMessage; -pub const MessageTypes = enum { Version }; +pub const MessageTypes = enum { Version, Verack }; pub const Message = union(MessageTypes) { Version: VersionMessage, + Verack: VerackMessage, pub fn deinit(self: Message, allocator: std.mem.Allocator) void { switch (self) { .Version => |m| m.deinit(allocator), + .Verack => {}, } } + pub fn checksum(self: Message) [4]u8 { + return switch (self) { + .Version => |m| m.checksum(), + .Verack => |m| m.checksum(), + }; + } + + pub fn hintSerializedLen(self: Message) usize { + return switch (self) { + .Version => |m| m.hintSerializedLen(), + .Verack => |m| m.hintSerializedLen(), + }; + } }; diff --git a/src/network/protocol/messages/verack.zig b/src/network/protocol/messages/verack.zig new file mode 100644 index 0000000..5dfe3b4 --- /dev/null +++ b/src/network/protocol/messages/verack.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const native_endian = @import("builtin").target.cpu.arch.endian(); +const protocol = @import("../lib.zig"); + +const ServiceFlags = protocol.ServiceFlags; + +const Endian = std.builtin.Endian; +const Sha256 = std.crypto.hash.sha2.Sha256; + +const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; + +/// VerackMessage represents the "verack" message +/// +/// https://developer.bitcoin.org/reference/p2p_networking.html#version +pub const VerackMessage = struct { + // verack message do not contain any payload, thus there is no field + + pub inline fn name() *const [12]u8 { + return protocol.CommandNames.VERACK ++ [_]u8{0} ** 6; + } + + pub fn checksum(self: VerackMessage) [4]u8 { + _ = self; + // If payload is empty, the checksum is always 0x5df6e0e2 (SHA256(SHA256(""))) + return [4]u8{ 0x5d, 0xf6, 0xe0, 0xe2 }; + } + + /// Serialize a message as bytes and return them. + pub fn serialize(self: *const VerackMessage, allocator: std.mem.Allocator) ![]u8 { + _ = self; + _ = allocator; + return &.{}; + } + + pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !VerackMessage { + _ = allocator; + _ = r; + return VerackMessage{}; + } + + pub fn hintSerializedLen(self: VerackMessage) usize { + _ = self; + return 0; + } + +}; + +// TESTS + +test "ok_full_flow_VerackMessage" { + const allocator = std.testing.allocator; + + { + const msg = VerackMessage{}; + + const payload = try msg.serialize(allocator); + defer allocator.free(payload); + const deserialized_msg = try VerackMessage.deserializeReader(allocator, payload); + _ = deserialized_msg; + + try std.testing.expect(payload.len == 0); + } +} diff --git a/src/network/wire/lib.zig b/src/network/wire/lib.zig index e1fd910..578f5c4 100644 --- a/src/network/wire/lib.zig +++ b/src/network/wire/lib.zig @@ -76,8 +76,10 @@ pub fn receiveMessage(allocator: std.mem.Allocator, r: anytype) !protocol.messag const checksum = try r.readBytesNoEof(4); // Read payload - const message = if (std.mem.eql(u8, &command, protocol.messages.VersionMessage.name())) - try protocol.messages.VersionMessage.deserializeReader(allocator, r) + const message: protocol.messages.Message = if (std.mem.eql(u8, &command, protocol.messages.VersionMessage.name())) + protocol.messages.Message{ .Version = try protocol.messages.VersionMessage.deserializeReader(allocator, r)} + else if (std.mem.eql(u8, &command, protocol.messages.VerackMessage.name())) + protocol.messages.Message{ .Verack = try protocol.messages.VerackMessage.deserializeReader(allocator, r)} else return error.InvalidCommand; errdefer message.deinit(allocator); @@ -89,12 +91,12 @@ pub fn receiveMessage(allocator: std.mem.Allocator, r: anytype) !protocol.messag return error.InvaliPayloadLen; } - return protocol.messages.Message{ .Version = message }; + return message; } // TESTS -test "ok_send_message" { +test "ok_send_version_message" { const ArrayList = std.ArrayList; const test_allocator = std.testing.allocator; const VersionMessage = protocol.messages.VersionMessage; @@ -130,6 +132,31 @@ test "ok_send_message" { switch (received_message) { .Version => |rm| try std.testing.expect(message.eql(&rm)), + .Verack => unreachable, + } +} + +test "ok_send_verack_message" { + const ArrayList = std.ArrayList; + const test_allocator = std.testing.allocator; + const VerackMessage = protocol.messages.VerackMessage; + + var list: std.ArrayListAligned(u8, null) = ArrayList(u8).init(test_allocator); + defer list.deinit(); + + const message = VerackMessage{}; + + const writer = list.writer(); + try sendMessage(test_allocator, writer, protocol.PROTOCOL_VERSION, protocol.BitcoinNetworkId.MAINNET, message); + var fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(list.items); + const reader = fbs.reader(); + + const received_message = try receiveMessage(test_allocator, reader); + defer received_message.deinit(test_allocator); + + switch (received_message) { + .Verack => {}, + .Version => unreachable, } } From 392c428a1d54f02bcd0fe809666eef1a5b99131e Mon Sep 17 00:00:00 2001 From: Supreme Labs <100731397+supreme2580@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:10:39 +0100 Subject: [PATCH 3/4] Implemented OP_OVER (#59) --- src/script/engine.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/script/engine.zig b/src/script/engine.zig index b561f80..be605e0 100644 --- a/src/script/engine.zig +++ b/src/script/engine.zig @@ -613,6 +613,38 @@ test "Script execution - OP_1 OP_2 OP_IFDUP" { try std.testing.expectEqual(2, element1); } +test "Script execution - OP_OVER" { + const allocator = std.testing.allocator; + + // Simple script: OP_1 OP_2 OP_3 OP_OVER + const script_bytes = [_]u8{ + Opcode.OP_1.toBytes(), + Opcode.OP_2.toBytes(), + Opcode.OP_3.toBytes(), + Opcode.OP_OVER.toBytes(), + }; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, .{}); + defer engine.deinit(); + + try engine.execute(); + + // Ensure the stack has the expected number of elements + try std.testing.expectEqual(@as(usize, 4), engine.stack.len()); + + // Check the stack elements + const element0 = try engine.stack.peekInt(0); + const element1 = try engine.stack.peekInt(1); + const element2 = try engine.stack.peekInt(2); + const element3 = try engine.stack.peekInt(3); + + try std.testing.expectEqual(2, element0); + try std.testing.expectEqual(3, element1); + try std.testing.expectEqual(2, element2); + try std.testing.expectEqual(1, element3); +} + test "Script execution - OP_1 OP_2 OP_DEPTH" { const allocator = std.testing.allocator; From 35eb160aea54ec9745de348b9bd01403f6d02ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= <34384633+tdelabro@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:41:25 +0200 Subject: [PATCH 4/4] fix: have protocol correct encoding of the bitcoin numbers (#69) --- src/script/engine.zig | 5 +- src/script/lib.zig | 124 ++++++- src/script/opcodes/arithmetic.zig | 557 ++++++++++++++++++------------ src/script/stack.zig | 64 +++- 4 files changed, 504 insertions(+), 246 deletions(-) diff --git a/src/script/engine.zig b/src/script/engine.zig index be605e0..e823778 100644 --- a/src/script/engine.zig +++ b/src/script/engine.zig @@ -84,7 +84,7 @@ pub const Engine = struct { } } - fn executeOpcode(self: *Engine, opcode: Opcode) !void { + fn executeOpcode(self: *Engine, opcode: Opcode) EngineError!void { self.log("Executing opcode: 0x{x:0>2}\n", .{opcode.toBytes()}); // Check if the opcode is a push data opcode @@ -411,7 +411,8 @@ pub const Engine = struct { fn opSize(self: *Engine) !void { const top_value = try self.stack.pop(); const len = top_value.len; - const result: ScriptNum = @intCast(len); + // Should be ok as the max len of an elem is MAX_SCRIPT_ELEMENT_SIZE (520) + const result: i32 = @intCast(len); try self.stack.pushElement(top_value); try self.stack.pushInt(result); diff --git a/src/script/lib.zig b/src/script/lib.zig index 879ef08..bcd8ca7 100644 --- a/src/script/lib.zig +++ b/src/script/lib.zig @@ -1,10 +1,9 @@ +const std = @import("std"); pub const engine = @import("engine.zig"); pub const stack = @import("stack.zig"); pub const arithmetic = @import("opcodes/arithmetic.zig"); const StackError = @import("stack.zig").StackError; -pub const ScriptNum = i64; - /// Maximum number of bytes pushable to the stack const MAX_SCRIPT_ELEMENT_SIZE = 520; @@ -78,3 +77,124 @@ pub const EngineError = error{ /// Encountered a disabled opcode DisabledOpcode, } || StackError; +/// A struct allowing for safe reading and writing of bitcoin numbers as well as performing mathematical operations. +/// +/// Bitcoin numbers are represented on the stack as 0 to 4 bytes little endian variable-lenght integer, +/// with the most significant bit reserved for the sign flag. +/// In the msb is already used an additionnal bytes will be added to carry the flag. +/// Eg. 0xff is encoded as [0xff, 0x00]. +/// +/// Thus both `0x80` and `0x00` can be read as zero, while it should be written as [0]u8{}. +/// It also implies that the largest negative number representable is not i32.MIN but i32.MIN + 1 == -i32.MAX. +/// +/// The mathematical operation performed on those number are allowd to overflow, making the result expand to 5 bytes. +/// Eg. ScriptNum.MAX + 1 will be encoded [0x0, 0x0, 0x0. 0x80, 0x0]. +/// Those overflowed value can successfully be writen back onto the stack as [5]u8, but any attempt to read them bac +/// as number will fail. They can still be read in other way tho (bool, array, etc). +/// +/// In order to handle this possibility of overflow the ScripNum are internally represented as i36, not i32. +pub const ScriptNum = struct { + /// The type used to internaly represent and do math onto the ScriptNum + pub const InnerReprType = i36; + /// The greatest valid number handled by the protocol + pub const MAX: i32 = std.math.maxInt(i32); + /// The lowest valid number handled by the protocol + pub const MIN: i32 = std.math.minInt(i32) + 1; + + value: Self.InnerReprType, + + const Self = @This(); + + /// Encode `Self.value` as variable-lenght integer + /// + /// In case of overflow, it can return as much as 5 bytes. + pub fn toBytes(self: Self, allocator: std.mem.Allocator) ![]u8 { + if (self.value == 0) { + return allocator.alloc(u8, 0); + } + + const is_negative = self.value < 0; + const bytes: [8]u8 = @bitCast(std.mem.nativeToLittle(u64, @abs(self.value))); + + var i: usize = 8; + while (i > 0) { + i -= 1; + if (bytes[i] != 0) { + i = i; + break; + } + } + const additional_byte: usize = @intFromBool(bytes[i] & 0x80 != 0); + var elem = try allocator.alloc(u8, i + 1 + additional_byte); + errdefer allocator.free(elem); + for (0..elem.len) |idx| elem[idx] = 0; + + @memcpy(elem[0 .. i + 1], bytes[0 .. i + 1]); + if (is_negative) { + elem[elem.len - 1] |= 0x80; + } + + return elem; + } + + /// Decode a variable-length integer as an instance of Self + /// + /// Will error if the input does not represent an int beetween ScriptNum.MIN and ScriptNum.MAX, + /// meaning that it cannot read back overflown numbers. + pub fn fromBytes(bytes: []u8) !Self { + if (bytes.len > 4) { + return StackError.InvalidValue; + } + if (bytes.len == 0) { + return .{ .value = 0 }; + } + + const is_negative = if (bytes[bytes.len - 1] & 0x80 != 0) true else false; + bytes[bytes.len - 1] &= 0x7f; + + const abs_value = std.mem.readVarInt(i32, bytes, .little); + + return .{ .value = if (is_negative) -abs_value else abs_value }; + } + + /// Add `rhs` to `self` + /// + /// * Safety: both arguments should be valid Bitcoin integer values (non overflown) + pub fn add(self: Self, rhs: Self) Self { + const result = std.math.add(Self.InnerReprType, self.value, rhs.value) catch unreachable; + return .{ .value = result }; + } + /// Substract `rhs` to `self` + /// + /// * Safety: both arguments should be valid Bitcoin integer values (non overflown) + pub fn sub(self: Self, rhs: Self) Self { + const result = std.math.sub(Self.InnerReprType, self.value, rhs.value) catch unreachable; + return .{ .value = result }; + } + /// Increment `self` by 1 + /// + /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) + pub fn addOne(self: Self) Self { + const result = std.math.add(Self.InnerReprType, self.value, 1) catch unreachable; + return .{ .value = result }; + } + /// Decrement `self` by 1 + /// + /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) + pub fn subOne(self: Self) Self { + const result = std.math.sub(Self.InnerReprType, self.value, 1) catch unreachable; + return .{ .value = result }; + } + /// Return the absolute value of `self` + /// + /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) + pub fn abs(self: Self) Self { + return if (self.value < 0) .{ .value = std.math.negate(self.value) catch unreachable } else self; + } + /// Return the opposite of `self` + /// + /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) + pub fn negate(self: Self) Self { + return .{ .value = std.math.negate(self.value) catch unreachable }; + } +}; diff --git a/src/script/opcodes/arithmetic.zig b/src/script/opcodes/arithmetic.zig index 9b731e2..a28aa13 100644 --- a/src/script/opcodes/arithmetic.zig +++ b/src/script/opcodes/arithmetic.zig @@ -6,192 +6,192 @@ const ScriptNum = @import("../lib.zig").ScriptNum; const ScriptFlags = @import("../lib.zig").ScriptFlags; const StackError = @import("../stack.zig").StackError; -/// OP_1ADD: Add 1 to the top stack item -pub fn op1Add(self: *Engine) !void { - const value = try self.stack.popInt(); - const result = @addWithOverflow(value, 1); - try self.stack.pushInt(result[0]); +/// Add 1 to the top stack item +pub fn op1Add(engine: *Engine) !void { + const value = try engine.stack.popScriptNum(); + const result = value.addOne(); + try engine.stack.pushScriptNum(result); } -/// OP_1SUB: Subtract 1 from the top stack item -pub fn op1Sub(self: *Engine) !void { - const value = try self.stack.popInt(); - const result = @subWithOverflow(value, 1); - try self.stack.pushInt(result[0]); +/// Subtract 1 from the top stack item +pub fn op1Sub(engine: *Engine) !void { + const value = try engine.stack.popScriptNum(); + const result = value.subOne(); + try engine.stack.pushScriptNum(result); } -/// OP_NEGATE: Negate the top stack item -pub fn opNegate(self: *Engine) !void { - const value = try self.stack.popInt(); - const result = if (value == std.math.minInt(ScriptNum)) - std.math.minInt(ScriptNum) - else - -value; - try self.stack.pushInt(result); +/// Negate the top stack item +pub fn opNegate(engine: *Engine) !void { + const value = try engine.stack.popScriptNum(); + const result = value.negate(); + try engine.stack.pushScriptNum(result); } /// Computes the absolute value of the top stack item pub fn opAbs(engine: *Engine) !void { - const value = try engine.stack.popInt(); - const result = if (value == std.math.minInt(ScriptNum)) - std.math.minInt(ScriptNum) // Handle overflow case - else if (value < 0) - -value - else - value; - try engine.stack.pushInt(result); + const value = try engine.stack.popScriptNum(); + const result = value.abs(); + try engine.stack.pushScriptNum(result); } -/// Pushes true if the top stack item is 0, false otherwise -pub fn opNot(self: *Engine) !void { - const value = try self.stack.popInt(); - const result = if (value == 0) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the top stack item is 0, 0 otherwise +/// +/// The consensus require we treat those as numbers and not boolean, +/// both while reading and writing. +pub fn opNot(engine: *Engine) !void { + const value = try engine.stack.popInt(); + const result: u8 = @intFromBool(value == 0); + try engine.stack.pushInt(result); } /// Pushes 1 if the top stack item is not 0, 0 otherwise -pub fn op0NotEqual(self: *Engine) !void { - const value = try self.stack.popInt(); - const result: ScriptNum = if (value != 0) 1 else 0; - try self.stack.pushInt(result); +pub fn op0NotEqual(engine: *Engine) !void { + const value = try engine.stack.popInt(); + const result: u8 = @intFromBool(value != 0); + try engine.stack.pushInt(result); } /// Adds the top two stack items -pub fn opAdd(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = @addWithOverflow(a, b); - try self.stack.pushInt(result[0]); +pub fn opAdd(engine: *Engine) !void { + const first = try engine.stack.popScriptNum(); + const second = try engine.stack.popScriptNum(); + const result = second.add(first); + try engine.stack.pushScriptNum(result); } /// Subtracts the top stack item from the second top stack item -pub fn opSub(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = @subWithOverflow(a, b); - try self.stack.pushInt(result[0]); +pub fn opSub(engine: *Engine) !void { + const first = try engine.stack.popScriptNum(); + const second = try engine.stack.popScriptNum(); + const result = second.sub(first); + try engine.stack.pushScriptNum(result); } -/// Pushes true if both top two stack items are non-zero, false otherwise -pub fn opBoolAnd(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if ((a != 0) and (b != 0)) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if both top two stack items are non-zero, 0 otherwise +pub fn opBoolAnd(engine: *Engine) !void { + const first = try engine.stack.popInt(); + const second = try engine.stack.popInt(); + const result: u8 = @intFromBool(first != 0 and second != 0); + try engine.stack.pushInt(result); } -/// Pushes true if either of the top two stack items is non-zero, false otherwise -pub fn opBoolOr(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if ((a != 0) or (b != 0)) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if either of the top two stack items is non-zero, 0 otherwise +pub fn opBoolOr(engine: *Engine) !void { + const first = try engine.stack.popInt(); + const second = try engine.stack.popInt(); + const result: u8 = @intFromBool(first != 0 or second != 0); + try engine.stack.pushInt(result); } -/// Pushes true if the top two stack items are equal, false otherwise -pub fn opNumEqual(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a == b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the top two stack items are equal, 0 otherwise +pub fn opNumEqual(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result: u8 = @intFromBool(a == b); + try engine.stack.pushInt(result); } /// Helper function to verify the top stack item is true -pub fn abstractVerify(self: *Engine) !void { - const verified = try self.stack.popBool(); +pub fn abstractVerify(engine: *Engine) !void { + const verified = try engine.stack.popBool(); if (!verified) { return StackError.VerifyFailed; } } /// Combines opNumEqual and abstractVerify operations -pub fn opNumEqualVerify(self: *Engine) !void { - try opNumEqual(self); - try abstractVerify(self); +pub fn opNumEqualVerify(engine: *Engine) !void { + try opNumEqual(engine); + try abstractVerify(engine); } -/// Pushes true if the top two stack items are not equal, false otherwise -pub fn opNumNotEqual(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a != b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the top two stack items are not equal, 0 otherwise +pub fn opNumNotEqual(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result: u8 = @intFromBool(a != b); + try engine.stack.pushInt(result); } -/// Pushes true if the second top stack item is less than the top stack item, false otherwise -pub fn opLessThan(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a < b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the second top stack item is less than the top stack item, 0 otherwise +pub fn opLessThan(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result: u8 = @intFromBool(a < b); + try engine.stack.pushInt(result); } -/// Pushes true if the second top stack item is greater than the top stack item, false otherwise -pub fn opGreaterThan(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a > b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the second top stack item is greater than the top stack item, 0 otherwise +pub fn opGreaterThan(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result = @intFromBool(a > b); + try engine.stack.pushInt(result); } -/// Pushes true if the second top stack item is less than or equal to the top stack item, false otherwise -pub fn opLessThanOrEqual(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a <= b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the second top stack item is less than or equal to the top stack item, 0 otherwise +pub fn opLessThanOrEqual(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result = @intFromBool(a <= b); + try engine.stack.pushInt(result); } -/// Pushes true if the second top stack item is greater than or equal to the top stack item, false otherwise -pub fn opGreaterThanOrEqual(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); - const result = if (a >= b) true else false; - try self.stack.pushBool(result); +/// Pushes 1 if the second top stack item is greater than or equal to the top stack item, 0 otherwise +pub fn opGreaterThanOrEqual(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); + const result = @intFromBool(a >= b); + try engine.stack.pushInt(result); } /// Pushes the minimum of the top two stack items -pub fn opMin(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); +pub fn opMin(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); const result = if (a < b) a else b; - try self.stack.pushInt(result); + try engine.stack.pushInt(result); } /// Pushes the maximum of the top two stack items -pub fn opMax(self: *Engine) !void { - const b = try self.stack.popInt(); - const a = try self.stack.popInt(); +pub fn opMax(engine: *Engine) !void { + const b = try engine.stack.popInt(); + const a = try engine.stack.popInt(); const result = if (a > b) a else b; - try self.stack.pushInt(result); + try engine.stack.pushInt(result); } /// Pushes true if x is within the range [min, max], false otherwise -pub fn opWithin(self: *Engine) !void { - const max = try self.stack.popInt(); - const min = try self.stack.popInt(); - const x = try self.stack.popInt(); - const result = if ((min <= x) and (x < max)) true else false; - try self.stack.pushBool(result); +pub fn opWithin(engine: *Engine) !void { + const max = try engine.stack.popInt(); + const min = try engine.stack.popInt(); + const x = try engine.stack.popInt(); + const result = @intFromBool(min <= x and x < max); + try engine.stack.pushInt(result); } test "OP_1ADD operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - input: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + input: i32, + expected: i32, }{ .{ .input = 0, .expected = 1 }, .{ .input = -1, .expected = 0 }, .{ .input = 42, .expected = 43 }, .{ .input = -100, .expected = -99 }, - .{ .input = std.math.maxInt(ScriptNum), .expected = std.math.minInt(ScriptNum) }, // Overflow case - .{ .input = std.math.minInt(ScriptNum), .expected = std.math.minInt(ScriptNum) + 1 }, + .{ .input = ScriptNum.MIN, .expected = ScriptNum.MIN + 1 }, + }; + const overflowTestCases = [_]struct { + input: i32, + expected: []const u8, + }{ + .{ .input = ScriptNum.MAX, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x0 } }, }; - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -210,7 +210,29 @@ test "OP_1ADD operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); + } + for (overflowTestCases) |tc| { + // Create a dummy script (content doesn't matter for this test) + const script_bytes = [_]u8{0x00}; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, ScriptFlags{}); + defer engine.deinit(); + + // Push the input values onto the stack + try engine.stack.pushInt(tc.input); + + // Execute OP_1ADD + try op1Add(&engine); + + // Check the result + const result = try engine.stack.pop(); + defer engine.allocator.free(result); + try testing.expect(std.mem.eql(u8, tc.expected, result)); + + // Ensure the stack is empty after popping the result + try testing.expectEqual(0, engine.stack.len()); } } @@ -218,20 +240,25 @@ test "OP_1SUB operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - input: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + input: i32, + expected: i32, }{ .{ .input = 0, .expected = -1 }, .{ .input = 1, .expected = 0 }, .{ .input = -1, .expected = -2 }, .{ .input = 42, .expected = 41 }, .{ .input = -100, .expected = -101 }, - .{ .input = std.math.maxInt(ScriptNum), .expected = std.math.maxInt(ScriptNum) - 1 }, - .{ .input = std.math.minInt(ScriptNum), .expected = std.math.maxInt(ScriptNum) }, // Underflow case + .{ .input = ScriptNum.MAX, .expected = ScriptNum.MAX - 1 }, + }; + const overflowTestCases = [_]struct { + input: i32, + expected: []const u8, + }{ + .{ .input = ScriptNum.MIN, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x80 } }, // Overflow case }; - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -250,7 +277,29 @@ test "OP_1SUB operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); + } + for (overflowTestCases) |tc| { + // Create a dummy script (content doesn't matter for this test) + const script_bytes = [_]u8{0x00}; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, ScriptFlags{}); + defer engine.deinit(); + + // Push the input values onto the stack + try engine.stack.pushInt(tc.input); + + // Execute OP_1SUB + try op1Sub(&engine); + + // Check the result + const result = try engine.stack.pop(); + defer engine.allocator.free(result); + try testing.expect(std.mem.eql(u8, tc.expected, result)); + + // Ensure the stack is empty after popping the result + try testing.expectEqual(0, engine.stack.len()); } } @@ -258,20 +307,20 @@ test "OP_NEGATE operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - input: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + input: i32, + expected: i32, }{ .{ .input = 0, .expected = 0 }, .{ .input = 1, .expected = -1 }, .{ .input = -1, .expected = 1 }, .{ .input = 42, .expected = -42 }, .{ .input = -42, .expected = 42 }, - .{ .input = std.math.maxInt(ScriptNum), .expected = -std.math.maxInt(ScriptNum) }, - .{ .input = std.math.minInt(ScriptNum), .expected = std.math.minInt(ScriptNum) }, // Special case + .{ .input = ScriptNum.MAX, .expected = ScriptNum.MIN }, + .{ .input = ScriptNum.MIN, .expected = ScriptNum.MAX }, }; - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -290,7 +339,7 @@ test "OP_NEGATE operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -298,20 +347,19 @@ test "OP_ABS operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - input: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + input: i32, + expected: i32, }{ .{ .input = 0, .expected = 0 }, .{ .input = 1, .expected = 1 }, .{ .input = -1, .expected = 1 }, .{ .input = 42, .expected = 42 }, .{ .input = -42, .expected = 42 }, - .{ .input = std.math.maxInt(ScriptNum), .expected = std.math.maxInt(ScriptNum) }, - .{ .input = std.math.minInt(ScriptNum), .expected = std.math.minInt(ScriptNum) }, // Special case + .{ .input = ScriptNum.MAX, .expected = ScriptNum.MAX }, + .{ .input = ScriptNum.MIN, .expected = ScriptNum.MAX }, }; - - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -330,7 +378,7 @@ test "OP_ABS operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -339,7 +387,7 @@ test "OP_NOT operation" { // Test cases const testCases = [_]struct { - input: ScriptNum, + input: i32, expected: bool, }{ .{ .input = 0, .expected = true }, @@ -347,8 +395,8 @@ test "OP_NOT operation" { .{ .input = -1, .expected = false }, .{ .input = 42, .expected = false }, .{ .input = -42, .expected = false }, - .{ .input = std.math.maxInt(ScriptNum), .expected = false }, - .{ .input = std.math.minInt(ScriptNum), .expected = false }, // Special case + .{ .input = ScriptNum.MAX, .expected = false }, + .{ .input = ScriptNum.MIN, .expected = false }, // Special case }; for (testCases) |tc| { @@ -370,7 +418,7 @@ test "OP_NOT operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -379,16 +427,16 @@ test "OP_0NOTEQUAL operation" { // Test cases const testCases = [_]struct { - input: ScriptNum, - expected: ScriptNum, + input: i32, + expected: i32, }{ .{ .input = 0, .expected = 0 }, .{ .input = 1, .expected = 1 }, .{ .input = -1, .expected = 1 }, .{ .input = 42, .expected = 1 }, .{ .input = -42, .expected = 1 }, - .{ .input = std.math.maxInt(ScriptNum), .expected = 1 }, - .{ .input = std.math.minInt(ScriptNum), .expected = 1 }, // Special case + .{ .input = ScriptNum.MAX, .expected = 1 }, + .{ .input = ScriptNum.MIN, .expected = 1 }, // Special case }; for (testCases) |tc| { @@ -410,7 +458,7 @@ test "OP_0NOTEQUAL operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -418,10 +466,10 @@ test "OP_ADD operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + a: i32, + b: i32, + expected: i32, }{ .{ .a = 0, .b = 0, .expected = 0 }, .{ .a = 0, .b = 1, .expected = 1 }, @@ -430,11 +478,19 @@ test "OP_ADD operation" { .{ .a = -1, .b = 1, .expected = 0 }, .{ .a = 42, .b = 42, .expected = 84 }, .{ .a = -42, .b = 42, .expected = 0 }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = std.math.minInt(ScriptNum) }, // Overflow case - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = std.math.maxInt(ScriptNum) }, // Underflow case + }; + const overflowTestCases = [_]struct { + a: i32, + b: i32, + expected: []const u8, + }{ + .{ .a = ScriptNum.MAX, .b = 1, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x0 } }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x80 } }, + .{ .a = ScriptNum.MAX, .b = ScriptNum.MAX, .expected = &[_]u8{ 0xfe, 0xff, 0xff, 0xff, 0x0 } }, + .{ .a = ScriptNum.MIN, .b = ScriptNum.MIN, .expected = &[_]u8{ 0xfe, 0xff, 0xff, 0xff, 0x80 } }, }; - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -454,7 +510,30 @@ test "OP_ADD operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); + } + for (overflowTestCases) |tc| { + // Create a dummy script (content doesn't matter for this test) + const script_bytes = [_]u8{0x00}; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, ScriptFlags{}); + defer engine.deinit(); + + // Push the input values onto the stack + try engine.stack.pushInt(tc.a); + try engine.stack.pushInt(tc.b); + + // Execute OP_ADD + try opAdd(&engine); + + // Check the result + const result = try engine.stack.pop(); + defer engine.allocator.free(result); + try testing.expect(std.mem.eql(u8, tc.expected, result)); + + // Ensure the stack is empty after popping the result + try testing.expectEqual(0, engine.stack.len()); } } @@ -462,10 +541,10 @@ test "OP_SUB operation" { const allocator = testing.allocator; // Test cases - const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, - expected: ScriptNum, + const normalTestCases = [_]struct { + a: i32, + b: i32, + expected: i32, }{ .{ .a = 0, .b = 0, .expected = 0 }, .{ .a = 0, .b = 1, .expected = -1 }, @@ -474,11 +553,20 @@ test "OP_SUB operation" { .{ .a = -1, .b = 1, .expected = -2 }, .{ .a = 42, .b = 42, .expected = 0 }, .{ .a = -42, .b = 42, .expected = -84 }, - .{ .a = std.math.maxInt(ScriptNum), .b = -1, .expected = std.math.minInt(ScriptNum) }, // Overflow case - .{ .a = std.math.minInt(ScriptNum), .b = 1, .expected = std.math.maxInt(ScriptNum) }, // Underflow case + }; + // Those will overflow, meaning the cannot be read back as numbers, but can still successfully be pushed on the stack + const overflowTestCases = [_]struct { + a: i32, + b: i32, + expected: []const u8, + }{ + .{ .a = ScriptNum.MAX, .b = -1, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x0 } }, + .{ .a = ScriptNum.MIN, .b = 1, .expected = &[_]u8{ 0x0, 0x0, 0x0, 0x80, 0x80 } }, + .{ .a = ScriptNum.MIN, .b = ScriptNum.MAX, .expected = &[_]u8{ 0xfe, 0xff, 0xff, 0xff, 0x80 } }, + .{ .a = ScriptNum.MAX, .b = ScriptNum.MIN, .expected = &[_]u8{ 0xfe, 0xff, 0xff, 0xff, 0x00 } }, }; - for (testCases) |tc| { + for (normalTestCases) |tc| { // Create a dummy script (content doesn't matter for this test) const script_bytes = [_]u8{0x00}; const script = Script.init(&script_bytes); @@ -498,7 +586,30 @@ test "OP_SUB operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); + } + for (overflowTestCases) |tc| { + // Create a dummy script (content doesn't matter for this test) + const script_bytes = [_]u8{0x00}; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, ScriptFlags{}); + defer engine.deinit(); + + // Push the input values onto the stack + try engine.stack.pushInt(tc.a); + try engine.stack.pushInt(tc.b); + + // Execute OP_SUB + try opSub(&engine); + + // Check the result + const result = try engine.stack.pop(); + defer engine.allocator.free(result); + try testing.expect(std.mem.eql(u8, tc.expected, result)); + + // Ensure the stack is empty after popping the result + try testing.expectEqual(0, engine.stack.len()); } } @@ -507,8 +618,8 @@ test "OP_BOOLOR operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = false }, @@ -518,8 +629,8 @@ test "OP_BOOLOR operation" { .{ .a = -1, .b = 1, .expected = true }, .{ .a = 42, .b = 42, .expected = true }, .{ .a = -42, .b = 42, .expected = true }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = true }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = true }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = true }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = true }, }; for (testCases) |tc| { @@ -542,7 +653,7 @@ test "OP_BOOLOR operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -551,8 +662,8 @@ test "OP_NUMEQUAL operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = true }, @@ -562,8 +673,8 @@ test "OP_NUMEQUAL operation" { .{ .a = -1, .b = 1, .expected = false }, .{ .a = 42, .b = 42, .expected = true }, .{ .a = -42, .b = 42, .expected = false }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = false }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = false }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = false }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = false }, }; for (testCases) |tc| { @@ -586,7 +697,7 @@ test "OP_NUMEQUAL operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -595,8 +706,8 @@ test "OP_NUMNOTEQUAL operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = false }, @@ -606,8 +717,8 @@ test "OP_NUMNOTEQUAL operation" { .{ .a = -1, .b = 1, .expected = true }, .{ .a = 42, .b = 42, .expected = false }, .{ .a = -42, .b = 42, .expected = true }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = true }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = true }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = true }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = true }, }; for (testCases) |tc| { @@ -630,7 +741,7 @@ test "OP_NUMNOTEQUAL operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -639,8 +750,8 @@ test "OP_LESSTHAN operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = false }, @@ -650,8 +761,8 @@ test "OP_LESSTHAN operation" { .{ .a = -1, .b = 1, .expected = true }, .{ .a = 42, .b = 42, .expected = false }, .{ .a = -42, .b = 42, .expected = true }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = false }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = true }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = false }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = true }, }; for (testCases) |tc| { @@ -674,7 +785,7 @@ test "OP_LESSTHAN operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -683,8 +794,8 @@ test "OP_GREATERTHAN operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = false }, @@ -694,8 +805,8 @@ test "OP_GREATERTHAN operation" { .{ .a = -1, .b = 1, .expected = false }, .{ .a = 42, .b = 42, .expected = false }, .{ .a = -42, .b = 42, .expected = false }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = true }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = false }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = true }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = false }, }; for (testCases) |tc| { @@ -718,7 +829,7 @@ test "OP_GREATERTHAN operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -727,8 +838,8 @@ test "OP_LESSTHANOREQUAL operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = true }, @@ -738,8 +849,8 @@ test "OP_LESSTHANOREQUAL operation" { .{ .a = -1, .b = 1, .expected = true }, .{ .a = 42, .b = 42, .expected = true }, .{ .a = -42, .b = 42, .expected = true }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = false }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = true }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = false }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = true }, }; for (testCases) |tc| { @@ -762,7 +873,7 @@ test "OP_LESSTHANOREQUAL operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -771,8 +882,8 @@ test "OP_GREATERTHANOREQUAL operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, expected: bool, }{ .{ .a = 0, .b = 0, .expected = true }, @@ -782,8 +893,8 @@ test "OP_GREATERTHANOREQUAL operation" { .{ .a = -1, .b = 1, .expected = false }, .{ .a = 42, .b = 42, .expected = true }, .{ .a = -42, .b = 42, .expected = false }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = true }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = false }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = true }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = false }, }; for (testCases) |tc| { @@ -806,7 +917,7 @@ test "OP_GREATERTHANOREQUAL operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -815,9 +926,9 @@ test "OP_MIN operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, - expected: ScriptNum, + a: i32, + b: i32, + expected: i32, }{ .{ .a = 0, .b = 0, .expected = 0 }, .{ .a = 0, .b = 1, .expected = 0 }, @@ -826,8 +937,8 @@ test "OP_MIN operation" { .{ .a = -1, .b = 1, .expected = -1 }, .{ .a = 42, .b = 42, .expected = 42 }, .{ .a = -42, .b = 42, .expected = -42 }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = 1 }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = std.math.minInt(ScriptNum) }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = 1 }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = ScriptNum.MIN }, }; for (testCases) |tc| { @@ -850,7 +961,7 @@ test "OP_MIN operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -859,9 +970,9 @@ test "OP_MAX operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, - expected: ScriptNum, + a: i32, + b: i32, + expected: i32, }{ .{ .a = 0, .b = 0, .expected = 0 }, .{ .a = 0, .b = 1, .expected = 1 }, @@ -870,8 +981,8 @@ test "OP_MAX operation" { .{ .a = -1, .b = 1, .expected = 1 }, .{ .a = 42, .b = 42, .expected = 42 }, .{ .a = -42, .b = 42, .expected = 42 }, - .{ .a = std.math.maxInt(ScriptNum), .b = 1, .expected = std.math.maxInt(ScriptNum) }, - .{ .a = std.math.minInt(ScriptNum), .b = -1, .expected = -1 }, + .{ .a = ScriptNum.MAX, .b = 1, .expected = ScriptNum.MAX }, + .{ .a = ScriptNum.MIN, .b = -1, .expected = -1 }, }; for (testCases) |tc| { @@ -894,7 +1005,7 @@ test "OP_MAX operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -903,9 +1014,9 @@ test "OP_WITHIN operation" { // Test cases const testCases = [_]struct { - x: ScriptNum, - min: ScriptNum, - max: ScriptNum, + x: i32, + min: i32, + max: i32, expected: bool, }{ .{ .x = 0, .min = -1, .max = 1, .expected = true }, @@ -937,7 +1048,7 @@ test "OP_WITHIN operation" { try testing.expectEqual(tc.expected, result); // Ensure the stack is empty after popping the result - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } @@ -946,15 +1057,15 @@ test "OP_NUMEQUALVERIFY operation" { // Test cases const testCases = [_]struct { - a: ScriptNum, - b: ScriptNum, + a: i32, + b: i32, shouldVerify: bool, }{ .{ .a = 0, .b = 0, .shouldVerify = true }, .{ .a = 1, .b = 1, .shouldVerify = true }, .{ .a = -1, .b = -1, .shouldVerify = true }, - .{ .a = std.math.maxInt(ScriptNum), .b = std.math.maxInt(ScriptNum), .shouldVerify = true }, - .{ .a = std.math.minInt(ScriptNum), .b = std.math.minInt(ScriptNum), .shouldVerify = true }, + .{ .a = ScriptNum.MAX, .b = ScriptNum.MAX, .shouldVerify = true }, + .{ .a = ScriptNum.MIN, .b = ScriptNum.MIN, .shouldVerify = true }, .{ .a = 0, .b = 1, .shouldVerify = false }, .{ .a = 1, .b = 0, .shouldVerify = false }, .{ .a = -1, .b = 1, .shouldVerify = false }, @@ -978,12 +1089,12 @@ test "OP_NUMEQUALVERIFY operation" { // If it should verify, expect no error try opNumEqualVerify(&engine); // Ensure the stack is empty after successful verification - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } else { // If it should not verify, expect VerifyFailed error try testing.expectError(StackError.VerifyFailed, opNumEqualVerify(&engine)); // The stack should be empty even after a failed verification - try testing.expectEqual(@as(usize, 0), engine.stack.len()); + try testing.expectEqual(0, engine.stack.len()); } } } diff --git a/src/script/stack.zig b/src/script/stack.zig index 8c9a467..752b5d5 100644 --- a/src/script/stack.zig +++ b/src/script/stack.zig @@ -64,17 +64,6 @@ pub const Stack = struct { }; } - /// Push an integer onto the stack - /// - /// # Arguments - /// - `value`: The integer value to be pushed onto the stack - /// - /// # Returns - /// - `StackError` if out of memory - pub fn pushInt(self: *Stack, value: ScriptNum) StackError!void { - try self.pushByteArray(std.mem.asBytes(&value)); - } - /// Push an item onto the stack(does not create copy of item) /// /// # Arguments @@ -89,20 +78,57 @@ pub const Stack = struct { }; } + pub fn pushInt(self: *Stack, value: i32) StackError!void { + if (value == 0) { + const elem = try self.allocator.alloc(u8, 0); + errdefer self.allocator.free(elem); + try self.pushElement(elem); + return; + } + + const is_negative = value < 0; + const bytes: [4]u8 = @bitCast(std.mem.nativeToLittle(u32, @abs(value))); + + var i: usize = 4; + while (i > 0) { + i -= 1; + if (bytes[i] != 0) { + i = i; + break; + } + } + const additional_byte: usize = @intFromBool(bytes[i] & 0x80 != 0); + var elem = try self.allocator.alloc(u8, i + 1 + additional_byte); + errdefer self.allocator.free(elem); + for (0..elem.len) |idx| elem[idx] = 0; + + @memcpy(elem[0 .. i + 1], bytes[0 .. i + 1]); + if (is_negative) { + elem[elem.len - 1] |= 0x80; + } + + try self.pushElement(elem); + } + + pub fn pushScriptNum(self: *Stack, value: ScriptNum) StackError!void { + try self.pushElement(try value.toBytes(self.allocator)); + } + /// Pop an integer from the stack /// /// # Returns /// - `ScriptNum`: The popped integer value /// - `StackError` if the stack is empty or the value is invalid - pub fn popInt(self: *Stack) StackError!ScriptNum { + pub fn popScriptNum(self: *Stack) StackError!ScriptNum { const value = try self.pop(); defer self.allocator.free(value); - if (value.len > 8) return StackError.InvalidValue; - - return std.mem.readVarInt(ScriptNum, value, native_endian); + return ScriptNum.fromBytes(value); } + pub fn popInt(self: *Stack) !i32 { + return @intCast((try self.popScriptNum()).value); + } /// Pop a boolean value from the stack /// /// # Returns @@ -302,11 +328,11 @@ test "Stack pushInt and popInt" { try testing.expectEqual(0, try stack.popInt()); // Test pushing and popping large integers - try stack.pushInt(std.math.maxInt(ScriptNum)); - try testing.expectEqual(std.math.maxInt(ScriptNum), try stack.popInt()); + try stack.pushInt(ScriptNum.MAX); + try testing.expectEqual(ScriptNum.MAX, try stack.popInt()); - try stack.pushInt(std.math.minInt(ScriptNum)); - try testing.expectEqual(std.math.minInt(ScriptNum), try stack.popInt()); + try stack.pushInt(ScriptNum.MIN); + try testing.expectEqual(ScriptNum.MIN, try stack.popInt()); // Test popping from empty stack try testing.expectError(StackError.StackUnderflow, stack.popInt());