Skip to content
Merged
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
847 changes: 553 additions & 294 deletions README.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ pub fn build(b: *std.Build) void {
comp.addLibraryPath(.{ .cwd_relative = "/usr/lib/x86_64-linux-gnu" });
comp.addLibraryPath(.{ .cwd_relative = "/usr/x86_64-linux-gnu/lib" });
}
// For macOS, let Zig's linkSystemLibrary find the library automatically
// (Homebrew libraries are in standard locations)
// For macOS, try to link LMDB - if cross-compiling to different arch, it will fail gracefully
// (Homebrew installs architecture-specific libraries, so cross-compilation may not work)
// We let the linker fail if the library architecture doesn't match
comp.linkSystemLibrary("lmdb");
}
}.add;
Expand Down
146 changes: 143 additions & 3 deletions src/api/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ pub const JsonRpcServer = struct {
// Handle method
if (std.mem.eql(u8, request.method, "eth_sendRawTransaction")) {
return try self.handleSendRawTransaction(&request);
} else if (std.mem.eql(u8, request.method, "eth_sendRawTransactionConditional")) {
return try self.handleSendRawTransactionConditional(&request);
} else if (std.mem.eql(u8, request.method, "eth_getTransactionReceipt")) {
return try self.handleGetTransactionReceipt(&request);
} else if (std.mem.eql(u8, request.method, "eth_blockNumber")) {
Expand Down Expand Up @@ -267,6 +269,136 @@ pub const JsonRpcServer = struct {
}
}

fn handleSendRawTransactionConditional(self: *JsonRpcServer, request: *const jsonrpc.JsonRpcRequest) ![]u8 {
self.metrics.incrementTransactionsReceived();

// Parse params
const params = request.params orelse {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Missing params");
};

const params_array = switch (params) {
.array => |arr| arr,
else => {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid params - expected array");
},
};

if (params_array.items.len < 2) {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Missing transaction data or options");
}

// Parse transaction hex
const first_param = params_array.items[0];
const tx_hex = switch (first_param) {
.string => |s| s,
else => {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction format");
},
};

// Parse conditional options
const second_param = params_array.items[1];
const options_json = switch (second_param) {
.object => |obj| std.json.Value{ .object = obj },
else => {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid options format - expected object");
},
};

// Parse conditional options
const conditional_options = core.conditional_tx.ConditionalOptions.fromJson(self.allocator, options_json) catch {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Failed to parse conditional options");
};

// Decode hex string (remove 0x prefix if present)
const hex_start: usize = if (std.mem.startsWith(u8, tx_hex, "0x")) 2 else 0;
const hex_data = tx_hex[hex_start..];

var tx_bytes = std.ArrayList(u8).init(self.allocator);
defer tx_bytes.deinit();

var i: usize = 0;
while (i < hex_data.len) : (i += 2) {
if (i + 1 >= hex_data.len) break;
const byte = try std.fmt.parseInt(u8, hex_data[i .. i + 2], 16);
try tx_bytes.append(byte);
}

const tx_bytes_slice = try tx_bytes.toOwnedSlice();
defer self.allocator.free(tx_bytes_slice);

// Check transaction type (EIP-2718)
if (tx_bytes_slice.len > 0 and tx_bytes_slice[0] == core.transaction.ExecuteTxType) {
// ExecuteTx transactions don't support conditional submission
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "ExecuteTx transactions do not support conditional submission");
}

// Parse legacy transaction
const tx = core.transaction.Transaction.fromRaw(self.allocator, tx_bytes_slice) catch {
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction encoding");
};
defer self.allocator.free(tx.data);

// Validate transaction first
const result = self.ingress_handler.acceptTransaction(tx) catch {
self.metrics.incrementTransactionsRejected();
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction processing failed");
};

if (result != .valid) {
self.metrics.incrementTransactionsRejected();
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction validation failed");
}

// Clone transaction data since acceptTransaction may have consumed it
const tx_data_clone = try self.allocator.dupe(u8, tx.data);
const tx_clone = core.transaction.Transaction{
.nonce = tx.nonce,
.gas_price = tx.gas_price,
.gas_limit = tx.gas_limit,
.to = tx.to,
.value = tx.value,
.data = tx_data_clone,
.v = tx.v,
.r = tx.r,
.s = tx.s,
};

// Insert transaction with conditional options into mempool
const inserted = self.ingress_handler.mempool.insertWithConditions(tx_clone, conditional_options) catch {
self.allocator.free(tx_data_clone);
self.metrics.incrementTransactionsRejected();
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Failed to insert conditional transaction");
};

if (!inserted) {
self.allocator.free(tx_data_clone);
self.metrics.incrementTransactionsRejected();
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction already in mempool");
}

self.metrics.incrementTransactionsAccepted();

// Return transaction hash
const tx_hash = try tx.hash(self.allocator);
const hash_bytes = core.types.hashToBytes(tx_hash);
var hex_buf: [66]u8 = undefined; // 0x + 64 hex chars
hex_buf[0] = '0';
hex_buf[1] = 'x';
var j: usize = 0;
while (j < 32) : (j += 1) {
const hex_digits = "0123456789abcdef";
hex_buf[2 + j * 2] = hex_digits[hash_bytes[j] >> 4];
hex_buf[2 + j * 2 + 1] = hex_digits[hash_bytes[j] & 0xf];
}
const hash_hex = try std.fmt.allocPrint(self.allocator, "{s}", .{&hex_buf});
defer self.allocator.free(hash_hex);

const result_value = std.json.Value{ .string = hash_hex };
return try jsonrpc.JsonRpcResponse.success(self.allocator, request.id, result_value);
}

