Skip to content

Commit 74780bf

Browse files
authored
Merge pull request #7 from blockblaz/feature
fix: Corrected sequencer to work like op-node
2 parents 4bfa86e + 8fe23c3 commit 74780bf

28 files changed

+2662
-1071
lines changed

README.md

Lines changed: 553 additions & 294 deletions
Large diffs are not rendered by default.

build.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ pub fn build(b: *std.Build) void {
2222
comp.addLibraryPath(.{ .cwd_relative = "/usr/lib/x86_64-linux-gnu" });
2323
comp.addLibraryPath(.{ .cwd_relative = "/usr/x86_64-linux-gnu/lib" });
2424
}
25-
// For macOS, let Zig's linkSystemLibrary find the library automatically
26-
// (Homebrew libraries are in standard locations)
25+
// For macOS, try to link LMDB - if cross-compiling to different arch, it will fail gracefully
26+
// (Homebrew installs architecture-specific libraries, so cross-compilation may not work)
27+
// We let the linker fail if the library architecture doesn't match
2728
comp.linkSystemLibrary("lmdb");
2829
}
2930
}.add;

src/api/server.zig

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ pub const JsonRpcServer = struct {
111111
// Handle method
112112
if (std.mem.eql(u8, request.method, "eth_sendRawTransaction")) {
113113
return try self.handleSendRawTransaction(&request);
114+
} else if (std.mem.eql(u8, request.method, "eth_sendRawTransactionConditional")) {
115+
return try self.handleSendRawTransactionConditional(&request);
114116
} else if (std.mem.eql(u8, request.method, "eth_getTransactionReceipt")) {
115117
return try self.handleGetTransactionReceipt(&request);
116118
} else if (std.mem.eql(u8, request.method, "eth_blockNumber")) {
@@ -267,6 +269,136 @@ pub const JsonRpcServer = struct {
267269
}
268270
}
269271

272+
fn handleSendRawTransactionConditional(self: *JsonRpcServer, request: *const jsonrpc.JsonRpcRequest) ![]u8 {
273+
self.metrics.incrementTransactionsReceived();
274+
275+
// Parse params
276+
const params = request.params orelse {
277+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Missing params");
278+
};
279+
280+
const params_array = switch (params) {
281+
.array => |arr| arr,
282+
else => {
283+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid params - expected array");
284+
},
285+
};
286+
287+
if (params_array.items.len < 2) {
288+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Missing transaction data or options");
289+
}
290+
291+
// Parse transaction hex
292+
const first_param = params_array.items[0];
293+
const tx_hex = switch (first_param) {
294+
.string => |s| s,
295+
else => {
296+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction format");
297+
},
298+
};
299+
300+
// Parse conditional options
301+
const second_param = params_array.items[1];
302+
const options_json = switch (second_param) {
303+
.object => |obj| std.json.Value{ .object = obj },
304+
else => {
305+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid options format - expected object");
306+
},
307+
};
308+
309+
// Parse conditional options
310+
const conditional_options = core.conditional_tx.ConditionalOptions.fromJson(self.allocator, options_json) catch {
311+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Failed to parse conditional options");
312+
};
313+
314+
// Decode hex string (remove 0x prefix if present)
315+
const hex_start: usize = if (std.mem.startsWith(u8, tx_hex, "0x")) 2 else 0;
316+
const hex_data = tx_hex[hex_start..];
317+
318+
var tx_bytes = std.ArrayList(u8).init(self.allocator);
319+
defer tx_bytes.deinit();
320+
321+
var i: usize = 0;
322+
while (i < hex_data.len) : (i += 2) {
323+
if (i + 1 >= hex_data.len) break;
324+
const byte = try std.fmt.parseInt(u8, hex_data[i .. i + 2], 16);
325+
try tx_bytes.append(byte);
326+
}
327+
328+
const tx_bytes_slice = try tx_bytes.toOwnedSlice();
329+
defer self.allocator.free(tx_bytes_slice);
330+
331+
// Check transaction type (EIP-2718)
332+
if (tx_bytes_slice.len > 0 and tx_bytes_slice[0] == core.transaction.ExecuteTxType) {
333+
// ExecuteTx transactions don't support conditional submission
334+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "ExecuteTx transactions do not support conditional submission");
335+
}
336+
337+
// Parse legacy transaction
338+
const tx = core.transaction.Transaction.fromRaw(self.allocator, tx_bytes_slice) catch {
339+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction encoding");
340+
};
341+
defer self.allocator.free(tx.data);
342+
343+
// Validate transaction first
344+
const result = self.ingress_handler.acceptTransaction(tx) catch {
345+
self.metrics.incrementTransactionsRejected();
346+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction processing failed");
347+
};
348+
349+
if (result != .valid) {
350+
self.metrics.incrementTransactionsRejected();
351+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction validation failed");
352+
}
353+
354+
// Clone transaction data since acceptTransaction may have consumed it
355+
const tx_data_clone = try self.allocator.dupe(u8, tx.data);
356+
const tx_clone = core.transaction.Transaction{
357+
.nonce = tx.nonce,
358+
.gas_price = tx.gas_price,
359+
.gas_limit = tx.gas_limit,
360+
.to = tx.to,
361+
.value = tx.value,
362+
.data = tx_data_clone,
363+
.v = tx.v,
364+
.r = tx.r,
365+
.s = tx.s,
366+
};
367+
368+
// Insert transaction with conditional options into mempool
369+
const inserted = self.ingress_handler.mempool.insertWithConditions(tx_clone, conditional_options) catch {
370+
self.allocator.free(tx_data_clone);
371+
self.metrics.incrementTransactionsRejected();
372+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Failed to insert conditional transaction");
373+
};
374+
375+
if (!inserted) {
376+
self.allocator.free(tx_data_clone);
377+
self.metrics.incrementTransactionsRejected();
378+
return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction already in mempool");
379+
}
380+
381+
self.metrics.incrementTransactionsAccepted();
382+
383+
// Return transaction hash
384+
const tx_hash = try tx.hash(self.allocator);
385+
const hash_bytes = core.types.hashToBytes(tx_hash);
386+
var hex_buf: [66]u8 = undefined; // 0x + 64 hex chars
387+
hex_buf[0] = '0';
388+
hex_buf[1] = 'x';
389+
var j: usize = 0;
390+
while (j < 32) : (j += 1) {
391+
const hex_digits = "0123456789abcdef";
392+
hex_buf[2 + j * 2] = hex_digits[hash_bytes[j] >> 4];
393+
hex_buf[2 + j * 2 + 1] = hex_digits[hash_bytes[j] & 0xf];
394+
}
395+
const hash_hex = try std.fmt.allocPrint(self.allocator, "{s}", .{&hex_buf});
396+
defer self.allocator.free(hash_hex);
397+
398+
const result_value = std.json.Value{ .string = hash_hex };
399+
return try jsonrpc.JsonRpcResponse.success(self.allocator, request.id, result_value);
400+
}
401+
270402
fn handleGetTransactionReceipt(self: *JsonRpcServer, request: *const jsonrpc.JsonRpcRequest) ![]u8 {
271403
// In production, fetch receipt from state manager
272404
const result_value = std.json.Value{ .null = {} };
@@ -343,8 +475,10 @@ pub const JsonRpcServer = struct {
343475
var witness_builder = core.witness_builder.WitnessBuilder.init(self.allocator);
344476
defer witness_builder.deinit();
345477

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

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

582+
// Create execution engine for witness generation
583+
// Note: In op-node architecture, execution is delegated to L2 geth,
584+
// but we still need local execution for witness generation (debug endpoint)
585+
var exec_engine = @import("../sequencer/execution.zig").ExecutionEngine.init(self.allocator, sequencer.state_manager);
586+
exec_engine.witness_builder = &witness_builder;
587+
448588
// Generate witness for the block
449-
try witness_builder.generateBlockWitness(&block, &sequencer.execution_engine);
589+
try witness_builder.generateBlockWitness(&block, &exec_engine);
450590

451591
// Build witness
452592
_ = try witness_builder.buildWitness(sequencer.state_manager, null);

src/config/config.zig

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ pub const Config = struct {
77

88
// L1 Connection
99
l1_rpc_url: []const u8 = "http://localhost:8545",
10-
l1_chain_id: u64 = 1,
10+
l1_chain_id: u64 = 61971,
1111

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

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

55+
if (std.process.getEnvVarOwned(allocator, "L1_CHAIN_ID")) |chain_id_str| {
56+
defer allocator.free(chain_id_str);
57+
config.l1_chain_id = try std.fmt.parseInt(u64, chain_id_str, 10);
58+
} else |_| {}
59+
5460
if (std.process.getEnvVarOwned(allocator, "L2_RPC_URL")) |url| {
5561
config.l2_rpc_url = url;
5662
} else |_| {}
5763

64+
if (std.process.getEnvVarOwned(allocator, "L2_CHAIN_ID")) |chain_id_str| {
65+
defer allocator.free(chain_id_str);
66+
config.l2_chain_id = try std.fmt.parseInt(u64, chain_id_str, 10);
67+
} else |_| {}
68+
5869
if (std.process.getEnvVarOwned(allocator, "L2_ENGINE_API_PORT")) |port_str| {
5970
config.l2_engine_api_port = try std.fmt.parseInt(u16, port_str, 10);
6071
allocator.free(port_str);
6172
} else |_| {}
6273

74+
if (std.process.getEnvVarOwned(allocator, "L2_JWT_SECRET")) |secret_hex| {
75+
defer allocator.free(secret_hex);
76+
// Parse hex secret (remove 0x prefix if present)
77+
const hex_start: usize = if (std.mem.startsWith(u8, secret_hex, "0x")) 2 else 0;
78+
const hex_data = secret_hex[hex_start..];
79+
80+
if (hex_data.len != 64) {
81+
return error.InvalidJWTSecret;
82+
}
83+
84+
var secret_bytes: [32]u8 = undefined;
85+
var i: usize = 0;
86+
while (i < 32) : (i += 1) {
87+
const high = try std.fmt.parseInt(u8, hex_data[i * 2 .. i * 2 + 1], 16);
88+
const low = try std.fmt.parseInt(u8, hex_data[i * 2 + 1 .. i * 2 + 2], 16);
89+
secret_bytes[i] = (high << 4) | low;
90+
}
91+
92+
config.l2_jwt_secret = secret_bytes;
93+
} else |_| {}
94+
6395
if (std.process.getEnvVarOwned(allocator, "SEQUENCER_KEY")) |key_hex| {
6496
defer allocator.free(key_hex);
6597
// Parse hex key (remove 0x prefix if present)

src/core/conditional_tx.zig

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Conditional transaction options (EIP-7796)
2+
// Supports conditional transaction submission with block number and timestamp constraints
3+
4+
const std = @import("std");
5+
const types = @import("types.zig");
6+
7+
/// Conditional options for transaction submission
8+
pub const ConditionalOptions = struct {
9+
block_number_min: ?u64 = null,
10+
block_number_max: ?u64 = null,
11+
timestamp_min: ?u64 = null,
12+
timestamp_max: ?u64 = null,
13+
// known_accounts: ?std.json.Value = null, // Future: support account state checks
14+
15+
pub fn deinit(self: *ConditionalOptions) void {
16+
_ = self;
17+
// No cleanup needed for now
18+
}
19+
20+
/// Check if conditions are satisfied given current block state
21+
pub fn checkConditions(self: *const ConditionalOptions, current_block_number: u64, current_timestamp: u64) bool {
22+
// Check block number constraints
23+
if (self.block_number_min) |min| {
24+
if (current_block_number < min) {
25+
return false;
26+
}
27+
}
28+
if (self.block_number_max) |max| {
29+
if (current_block_number > max) {
30+
return false;
31+
}
32+
}
33+
34+
// Check timestamp constraints
35+
if (self.timestamp_min) |min| {
36+
if (current_timestamp < min) {
37+
return false;
38+
}
39+
}
40+
if (self.timestamp_max) |max| {
41+
if (current_timestamp > max) {
42+
return false;
43+
}
44+
}
45+
46+
return true;
47+
}
48+
49+
/// Parse conditional options from JSON-RPC params
50+
pub fn fromJson(allocator: std.mem.Allocator, options_json: std.json.Value) !ConditionalOptions {
51+
_ = allocator;
52+
var options = ConditionalOptions{};
53+
54+
const options_obj = switch (options_json) {
55+
.object => |obj| obj,
56+
else => return error.InvalidOptionsFormat,
57+
};
58+
59+
// Parse blockNumberMin
60+
if (options_obj.get("blockNumberMin")) |value| {
61+
const block_num_str = switch (value) {
62+
.string => |s| s,
63+
else => return error.InvalidBlockNumberFormat,
64+
};
65+
const hex_start: usize = if (std.mem.startsWith(u8, block_num_str, "0x")) 2 else 0;
66+
options.block_number_min = try std.fmt.parseInt(u64, block_num_str[hex_start..], 16);
67+
}
68+
69+
// Parse blockNumberMax
70+
if (options_obj.get("blockNumberMax")) |value| {
71+
const block_num_str = switch (value) {
72+
.string => |s| s,
73+
else => return error.InvalidBlockNumberFormat,
74+
};
75+
const hex_start: usize = if (std.mem.startsWith(u8, block_num_str, "0x")) 2 else 0;
76+
options.block_number_max = try std.fmt.parseInt(u64, block_num_str[hex_start..], 16);
77+
}
78+
79+
// Parse timestampMin
80+
if (options_obj.get("timestampMin")) |value| {
81+
const timestamp_val = switch (value) {
82+
.string => |s| blk: {
83+
const hex_start: usize = if (std.mem.startsWith(u8, s, "0x")) 2 else 0;
84+
break :blk try std.fmt.parseInt(u64, s[hex_start..], 16);
85+
},
86+
.integer => |i| @as(u64, @intCast(i)),
87+
else => return error.InvalidTimestampFormat,
88+
};
89+
options.timestamp_min = timestamp_val;
90+
}
91+
92+
// Parse timestampMax
93+
if (options_obj.get("timestampMax")) |value| {
94+
const timestamp_val = switch (value) {
95+
.string => |s| blk: {
96+
const hex_start: usize = if (std.mem.startsWith(u8, s, "0x")) 2 else 0;
97+
break :blk try std.fmt.parseInt(u64, s[hex_start..], 16);
98+
},
99+
.integer => |i| @as(u64, @intCast(i)),
100+
else => return error.InvalidTimestampFormat,
101+
};
102+
options.timestamp_max = timestamp_val;
103+
}
104+
105+
return options;
106+
}
107+
};
108+
109+
/// Conditional transaction entry (transaction + conditions)
110+
pub const ConditionalTx = struct {
111+
tx: transaction.Transaction,
112+
conditions: ConditionalOptions,
113+
114+
pub fn deinit(self: *ConditionalTx, allocator: std.mem.Allocator) void {
115+
self.tx.deinit(allocator);
116+
self.conditions.deinit();
117+
}
118+
};
119+
120+
const transaction = @import("transaction.zig");

src/core/root.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ pub const rlp = @import("rlp.zig");
1212
pub const witness = @import("witness.zig");
1313
pub const witness_builder = @import("witness_builder.zig");
1414
pub const trie = @import("trie.zig");
15-
pub const storage_trie = @import("storage_trie.zig");
15+
pub const conditional_tx = @import("conditional_tx.zig");

0 commit comments

Comments
 (0)