Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Op conditions #163

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,12 @@ pie showData
| op16 | 0x60 | ✅ | The number 16 is pushed onto the stack. |
| opNop | 0x61 | ✅ | Does nothing. |
| opVer | 0x62 | ❌ | Transaction is invalid unless occurring in an unexecuted opIF branch |
| opIf | 0x63 | | If the top stack value is not False, the statements are executed. The top stack value is removed. |
| opNotIf | 0x64 | | If the top stack value is False, the statements are executed. The top stack value is removed. |
| opIf | 0x63 | | If the top stack value is not False, the statements are executed. The top stack value is removed. |
| opNotIf | 0x64 | | If the top stack value is False, the statements are executed. The top stack value is removed. |
| opVerIf | 0x65 | ❌ | Transaction is invalid even when occurring in an unexecuted opIF branch |
| opVerNotIf | 0x66 | ❌ | Transaction is invalid even when occurring in an unexecuted opIF branch |
| opElse | 0x67 | | If the preceding opIF or opNOTIF or opELSE was not executed then these statements are and if the preceding opIF or opNOTIF or opELSE was executed then these statements are not. |
| opEndIf | 0x68 | | Ends an if/else block. |
| opElse | 0x67 | | If the preceding opIF or opNOTIF or opELSE was not executed then these statements are and if the preceding opIF or opNOTIF or opELSE was executed then these statements are not. |
| opEndIf | 0x68 | | Ends an if/else block. |
| opVerify | 0x69 | ✅ | Marks transaction as invalid if top stack value is not true. |
| opReturn | 0x6a | ✅ | Marks transaction as invalid. |
| opToAltStack | 0x6b | ✅ | Puts the input onto the top of the alt stack. Removes it from the main stack. |
Expand Down
95 changes: 95 additions & 0 deletions src/script/cond_stack.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const testing = std.testing;

/// Errors that can occur during stack operations
pub const ConditionalStackError = error{
StackUnderflow,
OutOfMemory,
};

pub const ConditionalValues = enum(u8) {
False = 0,
True = 1,
Skip = 2,
};

/// ConditionalStack for script execution
///
pub const ConditionalStack = struct {
stack: std.ArrayList(ConditionalValues),
/// Memory allocator used for managing item storage
allocator: Allocator,

/// Initialize a new ConditionalStack
pub fn init(allocator: Allocator) ConditionalStack {
return .{
.stack = std.ArrayList(ConditionalValues).init(allocator),
.allocator = allocator,
};
}

/// Deallocate all resources used by the ConditionalStack
pub fn deinit(self: *ConditionalStack) void {
self.stack.deinit();
}

/// Push an item onto the stack
pub fn push(self: *ConditionalStack, item: ConditionalValues) ConditionalStackError!void {
self.stack.append(item) catch {
return ConditionalStackError.OutOfMemory;
};
}

/// Delete an item from the stack
pub fn delete(self: *ConditionalStack) ConditionalStackError!void {
if (self.stack.items.len == 0) {
return ConditionalStackError.StackUnderflow;
}
self.stack.items.len -= 1;
}

pub fn branchExecuting(self: ConditionalStack) bool {
if (self.stack.items.len == 0) {
return true;
}
return self.stack.items[self.stack.items.len-1] == ConditionalValues.True;
}

/// Swap the top value of the conditional stack between True and False.
/// If the top value is Skip, it remains unchanged.
pub fn swap(self: *ConditionalStack) ConditionalStackError!void {
if (self.stack.items.len == 0) {
return ConditionalStackError.StackUnderflow;
}
const index = self.stack.items.len - 1;

switch (self.stack.items[index]) {
ConditionalValues.False => self.stack.items[index] = ConditionalValues.True,
ConditionalValues.True => self.stack.items[index] = ConditionalValues.False,
ConditionalValues.Skip => {},
}
}

/// Get the number of items in the stack
pub fn len(self: ConditionalStack) usize {
return self.stack.items.len;
}
};