fn handleGetTransactionReceipt(self: *JsonRpcServer, request: *const jsonrpc.JsonRpcRequest) ![]u8 {
// In production, fetch receipt from state manager
const result_value = std.json.Value{ .null = {} };
Expand Down Expand Up @@ -343,8 +475,10 @@ pub const JsonRpcServer = struct {
var witness_builder = core.witness_builder.WitnessBuilder.init(self.allocator);
defer witness_builder.deinit();

// Create execution engine with witness builder
var exec_engine = sequencer.execution_engine;
// Create execution engine with witness builder for witness generation
// Note: In op-node architecture, execution is delegated to L2 geth,
// but we still need local execution for witness generation (debug endpoint)
var exec_engine = @import("../sequencer/execution.zig").ExecutionEngine.init(self.allocator, sequencer.state_manager);
exec_engine.witness_builder = &witness_builder;

// Execute transaction (this will track state access)
Expand Down Expand Up @@ -445,8 +579,14 @@ pub const JsonRpcServer = struct {
var witness_builder = core.witness_builder.WitnessBuilder.init(self.allocator);
defer witness_builder.deinit();

// Create execution engine for witness generation
// Note: In op-node architecture, execution is delegated to L2 geth,
// but we still need local execution for witness generation (debug endpoint)
var exec_engine = @import("../sequencer/execution.zig").ExecutionEngine.init(self.allocator, sequencer.state_manager);
exec_engine.witness_builder = &witness_builder;

// Generate witness for the block
try witness_builder.generateBlockWitness(&block, &sequencer.execution_engine);
try witness_builder.generateBlockWitness(&block, &exec_engine);

// Build witness
_ = try witness_builder.buildWitness(sequencer.state_manager, null);
Expand Down
40 changes: 36 additions & 4 deletions src/config/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ pub const Config = struct {

// L1 Connection
l1_rpc_url: []const u8 = "http://localhost:8545",
l1_chain_id: u64 = 1,
l1_chain_id: u64 = 61971,

// L2 Connection
l2_rpc_url: []const u8 = "http://localhost:8545",
l2_engine_api_port: u16 = 8551,
l2_chain_id: u64 = 1337,
l2_rpc_url: []const u8 = "http://localhost:18545",
l2_engine_api_port: u16 = 18551,
l2_chain_id: u64 = 61972,
l2_jwt_secret: ?[32]u8 = null, // JWT secret for Engine API authentication (32 bytes)

// Sequencer
sequencer_private_key: ?[32]u8 = null,
Expand Down Expand Up @@ -51,15 +52,46 @@ pub const Config = struct {
config.l1_rpc_url = url;
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "L1_CHAIN_ID")) |chain_id_str| {
defer allocator.free(chain_id_str);
config.l1_chain_id = try std.fmt.parseInt(u64, chain_id_str, 10);
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "L2_RPC_URL")) |url| {
config.l2_rpc_url = url;
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "L2_CHAIN_ID")) |chain_id_str| {
defer allocator.free(chain_id_str);
config.l2_chain_id = try std.fmt.parseInt(u64, chain_id_str, 10);
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "L2_ENGINE_API_PORT")) |port_str| {
config.l2_engine_api_port = try std.fmt.parseInt(u16, port_str, 10);
allocator.free(port_str);
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "L2_JWT_SECRET")) |secret_hex| {
defer allocator.free(secret_hex);
// Parse hex secret (remove 0x prefix if present)
const hex_start: usize = if (std.mem.startsWith(u8, secret_hex, "0x")) 2 else 0;
const hex_data = secret_hex[hex_start..];

if (hex_data.len != 64) {
return error.InvalidJWTSecret;
}

var secret_bytes: [32]u8 = undefined;
var i: usize = 0;
while (i < 32) : (i += 1) {
const high = try std.fmt.parseInt(u8, hex_data[i * 2 .. i * 2 + 1], 16);
const low = try std.fmt.parseInt(u8, hex_data[i * 2 + 1 .. i * 2 + 2], 16);
secret_bytes[i] = (high << 4) | low;
}

config.l2_jwt_secret = secret_bytes;
} else |_| {}

