diff --git a/src/blockchain/blockchain.zig b/src/blockchain/blockchain.zig index ca8583a..be85ba7 100644 --- a/src/blockchain/blockchain.zig +++ b/src/blockchain/blockchain.zig @@ -29,14 +29,15 @@ const Log = types.Log; const TxSigner = signer.TxSigner; const VM = vm.VM; const Keccak256 = std.crypto.hash.sha3.Keccak256; +pub const Fork = @import("fork.zig"); pub const Blockchain = struct { allocator: Allocator, chain_id: config.ChainId, state: *StateDB, prev_block: BlockHeader, - last_256_blocks_hashes: [256]Hash32, // ordered in asc order tx_signer: TxSigner, + fork: *Fork, // init initializes a blockchain. // The caller **does not** transfer ownership of prev_block. @@ -45,14 +46,14 @@ pub const Blockchain = struct { chain_id: config.ChainId, state: *StateDB, prev_block: BlockHeader, - last_256_blocks_hashes: [256]Hash32, + fork: *Fork, ) !Blockchain { return .{ .allocator = allocator, .chain_id = chain_id, .state = state, .prev_block = try prev_block.clone(allocator), - .last_256_blocks_hashes = last_256_blocks_hashes, + .fork = fork, .tx_signer = try signer.TxSigner.init(@intFromEnum(chain_id)), }; } @@ -66,6 +67,9 @@ pub const Blockchain = struct { defer arena.deinit(); const allocator = arena.allocator(); + // Add the current block to the last 256 block hashes. + try self.fork.update_parent_block_hash(block.header.block_number - 1, block.header.parent_hash); + // Execute block. var result = try applyBody(allocator, self, self.state, block, self.tx_signer); @@ -85,12 +89,6 @@ pub const Blockchain = struct { if (!std.mem.eql(u8, &result.withdrawals_root, &block.header.withdrawals_root)) return error.InvalidWithdrawalsRoot; - // Add the current block to the last 256 block hashes. - // TODO: this can be done more efficiently with some ring buffer to avoid copying the slice - // to make room for the new block hash. - std.mem.copyForwards(Hash32, &self.last_256_blocks_hashes, self.last_256_blocks_hashes[1..255]); - self.last_256_blocks_hashes[255] = try common.encodeToRLPAndHash(BlockHeader, allocator, block.header, null); - // Note that we free and clone with the Blockchain allocator, and not the arena allocator. // This is required since Blockchain field lifetimes are longer than the block execution processing. self.prev_block.deinit(self.allocator); @@ -164,8 +162,8 @@ pub const Blockchain = struct { const tx_info = try checkTransaction(allocator, tx, block.header.base_fee_per_gas, gas_available, tx_signer); const env: Environment = .{ + .fork = chain.fork, .origin = tx_info.sender_address, - .block_hashes = chain.last_256_blocks_hashes, .coinbase = block.header.fee_recipient, .number = block.header.block_number, .gas_limit = block.header.gas_limit, diff --git a/src/blockchain/fork.zig b/src/blockchain/fork.zig new file mode 100644 index 0000000..af91e13 --- /dev/null +++ b/src/blockchain/fork.zig @@ -0,0 +1,28 @@ +const lib = @import("../lib.zig"); +const Hash32 = lib.types.Hash32; +const Fork = @This(); +pub const frontier = @import("./forks/frontier.zig"); +pub const prague = @import("./forks/prague.zig"); + +vtable: *const VTable, + +pub const VTable = struct { + update_parent_block_hash: *const fn (self: *Fork, num: u64, hash: Hash32) anyerror!void, + get_parent_block_hash: *const fn (self: *Fork, index: u64) anyerror!Hash32, + deinit: *const fn (self: *Fork) void, +}; + +// Used to update the parent hash at the end of a block execution +pub fn update_parent_block_hash(self: *Fork, num: u64, hash: Hash32) !void { + return self.vtable.update_parent_block_hash(self, num, hash); +} + +// Used to get the block hash of a parent when implementing the +// BLOCKHASH instruction. +pub fn get_parent_block_hash(self: *Fork, index: u64) !Hash32 { + return self.vtable.get_parent_block_hash(self, index); +} + +pub fn deinit(self: *Fork) void { + self.vtable.deinit(self); +} diff --git a/src/blockchain/forks/frontier.zig b/src/blockchain/forks/frontier.zig new file mode 100644 index 0000000..2ee0524 --- /dev/null +++ b/src/blockchain/forks/frontier.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const Fork = @import("../fork.zig"); +const lib = @import("../../lib.zig"); +const Hash32 = lib.types.Hash32; + +const base_fork_vtable = Fork.VTable{ + .update_parent_block_hash = update_parent_block_hash, + .get_parent_block_hash = get_parent_block_hash, + .deinit = deinit, +}; + +const FrontierFork = struct { + const Self = @This(); + + fork: Fork = .{ + .vtable = &base_fork_vtable, + }, + + allocator: std.mem.Allocator, + next_block_hash_index: u64 = 0, // index of the next block hash to be written + block_hashes: [256]Hash32 = [_]Hash32{[_]u8{0} ** 32} ** 256, + + fn init(self: *Self) void { + self.next_block_hash_index = 0; + self.fork.vtable = &base_fork_vtable; + } +}; + +fn update_parent_block_hash(self: *Fork, block_num: u64, hash: Hash32) anyerror!void { + var base_fork: *FrontierFork = @fieldParentPtr("fork", self); + if (block_num != base_fork.next_block_hash_index) { + return error.NonSequentialParentUpdate; + } + + base_fork.block_hashes[base_fork.next_block_hash_index % base_fork.block_hashes.len] = hash; + base_fork.next_block_hash_index += 1; +} + +fn get_parent_block_hash(self: *Fork, block_num: u64) !Hash32 { + const base_fork: *FrontierFork = @fieldParentPtr("fork", self); + if (block_num > base_fork.next_block_hash_index or block_num + base_fork.block_hashes.len < base_fork.next_block_hash_index) { + return std.mem.zeroes(Hash32); + } + + return base_fork.block_hashes[block_num % base_fork.block_hashes.len]; +} + +pub fn newFrontierFork(allocator: std.mem.Allocator) !*Fork { + var base_fork = try allocator.create(FrontierFork); + base_fork.init(); + base_fork.allocator = allocator; + return &base_fork.fork; +} + +fn deinit(self: *Fork) void { + var base_fork: *FrontierFork = @fieldParentPtr("fork", self); + base_fork.allocator.destroy(base_fork); +} diff --git a/src/blockchain/forks/prague.zig b/src/blockchain/forks/prague.zig new file mode 100644 index 0000000..612d1b5 --- /dev/null +++ b/src/blockchain/forks/prague.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const Fork = @import("../fork.zig"); +const lib = @import("../../lib.zig"); +const Hash32 = lib.types.Hash32; +const StateDB = lib.state.StateDB; +const Address = lib.types.Address; + +const system_addr: Address = [_]u8{0xff} ** 19 ++ [_]u8{0xfe}; +const history_size: u64 = 8192; + +const vtable = Fork.VTable{ + .update_parent_block_hash = update_parent_block_hash, + .get_parent_block_hash = get_parent_block_hash, + .deinit = deinit, +}; + +const PragueFork = struct { + fork: Fork = Fork{ + .vtable = &vtable, + }, + + state_db: *StateDB, + allocator: std.mem.Allocator, +}; + +fn update_parent_block_hash(self: *Fork, num: u64, hash: Hash32) anyerror!void { + const prague_fork: *PragueFork = @fieldParentPtr("fork", self); + const slot: u256 = @intCast(num % history_size); + try prague_fork.state_db.setStorage(system_addr, slot, hash); +} + +fn get_parent_block_hash(self: *Fork, index: u64) !Hash32 { + const prague_fork: *PragueFork = @fieldParentPtr("fork", self); + const slot: u256 = @intCast(index % history_size); + return prague_fork.state_db.getStorage(system_addr, slot); +} + +// This method takes a parent fork and activate all the +// Prague-specific methods, superseding the previous fork. +pub fn enablePrague(state_db: *StateDB, _: ?*Fork, allocator: std.mem.Allocator) !*Fork { + var prague_fork = try allocator.create(PragueFork); + prague_fork.allocator = allocator; + prague_fork.state_db = state_db; + prague_fork.fork = Fork{ .vtable = &vtable }; + return &prague_fork.fork; +} + +fn deinit(self: *Fork) void { + const prague_fork: *PragueFork = @fieldParentPtr("fork", self); + prague_fork.allocator.destroy(prague_fork); +} + +// a helper function to deploy the 2935 contract on a testnet. +pub fn deployContract(self: *Fork) !void { + const prague_fork: *PragueFork = @fieldParentPtr("fork", self); + try prague_fork.state_db.db.put(system_addr, try lib.state.AccountState.init(prague_fork.allocator, system_addr, 1, 0, &[0]u8{})); +} diff --git a/src/blockchain/types.zig b/src/blockchain/types.zig index 452e576..8c24aa1 100644 --- a/src/blockchain/types.zig +++ b/src/blockchain/types.zig @@ -1,6 +1,7 @@ const types = @import("../types/types.zig"); const config = @import("../config/config.zig"); const common = @import("../common/common.zig"); +const Fork = @import("fork.zig"); const StateDB = @import("../state/state.zig").StateDB; const Address = types.Address; const Hash32 = types.Hash32; @@ -10,7 +11,7 @@ const AddresssKey = common.AddressKey; const AddressKeySet = common.AddressKeySet; pub const Environment = struct { - block_hashes: [256]Hash32, + fork: *Fork, origin: Address, coinbase: Address, number: u64, diff --git a/src/blockchain/vm.zig b/src/blockchain/vm.zig index f3e4a63..8f217c8 100644 --- a/src/blockchain/vm.zig +++ b/src/blockchain/vm.zig @@ -151,11 +151,7 @@ const EVMOneHost = struct { const vm: *VM = @as(*VM, @alignCast(@ptrCast(ctx.?))); const idx = vm.env.number - @as(u64, @intCast(block_number)); - if (idx < 0 or idx >= vm.env.block_hashes.len) { - return std.mem.zeroes(evmc.evmc_bytes32); - } - - return .{ .bytes = vm.env.block_hashes[idx] }; + return .{ .bytes = vm.env.fork.get_parent_block_hash(idx) catch @panic("unhandled error getting parent hash") }; } fn account_exists(ctx: ?*evmc.struct_evmc_host_context, addr: [*c]const evmc.evmc_address) callconv(.C) bool { diff --git a/src/engine_api/engine_api.zig b/src/engine_api/engine_api.zig index b786f7a..ef45d21 100644 --- a/src/engine_api/engine_api.zig +++ b/src/engine_api/engine_api.zig @@ -6,6 +6,8 @@ const Allocator = std.mem.Allocator; const Withdrawal = types.Withdrawal; const Tx = types.Tx; const ExecutionPayload = execution_payload.ExecutionPayload; +const lib = @import("../lib.zig"); +const Fork = lib.blockchain.Fork; pub const execution_payload = @import("execution_payload.zig"); @@ -85,21 +87,19 @@ pub const EngineAPIRequest = struct { test "deserialize sample engine_newPayloadV2" { const json = std.json; const expect = std.testing.expect; - const lib = @import("./../lib.zig"); const StateDB = lib.state.StateDB; const Blockchain = lib.blockchain.Blockchain; const AccountState = lib.state.AccountState; const BlockHeader = lib.blockchain.BlockHeader; - const Hash32 = lib.types.Hash32; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); + const allocator = arena.allocator(); const fileContent = @embedFile("./test_req.json"); - - const payload = try json.parseFromSlice(EngineAPIRequest, arena.allocator(), fileContent, .{ .ignore_unknown_fields = true }); + const payload = try json.parseFromSlice(EngineAPIRequest, allocator, fileContent, .{ .ignore_unknown_fields = true }); defer payload.deinit(); - var statedb = try StateDB.init(arena.allocator(), &[0]AccountState{}); + var statedb = try StateDB.init(allocator, &[0]AccountState{}); defer statedb.deinit(); const parent_header = BlockHeader{ .parent_hash = [_]u8{0} ** 32, @@ -120,11 +120,15 @@ test "deserialize sample engine_newPayloadV2" { .base_fee_per_gas = 7, .withdrawals_root = [_]u8{0} ** 32, }; - var blockchain = try Blockchain.init(arena.allocator(), .Testing, &statedb, parent_header, [_]Hash32{[_]u8{0} ** 32} ** 256); + // TODO pick the fork based on chain config + block number + timestamp + const prague_fork = try Fork.prague.enablePrague(&statedb, null, allocator); + defer prague_fork.deinit(); + try Fork.prague.deployContract(prague_fork); + var blockchain = try Blockchain.init(allocator, .Testing, &statedb, parent_header, prague_fork); try expect(std.mem.eql(u8, payload.value.method, "engine_newPayloadV2")); const execution_payload_json = payload.value.params[0]; - var ep = try execution_payload_json.to_execution_payload(arena.allocator()); - defer ep.deinit(arena.allocator()); + var ep = try execution_payload_json.to_execution_payload(allocator); + defer ep.deinit(allocator); try execution_payload.newPayloadV2Handler(&blockchain, &ep); } diff --git a/src/main.zig b/src/main.zig index 708318f..5d217f3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,6 +19,7 @@ const json = std.json; const simargs = @import("simargs"); const version = @import("version.zig").version; const Blockchain = lib.blockchain.Blockchain; +const Fork = lib.blockchain.Fork; fn engineAPIHandler(blockchain: *Blockchain, req: *httpz.Request, res: *httpz.Response) !void { if (try req.json(engine_api.EngineAPIRequest)) |payload| { @@ -98,7 +99,7 @@ pub fn main() !void { .base_fee_per_gas = 0, .withdrawals_root = [_]u8{0} ** 32, }; - var blockchain = try Blockchain.init(allocator, config.chainId, &statedb, parent_header, [_]Hash32{[_]u8{0} ** 32} ** 256); + var blockchain = try Blockchain.init(allocator, config.chainId, &statedb, parent_header, try Fork.frontier.newFrontierFork(allocator)); var engine_api_server = try httpz.ServerApp(*Blockchain).init(allocator, .{ .port = port, diff --git a/src/tests/custom_tests.zig b/src/tests/custom_tests.zig index 3629c42..e2172f9 100644 --- a/src/tests/custom_tests.zig +++ b/src/tests/custom_tests.zig @@ -12,6 +12,7 @@ const Hash32 = types.Hash32; const Address = types.Address; const StateDB = state.StateDB; const ChainID = config.ChainId; +const Fork = blockchain.Fork; test "create contract" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); @@ -25,7 +26,7 @@ test "create contract" { // Configure an EVM execution enviroment for a block from this coinbase. const env: Environment = .{ - .block_hashes = [_]Hash32{std.mem.zeroes(Hash32)} ** 256, + .fork = try Fork.frontier.newFrontierFork(allocator), .origin = coinbase, .coinbase = coinbase, .number = 100, diff --git a/src/tests/spec_tests.zig b/src/tests/spec_tests.zig index b4a0ccf..e66e1fb 100644 --- a/src/tests/spec_tests.zig +++ b/src/tests/spec_tests.zig @@ -19,6 +19,7 @@ const VM = vm.VM; const StateDB = state.StateDB; const AccountState = state.AccountState; const log = std.log.scoped(.execspectests); +const Fork = blockchain.Fork; const HexString = []const u8; @@ -78,7 +79,7 @@ pub const FixtureTest = struct { var out = try allocator.alloc(u8, self.genesisRLP.len / 2); var rlp_bytes = try std.fmt.hexToBytes(out, self.genesisRLP[2..]); const parent_block = try Block.decode(allocator, rlp_bytes); - var chain = try blockchain.Blockchain.init(allocator, config.ChainId.Mainnet, &statedb, parent_block.header, std.mem.zeroes([256]Hash32)); + var chain = try blockchain.Blockchain.init(allocator, config.ChainId.Mainnet, &statedb, parent_block.header, try Fork.frontier.newFrontierFork(allocator)); // Execute blocks. for (self.blocks) |encoded_block| {