diff --git a/src/network/p2p.zig b/src/network/p2p.zig index 218b941..ebba510 100644 --- a/src/network/p2p.zig +++ b/src/network/p2p.zig @@ -51,6 +51,7 @@ pub const P2P = struct { // TODO: Implement the P2P network handler // Initialize the listener // const address = try net.Address.parseIp4("0.0.0.0", self.config.p2p_port); + // std.debug.panic("{any}", .{address}); // const stream = try net.tcpConnectToAddress(address); // self.listener = net.Server{ diff --git a/src/network/protocol/messages/getaddr.zig b/src/network/protocol/messages/getaddr.zig new file mode 100644 index 0000000..3d842b5 --- /dev/null +++ b/src/network/protocol/messages/getaddr.zig @@ -0,0 +1,62 @@ +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; + +/// GetaddrMessage represents the "getaddr" message +/// +/// https://developer.bitcoin.org/reference/p2p_networking.html#getaddr +pub const GetaddrMessage = struct { + // getaddr message do not contain any payload, thus there is no field + + pub inline fn name() *const [12]u8 { + return protocol.CommandNames.GETADDR ++ [_]u8{0} ** 5; + } + + pub fn checksum(self: GetaddrMessage) [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 GetaddrMessage, allocator: std.mem.Allocator) ![]u8 { + _ = self; + _ = allocator; + return &.{}; + } + + pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !GetaddrMessage { + _ = allocator; + _ = r; + return GetaddrMessage{}; + } + + pub fn hintSerializedLen(self: GetaddrMessage) usize { + _ = self; + return 0; + } +}; + +// TESTS + +test "ok_full_flow_GetaddrMessage" { + const allocator = std.testing.allocator; + + { + const msg = GetaddrMessage{}; + + const payload = try msg.serialize(allocator); + defer allocator.free(payload); + const deserialized_msg = try GetaddrMessage.deserializeReader(allocator, payload); + _ = deserialized_msg; + + try std.testing.expect(payload.len == 0); + } +} diff --git a/src/network/protocol/messages/lib.zig b/src/network/protocol/messages/lib.zig index aca20c0..dac5b76 100644 --- a/src/network/protocol/messages/lib.zig +++ b/src/network/protocol/messages/lib.zig @@ -2,19 +2,27 @@ const std = @import("std"); pub const VersionMessage = @import("version.zig").VersionMessage; pub const VerackMessage = @import("verack.zig").VerackMessage; pub const MempoolMessage = @import("mempool.zig").MempoolMessage; +pub const GetaddrMessage = @import("getaddr.zig").GetaddrMessage; -pub const MessageTypes = enum { Version, Verack, Mempool }; +pub const MessageTypes = enum { + Version, + Verack, + Mempool, + Getaddr, +}; pub const Message = union(MessageTypes) { Version: VersionMessage, Verack: VerackMessage, Mempool: MempoolMessage, + Getaddr: GetaddrMessage, pub fn deinit(self: Message, allocator: std.mem.Allocator) void { switch (self) { .Version => |m| m.deinit(allocator), .Verack => {}, .Mempool => {}, + .Getaddr => {}, } } pub fn checksum(self: Message) [4]u8 { @@ -22,6 +30,7 @@ pub const Message = union(MessageTypes) { .Version => |m| m.checksum(), .Verack => |m| m.checksum(), .Mempool => |m| m.checksum(), + .Getaddr => |m| m.checksum(), }; } @@ -30,6 +39,7 @@ pub const Message = union(MessageTypes) { .Version => |m| m.hintSerializedLen(), .Verack => |m| m.hintSerializedLen(), .Mempool => |m| m.hintSerializedLen(), + .Getaddr => |m| m.hintSerializedLen(), }; } }; diff --git a/src/network/wire/lib.zig b/src/network/wire/lib.zig index fe22328..59d8ea0 100644 --- a/src/network/wire/lib.zig +++ b/src/network/wire/lib.zig @@ -76,12 +76,14 @@ pub fn receiveMessage(allocator: std.mem.Allocator, r: anytype) !protocol.messag const checksum = try r.readBytesNoEof(4); // Read payload - 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)} + 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 if (std.mem.eql(u8, &command, protocol.messages.MempoolMessage.name())) protocol.messages.Message{ .Mempool = try protocol.messages.MempoolMessage.deserializeReader(allocator, r)} + else if (std.mem.eql(u8, &command, protocol.messages.GetaddrMessage.name())) + protocol.messages.Message{ .Getaddr = try protocol.messages.GetaddrMessage.deserializeReader(allocator, r) } else return error.InvalidCommand; errdefer message.deinit(allocator); @@ -136,6 +138,7 @@ test "ok_send_version_message" { .Version => |rm| try std.testing.expect(message.eql(&rm)), .Verack => unreachable, .Mempool => unreachable, + .Getaddr => unreachable, } } @@ -161,6 +164,7 @@ test "ok_send_verack_message" { .Verack => {}, .Version => unreachable, .Mempool => unreachable, + .Getaddr => unreachable, } } @@ -186,6 +190,7 @@ test "ok_send_mempool_message" { .Mempool => {}, .Verack => unreachable, .Version => unreachable, + .Getaddr => unreachable, } } diff --git a/src/script/engine.zig b/src/script/engine.zig index be605e0..3da6ce6 100644 --- a/src/script/engine.zig +++ b/src/script/engine.zig @@ -2,12 +2,14 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Stack = @import("stack.zig").Stack; const Script = @import("lib.zig").Script; +const asBool = @import("lib.zig").asBool; const ScriptNum = @import("lib.zig").ScriptNum; const ScriptFlags = @import("lib.zig").ScriptFlags; const arithmetic = @import("opcodes/arithmetic.zig"); const Opcode = @import("opcodes/constant.zig").Opcode; const isUnnamedPushNDataOpcode = @import("opcodes/constant.zig").isUnnamedPushNDataOpcode; const EngineError = @import("lib.zig").EngineError; +const ripemd160 = @import("bitcoin-primitives").hashes.Ripemd160; /// Engine is the virtual machine that executes Bitcoin scripts pub const Engine = struct { /// The script being executed @@ -52,7 +54,11 @@ pub const Engine = struct { /// Log debug information fn log(self: *Engine, comptime format: []const u8, args: anytype) void { _ = self; - std.debug.print(format, args); + _ = format; + _ = args; + // Uncomment this if you need to access the log + // In the future it would be cool to log somewhere else than stderr + // std.debug.print(format, args); } /// Execute the script @@ -84,7 +90,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 @@ -137,6 +143,7 @@ pub const Engine = struct { Opcode.OP_MIN => try arithmetic.opMin(self), Opcode.OP_MAX => try arithmetic.opMax(self), Opcode.OP_WITHIN => try arithmetic.opWithin(self), + Opcode.OP_RIPEMD160 => try self.opRipemd160(), Opcode.OP_HASH160 => try self.opHash160(), Opcode.OP_CHECKSIG => try self.opCheckSig(), Opcode.OP_NIP => try self.opNip(), @@ -241,201 +248,134 @@ pub const Engine = struct { _ = self; } - /// OP_IF: If the top stack value is not False, the statements are executed. The top stack value is removed. + /// OP_VERIFY: Pop the top value and verify it is true /// - /// # Returns - /// OP_VERIFY: Verify the top stack value - /// - /// # Returns - /// - `EngineError`: If verification fails or an error occurs + /// If verification fails or an error occurs fn opVerify(self: *Engine) !void { - const value = try self.stack.pop(); - defer self.allocator.free(value); - if (value.len == 0 or (value.len == 1 and value[0] == 0)) { + const value = try self.stack.popBool(); + if (!value) { return error.VerifyFailed; } } /// OP_RETURN: Immediately halt execution - /// - /// # Returns - /// - `EngineError.EarlyReturn`: Always fn opReturn(self: *Engine) !void { _ = self; return error.EarlyReturn; } /// OP_2DROP: Drops top 2 stack items - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 fn op2Drop(self: *Engine) !void { - if (self.stack.len() < 2) { - return error.StackUnderflow; - } - const a = try self.stack.pop(); - const b = try self.stack.pop(); - - defer self.allocator.free(a); - defer self.allocator.free(b); + const first = try self.stack.pop(); + defer self.allocator.free(first); + const second = try self.stack.pop(); + defer self.allocator.free(second); } /// OP_2DUP: Duplicates top 2 stack item - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 fn op2Dup(self: *Engine) !void { - if (self.stack.len() < 2) { - return error.StackUnderflow; - } - - const second_item = try self.stack.peek(0); - const first_item = try self.stack.peek(1); - try self.stack.pushByteArray(first_item); - try self.stack.pushByteArray(second_item); + const first = try self.stack.peek(0); + const second = try self.stack.peek(1); + try self.stack.pushByteArray(second); + try self.stack.pushByteArray(first); } /// OP_3DUP: Duplicates top 3 stack item - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 3 fn op3Dup(self: *Engine) !void { - if (self.stack.len() < 3) { - return error.StackUnderflow; - } - const third_item = try self.stack.peek(2); - const second_item = try self.stack.peek(1); - const first_item = try self.stack.peek(0); - try self.stack.pushByteArray(third_item); - try self.stack.pushByteArray(second_item); - try self.stack.pushByteArray(first_item); + const first = try self.stack.peek(0); + const second = try self.stack.peek(1); + const third = try self.stack.peek(2); + try self.stack.pushByteArray(third); + try self.stack.pushByteArray(second); + try self.stack.pushByteArray(first); } /// OP_DEPTH: Puts the number of stack items onto the stack. - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length == 0 fn opDepth(self: *Engine) !void { - if (self.stack.len() == 0) { - return error.StackUnderflow; - } const stack_length = self.stack.len(); - const u8_stack_length: u8 = @intCast(stack_length); - try self.stack.pushByteArray(&[_]u8{u8_stack_length}); + // Casting should be fine as stack length cannot contain more than 1000. + try self.stack.pushInt(@intCast(stack_length)); } - /// OP_IFDUP: If the top stack value is not 0, duplicate itp - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 1 + /// OP_IFDUP: If the top stack value is not 0, duplicate it fn opIfDup(self: *Engine) !void { - if (self.stack.len() < 1) { - return error.StackUnderflow; - } const value = try self.stack.peek(0); - if (value.len != 1 or value[0] != 0) { + if (asBool(value)) { try self.stack.pushByteArray(value); } } /// OP_DROP: Drops top stack item - /// - /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 1 fn opDrop(self: *Engine) !void { - if (self.stack.len() < 1) { - return error.StackUnderflow; - } - const a = try self.stack.pop(); - defer self.allocator.free(a); + const item = try self.stack.pop(); + defer self.allocator.free(item); } /// OP_DUP: Duplicate the top stack item - /// - /// # Returns - /// - `EngineError`: If an error occurs during execution fn opDup(self: *Engine) !void { const value = try self.stack.peek(0); try self.stack.pushByteArray(value); } /// OP_NIP: Removes the second-to-top stack item - /// - /// - will return an error if initial stack length < 2 fn opNip(self: *Engine) !void { - const top_value = try self.stack.pop(); - const second_to_top_value = try self.stack.pop(); - try self.stack.pushElement(top_value); - // defer self.allocator.free(top_value); - defer self.allocator.free(second_to_top_value); + const first = try self.stack.pop(); + errdefer self.allocator.free(first); + const second = try self.stack.pop(); + defer self.allocator.free(second); + try self.stack.pushElement(first); } /// OP_OVER: Copies the second-to-top stack item to the top - /// - /// /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 fn opOver(self: *Engine) !void { const value = try self.stack.peek(1); try self.stack.pushByteArray(value); } /// OP_SWAP: The top two items on the stack are swapped. - /// - /// /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 fn opSwap(self: *Engine) !void { - const top_value = try self.stack.pop(); - const second_to_top_value = try self.stack.pop(); + const first = try self.stack.pop(); + errdefer self.allocator.free(first); + const second = try self.stack.pop(); + errdefer self.allocator.free(second); - try self.stack.pushElement(top_value); - try self.stack.pushElement(second_to_top_value); + try self.stack.pushElement(first); + try self.stack.pushElement(second); } /// OP_TUCK: The item at the top of the stack is copied and inserted before the second-to-top item. - /// - /// /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 fn opTuck(self: *Engine) !void { - const top_value = try self.stack.pop(); - const second_to_top_value = try self.stack.pop(); + const first = try self.stack.pop(); + errdefer self.allocator.free(first); + const second = try self.stack.pop(); + errdefer self.allocator.free(second); - try self.stack.pushByteArray(second_to_top_value); //this must be pushBytesArray because we need the variable again - try self.stack.pushElement(top_value); - try self.stack.pushElement(second_to_top_value); + try self.stack.pushByteArray(first); + try self.stack.pushElement(second); + try self.stack.pushElement(first); } - /// OP_SIZE:Pushes the string length of the top element of the stack - /// - /// /// # Returns - /// - "EngineError.StackUnderflow": if initial stack length < 2 + /// OP_SIZE: Pushes the size of the top element fn opSize(self: *Engine) !void { - const top_value = try self.stack.pop(); - const len = top_value.len; - const result: ScriptNum = @intCast(len); + const first = try self.stack.peek(0); + // Should be ok as the max len of an elem is MAX_SCRIPT_ELEMENT_SIZE (520) + const len: i32 = @intCast(first.len); - try self.stack.pushElement(top_value); - try self.stack.pushInt(result); + try self.stack.pushInt(len); } /// OP_EQUAL: Push 1 if the top two items are equal, 0 otherwise - /// - /// # Returns - /// - `EngineError`: If an error occurs during execution fn opEqual(self: *Engine) EngineError!void { - const b = try self.stack.pop(); - const a = try self.stack.pop(); + const first = try self.stack.pop(); + defer self.allocator.free(first); + const second = try self.stack.pop(); + defer self.allocator.free(second); - defer self.allocator.free(b); - defer self.allocator.free(a); - - const equal = std.mem.eql(u8, a, b); - try self.stack.pushByteArray(if (equal) &[_]u8{1} else &[_]u8{0}); + const are_equal = std.mem.eql(u8, first, second); + try self.stack.pushBool(are_equal); } /// OP_EQUALVERIFY: OP_EQUAL followed by OP_VERIFY - /// - /// # Returns - /// - `EngineError`: If verification fails or an error occurs fn opEqualVerify(self: *Engine) !void { try self.opEqual(); try self.opVerify(); @@ -459,23 +399,31 @@ pub const Engine = struct { /// - `EngineError`: If an error occurs during execution fn opCheckSig(self: *Engine) !void { const pubkey = try self.stack.pop(); - const sig = try self.stack.pop(); defer self.allocator.free(pubkey); + const sig = try self.stack.pop(); defer self.allocator.free(sig); // TODO: Implement actual signature checking // Assume signature is valid for now - try self.stack.pushByteArray(&[_]u8{1}); + try self.stack.pushBool(true); } fn opDisabled(self: *Engine) !void { - std.debug.print("Attempt to execute disabled opcode: 0x{x:0>2}\n", .{self.script.data[self.pc]}); + _ = self; return error.DisabledOpcode; } fn opInvalid(self: *Engine) !void { - std.debug.print("Attempt to execute invalid opcode: 0x{x:0>2}\n", .{self.script.data[self.pc]}); + _ = self; return error.UnknownOpcode; } + + fn opRipemd160(self: *Engine) !void { + const data = try self.stack.pop(); + defer self.allocator.free(data); + var hash: [ripemd160.digest_length]u8 = undefined; + ripemd160.hash(data, &hash, .{}); + try self.stack.pushByteArray(&hash); + } }; test "Script execution - OP_1 OP_1 OP_EQUAL" { @@ -775,7 +723,7 @@ test "Script execution OP_1 OP_2 OP_3 OP_TUCK" { const allocator = std.testing.allocator; // Simple script: OP_1 OP_2 OP_3 OP_TUCK - const script_bytes = [_]u8{ 0x51, 0x52, 0x53, 0x7d }; + const script_bytes = [_]u8{ Opcode.OP_1.toBytes(), Opcode.OP_2.toBytes(), Opcode.OP_3.toBytes(), Opcode.OP_TUCK.toBytes() }; const script = Script.init(&script_bytes); var engine = Engine.init(allocator, script, .{}); @@ -787,10 +735,12 @@ test "Script execution OP_1 OP_2 OP_3 OP_TUCK" { 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(3, element0); + try std.testing.expectEqual(2, element1); + try std.testing.expectEqual(3, element2); + try std.testing.expectEqual(1, element3); } test "Script execution OP_1 OP_2 OP_3 OP_SIZE" { @@ -812,3 +762,42 @@ test "Script execution OP_1 OP_2 OP_3 OP_SIZE" { try std.testing.expectEqual(1, element0); try std.testing.expectEqual(3, element1); } + +test "Script execution OP_RIPEMD160" { + const test_cases = [_]struct { + input: []const u8, + expected: []const u8, + }{ + .{ .input = "", .expected = "9c1185a5c5e9fc54612808977ee8f548b2258d31" }, + .{ .input = "a", .expected = "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe" }, + .{ .input = "abc", .expected = "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc" }, + .{ .input = "message digest", .expected = "5d0689ef49d2fae572b881b123a85ffa21595f36" }, + }; + + for (test_cases) |case| { + const allocator = std.testing.allocator; + + const script_bytes = [_]u8{Opcode.OP_RIPEMD160.toBytes()}; + const script = Script.init(&script_bytes); + + var engine = Engine.init(allocator, script, .{}); + defer engine.deinit(); + + // Push the input onto the stack + try engine.stack.pushByteArray(case.input); + + // Call opRipemd160 + try engine.execute(); + + // Pop the result from the stack + const result = try engine.stack.pop(); + defer engine.allocator.free(result); // Free the result after use + + // Convert expected hash to bytes + var expected_output: [ripemd160.digest_length]u8 = undefined; + _ = try std.fmt.hexToBytes(&expected_output, case.expected); + + // Compare the result with the expected hash + try std.testing.expectEqualSlices(u8, &expected_output, result); + } +} diff --git a/src/script/lib.zig b/src/script/lib.zig index 879ef08..d63a507 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,145 @@ pub const EngineError = error{ /// Encountered a disabled opcode DisabledOpcode, } || StackError; + +/// Decode bytes as a boolean +/// +/// false can be represented as empty array [], positive zero [0x0 ... 0x0] and negative zero [0x0 ... 0x0 0x80]. +/// any other sequence of bytes means true. +pub fn asBool(bytes: []const u8) bool { + for (0..bytes.len) |i| { + if (bytes[i] != 0 and (i != bytes.len - 1 or bytes[i] != 0)) { + return true; + } + } + return false; +} + +/// Decode bytesas the little endian, bit flag signed, variable-length representation of an i32 +/// +/// 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 asInt(bytes: []const u8) StackError!i32 { + if (bytes.len > 4) { + return StackError.InvalidValue; + } + if (bytes.len == 0) { + return 0; + } + + const is_negative = if (bytes[bytes.len - 1] & 0x80 != 0) true else false; + var owned_bytes = std.mem.zeroes([4]u8); + @memcpy(owned_bytes[0..bytes.len], bytes[0..bytes.len]); + // Erase the sign bit + owned_bytes[bytes.len - 1] &= 0x7f; + + const abs_value = std.mem.readInt(i32, &owned_bytes, .little); + + return if (is_negative) -abs_value else abs_value; +} + +/// 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(); + + pub fn new(value: i32) Self { + return .{ .value = value }; + } + + /// 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; + } + + /// 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..1c03843 100644 --- a/src/script/stack.zig +++ b/src/script/stack.zig @@ -1,6 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const ScriptNum = @import("lib.zig").ScriptNum; +const script = @import("lib.zig"); +const ScriptNum = script.ScriptNum; +const asBool = script.asBool; +const asInt = script.asInt; const testing = std.testing; const native_endian = @import("builtin").target.cpu.arch.endian(); @@ -45,13 +48,7 @@ pub const Stack = struct { self.items.deinit(); } - /// Push an item onto the stack - /// - /// # Arguments - /// - `item`: Slice of bytes to be pushed onto the stack - /// - /// # Returns - /// - `StackError` if out of memory + /// Push an element onto the stack (allocate a copy of it) pub fn pushByteArray(self: *Stack, item: []const u8) StackError!void { // Create a copy of the input item const copy = self.allocator.dupe(u8, item) catch return StackError.OutOfMemory; @@ -64,24 +61,7 @@ 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 - /// - `item`: Slice of bytes to be pushed onto the stack - /// - /// # Returns - /// - `StackError` if out of memory + /// Push an element onto the stack(does not create copy of item) pub fn pushElement(self: *Stack, item: []u8) StackError!void { // Append the item directly to the stack self.items.append(item) catch { @@ -89,29 +69,65 @@ pub const Stack = struct { }; } - /// Pop an integer from the stack + /// Push a number onto the stack (allocate a copy of it) + 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 a ScriptNum 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 { + /// Suitable when you need to do some potentially overflowing operations on it. + /// Otherwise prefer popInt. + pub fn popScriptNum(self: *Stack) StackError!ScriptNum { const value = try self.pop(); defer self.allocator.free(value); + return ScriptNum.new(try asInt(value)); + } - if (value.len > 8) return StackError.InvalidValue; - - return std.mem.readVarInt(ScriptNum, value, native_endian); + /// Pop an integer from the stack + pub fn popInt(self: *Stack) !i32 { + const value = try self.pop(); + defer self.allocator.free(value); + return asInt(value); } /// Pop a boolean value from the stack - /// - /// # Returns - /// - `bool`: The popped boolean value - /// - `StackError` if the stack is empty pub fn popBool(self: *Stack) StackError!bool { const value = try self.pop(); defer self.allocator.free(value); - return if (value.len == 1 and value[0] != 0) true else false; + return asBool(value); } // Function to push a boolean value onto the stack @@ -119,7 +135,8 @@ pub const Stack = struct { if (value) { try self.pushByteArray(&[_]u8{1}); } else { - try self.pushByteArray(&[_]u8{0}); + const empty_slice: []u8 = &.{}; + self.items.append(empty_slice) catch return error.OutOfMemory; } } @@ -147,15 +164,23 @@ pub const Stack = struct { return self.items.items[self.items.items.len - 1 - index]; } - pub fn peekInt(self: *Stack, index: usize) StackError!i64 { + pub fn peekInt(self: *Stack, index: usize) StackError!i32 { + if (index >= self.items.items.len) { + return StackError.StackUnderflow; + } + + const bytes = self.items.items[self.items.items.len - 1 - index]; + + return asInt(bytes); + } + pub fn peekBool(self: *Stack, index: usize) StackError!bool { if (index >= self.items.items.len) { return StackError.StackUnderflow; } - const elem = self.items.items[self.items.items.len - 1 - index]; - if (elem.len > 8) return StackError.InvalidValue; + const bytes = self.items.items[self.items.items.len - 1 - index]; - return std.mem.readVarInt(i64, elem, native_endian); + return asBool(bytes); } /// Get the number of items in the stack @@ -302,11 +327,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());