if (std.process.getEnvVarOwned(allocator, "SEQUENCER_KEY")) |key_hex| {
defer allocator.free(key_hex);
// Parse hex key (remove 0x prefix if present)
Expand Down
120 changes: 120 additions & 0 deletions src/core/conditional_tx.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Conditional transaction options (EIP-7796)
// Supports conditional transaction submission with block number and timestamp constraints

const std = @import("std");
const types = @import("types.zig");

/// Conditional options for transaction submission
pub const ConditionalOptions = struct {
block_number_min: ?u64 = null,
block_number_max: ?u64 = null,
timestamp_min: ?u64 = null,
timestamp_max: ?u64 = null,
// known_accounts: ?std.json.Value = null, // Future: support account state checks

pub fn deinit(self: *ConditionalOptions) void {
_ = self;
// No cleanup needed for now
}

/// Check if conditions are satisfied given current block state
pub fn checkConditions(self: *const ConditionalOptions, current_block_number: u64, current_timestamp: u64) bool {
// Check block number constraints
if (self.block_number_min) |min| {
if (current_block_number < min) {
return false;
}
}
if (self.block_number_max) |max| {
if (current_block_number > max) {
return false;
}
}

// Check timestamp constraints
if (self.timestamp_min) |min| {
if (current_timestamp < min) {
return false;
}
}
if (self.timestamp_max) |max| {
if (current_timestamp > max) {
return false;
}
}

return true;
}

/// Parse conditional options from JSON-RPC params
pub fn fromJson(allocator: std.mem.Allocator, options_json: std.json.Value) !ConditionalOptions {
_ = allocator;
var options = ConditionalOptions{};

const options_obj = switch (options_json) {
.object => |obj| obj,
else => return error.InvalidOptionsFormat,
};

// Parse blockNumberMin
if (options_obj.get("blockNumberMin")) |value| {
const block_num_str = switch (value) {
.string => |s| s,
else => return error.InvalidBlockNumberFormat,
};
const hex_start: usize = if (std.mem.startsWith(u8, block_num_str, "0x")) 2 else 0;
options.block_number_min = try std.fmt.parseInt(u64, block_num_str[hex_start..], 16);
}

// Parse blockNumberMax
if (options_obj.get("blockNumberMax")) |value| {
const block_num_str = switch (value) {
.string => |s| s,
else => return error.InvalidBlockNumberFormat,
};
const hex_start: usize = if (std.mem.startsWith(u8, block_num_str, "0x")) 2 else 0;
options.block_number_max = try std.fmt.parseInt(u64, block_num_str[hex_start..], 16);
}

// Parse timestampMin
if (options_obj.get("timestampMin")) |value| {
const timestamp_val = switch (value) {
.string => |s| blk: {
const hex_start: usize = if (std.mem.startsWith(u8, s, "0x")) 2 else 0;
break :blk try std.fmt.parseInt(u64, s[hex_start..], 16);
},
.integer => |i| @as(u64, @intCast(i)),
else => return error.InvalidTimestampFormat,
};
options.timestamp_min = timestamp_val;
}

// Parse timestampMax
if (options_obj.get("timestampMax")) |value| {
const timestamp_val = switch (value) {
.string => |s| blk: {
const hex_start: usize = if (std.mem.startsWith(u8, s, "0x")) 2 else 0;
break :blk try std.fmt.parseInt(u64, s[hex_start..], 16);
},
.integer => |i| @as(u64, @intCast(i)),
else => return error.InvalidTimestampFormat,
};
options.timestamp_max = timestamp_val;
}

return options;
}
};

/// Conditional transaction entry (transaction + conditions)
pub const ConditionalTx = struct {
tx: transaction.Transaction,
conditions: ConditionalOptions,

pub fn deinit(self: *ConditionalTx, allocator: std.mem.Allocator) void {
self.tx.deinit(allocator);
self.conditions.deinit();
}
};

const transaction = @import("transaction.zig");
2 changes: 1 addition & 1 deletion src/core/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ pub const rlp = @import("rlp.zig");
pub const witness = @import("witness.zig");
pub const witness_builder = @import("witness_builder.zig");
pub const trie = @import("trie.zig");
pub const storage_trie = @import("storage_trie.zig");
pub const conditional_tx = @import("conditional_tx.zig");
Loading
Loading