test "ConditionalStack basic operations" {
const allocator = testing.allocator;
var cond_stack = ConditionalStack.init(allocator);
defer cond_stack.deinit();

// Test branch executing
try testing.expectEqual(true, cond_stack.branchExecuting());

// Test push and len
try cond_stack.push(ConditionalValues.True);
try testing.expectEqual(1, cond_stack.len());

// Test delete
try cond_stack.delete();
try testing.expectEqual(0, cond_stack.len());
}
187 changes: 186 additions & 1 deletion src/script/engine.zig
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Stack = @import("stack.zig").Stack;
const ConditionalStack = @import("cond_stack.zig").ConditionalStack;
const ConditionalValues = @import("cond_stack.zig").ConditionalValues;
const Script = @import("lib.zig").Script;
const asBool = @import("lib.zig").asBool;
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 pushDataLen = @import("opcodes/constant.zig").pushDataLen;
const skipPushData = @import("opcodes/constant.zig").skipPushData;
const EngineError = @import("lib.zig").EngineError;
const ScriptBuilder = @import("scriptBuilder.zig").ScriptBuilder;
const sha1 = std.crypto.hash.Sha1;
Expand All @@ -21,6 +25,8 @@ pub const Engine = struct {
stack: Stack,
/// Alternative stack for some operations
alt_stack: Stack,
/// Conditional stack stack for some operations
cond_stack: ConditionalStack,
/// Program counter (current position in the script)
pc: usize,
/// Execution flags
Expand All @@ -42,6 +48,7 @@ pub const Engine = struct {
.script = script,
.stack = Stack.init(allocator),
.alt_stack = Stack.init(allocator),
.cond_stack = ConditionalStack.init(allocator),
.pc = 0,
.flags = flags,
.allocator = allocator,
Expand All @@ -52,6 +59,7 @@ pub const Engine = struct {
pub fn deinit(self: *Engine) void {
self.stack.deinit();
self.alt_stack.deinit();
self.cond_stack.deinit();
}

/// Log debug information
Expand All @@ -76,8 +84,20 @@ pub const Engine = struct {
self.log("\nPC: {d}, Opcode: 0x{x:0>2}\n", .{ self.pc, opcodeByte });
self.logStack();

self.pc += 1;
const opcode: Opcode = try Opcode.fromByte(opcodeByte);

if (!(self.cond_stack.branchExecuting()) and !(opcode.isConditional())) {
if (isUnnamedPushNDataOpcode(opcode)) |length| {
self.pc += 1 + length;
} else if (opcode.isPushData()) {
try skipPushData(self, opcode);
} else {
self.pc += 1;
}
continue;
}

self.pc += 1;
try self.executeOpcode(opcode);
}

Expand Down Expand Up @@ -116,6 +136,10 @@ pub const Engine = struct {
.OP_1, .OP_2, .OP_3, .OP_4, .OP_5, .OP_6, .OP_7, .OP_8, .OP_9, .OP_10, .OP_11, .OP_12, .OP_13, .OP_14, .OP_15, .OP_16 => try self.opN(opcode),
Opcode.OP_NOP => try self.opNop(),
Opcode.OP_VERIFY => try self.opVerify(),
Opcode.OP_IF => try self.opIf(),
Opcode.OP_NOTIF => try self.opNotIf(),
Opcode.OP_ELSE => try self.opElse(),
Opcode.OP_ENDIF => try self.opEndIf(),
Opcode.OP_RETURN => try self.opReturn(),
Opcode.OP_TOALTSTACK => try self.opToAltStack(),
Opcode.OP_FROMALTSTACK => try self.opFromAltStack(),
Expand Down Expand Up @@ -549,8 +573,169 @@ pub const Engine = struct {
sha1.hash(data, &hash, .{});
try self.stack.pushByteArray(&hash);
}

/// OP_IF: If the top stack value is not False, the statements are executed. The top stack value is removed.
fn opIf(self: *Engine) !void {
var cond: ConditionalValues = ConditionalValues.False;
if (self.cond_stack.branchExecuting()) {
const ok = try self.stack.popBool();
if (ok) {
cond = ConditionalValues.True;
}
} else {
cond = ConditionalValues.Skip;
}
try self.cond_stack.push(cond);
}

/// OP_NOTIF: If the top stack value is False, the statements are executed. The top stack value is removed.
fn opNotIf(self: *Engine) !void {
var cond: ConditionalValues = ConditionalValues.False;
if (self.cond_stack.branchExecuting()) {
const ok = try self.stack.popBool();
if (!ok) {
cond = ConditionalValues.True;
}
} else {
cond = ConditionalValues.Skip;
}
try self.cond_stack.push(cond);
}

/// OP_ELSE: If the preceding opIF or opNOTIF or opELSE was not executed then these statements are
/// and if the preceding opIF or opNOTIF or opELSE was executed then these statements are not.
fn opElse(self: *Engine) !void {
try self.cond_stack.swap();
}

/// OP_ENFIF: Ends an if/else block.
fn opEndIf(self: *Engine) !void {
try self.cond_stack.delete();
}
};

test "Script execution - OP_IF false" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_0.toBytes(),
Opcode.OP_IF.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(0, engine.stack.len());
}

test "Script execution - OP_IF true" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_1.toBytes(),
Opcode.OP_IF.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(1, engine.stack.len());
try std.testing.expectEqual(1, try engine.stack.peekInt(0));
}

test "Script execution - OP_NOTIF false" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_0.toBytes(),
Opcode.OP_NOTIF.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(1, engine.stack.len());
try std.testing.expectEqual(1, try engine.stack.peekInt(0));
}

test "Script execution - OP_NOTIF true" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_1.toBytes(),
Opcode.OP_NOTIF.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(0, engine.stack.len());
}

test "Script execution - OP_IF OP_ELSE false" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_0.toBytes(),
Opcode.OP_IF.toBytes(),
Opcode.OP_0.toBytes(),
Opcode.OP_ELSE.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(1, engine.stack.len());
try std.testing.expectEqual(1, try engine.stack.peekInt(0));
}

test "Script execution - OP_IF OP_ELSE true" {
const allocator = std.testing.allocator;

const script_bytes = [_]u8{
Opcode.OP_1.toBytes(),
Opcode.OP_IF.toBytes(),
Opcode.OP_0.toBytes(),
Opcode.OP_ELSE.toBytes(),
Opcode.OP_1.toBytes(),
Opcode.OP_ENDIF.toBytes(),
};
const script = Script.init(&script_bytes);

var engine = Engine.init(allocator, script, .{});
defer engine.deinit();

try engine.execute();

try std.testing.expectEqual(1, engine.stack.len());
try std.testing.expectEqual(0, try engine.stack.peekInt(0));
}

test "Script execution - OP_HASH256" {
const allocator = std.testing.allocator;

Expand Down
Loading
Loading