-
Notifications
You must be signed in to change notification settings - Fork 4
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
Make contract creation work #26
Changes from all commits
39f9783
9f51d2f
7e16741
b4c6e6a
763575f
1b2c564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,15 +70,15 @@ pub const VM = struct { | |
.gas = @intCast(msg.gas), | ||
.recipient = toEVMCAddress(msg.current_target), | ||
.sender = toEVMCAddress(msg.caller), | ||
.input_data = msg.data.ptr, | ||
.input_size = msg.data.len, | ||
.input_data = if (msg.target != null) msg.data.ptr else msg.code.ptr, | ||
.input_size = if (msg.target != null) msg.data.len else msg.code.len, | ||
.value = blk: { | ||
var tx_value: [32]u8 = undefined; | ||
std.mem.writeIntSliceBig(u256, &tx_value, msg.value); | ||
break :blk .{ .bytes = tx_value }; | ||
}, | ||
.create2_salt = undefined, // EVMC docs: field only mandatory for CREATE2 kind which doesn't apply at depth 0. | ||
.code_address = toEVMCAddress(msg.code_address), | ||
.code_address = toEVMCAddress(msg.target), | ||
}; | ||
|
||
const result = EVMOneHost.call(@ptrCast(self), @ptrCast(&evmc_message)); | ||
|
@@ -402,8 +402,16 @@ const EVMOneHost = struct { | |
}; | ||
} | ||
|
||
const code_address = fromEVMCAddress(msg.*.code_address); | ||
const code = vm.env.state.getAccount(code_address).code; | ||
const code = switch (msg.*.kind) { | ||
evmc.EVMC_CALL, evmc.EVMC_DELEGATECALL, evmc.EVMC_CALLCODE => blk: { | ||
const code_address = fromEVMCAddress(msg.*.code_address); | ||
const code = vm.env.state.getAccount(code_address).code; | ||
break :blk code; | ||
}, | ||
evmc.EVMC_CREATE, evmc.EVMC_CREATE2 => msg.*.input_data[0..msg.*.input_size], | ||
else => @panic("unkown message kind"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no support for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}; | ||
Comment on lines
-405
to
+413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depending of the call kind we take the code from the state or the message input. |
||
|
||
var result = vm.evm.*.execute.?( | ||
vm.evm, | ||
@ptrCast(&vm.host), | ||
|
@@ -415,6 +423,12 @@ const EVMOneHost = struct { | |
); | ||
|
||
if (result.status_code == evmc.EVMC_SUCCESS) { | ||
if (msg.*.kind == evmc.EVMC_CREATE or msg.*.kind == evmc.EVMC_CREATE2) { | ||
vm.env.state.setContractCode(recipient_addr, result.output_data[0..result.output_size]) catch |err| switch (err) { | ||
error.OutOfMemory => @panic("OOO"), | ||
error.AccountAlreadyHasCode => @panic("account already has code"), | ||
}; | ||
} | ||
Comment on lines
+426
to
+431
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After the EVM execution, we take the result and save it as contract code for the corresponding address. This is saving the code for future calls for this addr. |
||
// Free the backup and indireclty commit to the changes that happened. | ||
prev_statedb.deinit(); | ||
|
||
|
@@ -428,7 +442,8 @@ const EVMOneHost = struct { | |
vm.env.state.* = prev_statedb; | ||
} | ||
|
||
evmclog.debug("call() depth={d} ended", .{msg.*.depth}); | ||
evmclog.debug("call() end depth={d} status_code={} gas_left={}", .{ msg.*.depth, result.status_code, result.gas_left }); | ||
|
||
return result; | ||
} | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ test "tests" { | |
std.testing.refAllDeclsRecursive(@import("crypto/crypto.zig")); | ||
std.testing.refAllDeclsRecursive(@import("engine_api/engine_api.zig")); | ||
std.testing.refAllDeclsRecursive(@import("tests/spec_tests.zig")); | ||
std.testing.refAllDeclsRecursive(@import("tests/custom_tests.zig")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created this new For this PR, I made a custom test (that you'll see later) that creates a contract, and then calls it since none of the current spec tests do contract creation (yet). |
||
std.testing.refAllDeclsRecursive(@import("state/state.zig")); | ||
std.testing.refAllDeclsRecursive(@import("types/types.zig")); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ pub const StateDB = struct { | |
accessed_accounts: AddressSet, | ||
accessed_storage_keys: AddressKeySet, | ||
|
||
pub fn init(allocator: Allocator, accounts: []AccountState) !StateDB { | ||
pub fn init(allocator: Allocator, accounts: []const AccountState) !StateDB { | ||
var db = AccountDB.init(allocator); | ||
try db.ensureTotalCapacity(@intCast(accounts.len)); | ||
for (accounts) |account| { | ||
|
@@ -45,9 +45,15 @@ pub const StateDB = struct { | |
} | ||
|
||
pub fn deinit(self: *StateDB) void { | ||
var key_iterator = self.db.keyIterator(); | ||
while (key_iterator.next()) |addr| { | ||
self.db.getPtr(addr.*).?.deinit(); | ||
} | ||
Comment on lines
+48
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were missing deiniting stuff correctly, which prob wasn't detected before since we use arenas anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and, from a performance point of view, would it not make sense not to deinit anything since that would be running some code that isn't necessary? I agree that, in terms of correctness, it's the wrong thing to do... and it might cause a lot of issues with tests... but we could require at the API level that what we are passed is an area allocator. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rationale for this, is that this client isn't required to mine blocks, so all allocations are performed in a single "block execution" context. I think we can squeeze some performance and readability out of this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a stateless client usual workflow, that's correct and what we actually do today. (Use an arena per block, that we reset to the next). Indeed all this work is a noop if an arena allocator was passed, so I agree I'd like to remove all this if really, despite technically correct, it's somewhat code that we don't need. Now... the situation here is that for spec tests we're expected to provide an existing StateDB before any block is executed. So if more than one block is executed in the test, StateDB memory must survive so we can't really nuke the StateDB allocations after every block (i.e: the next block would have missing data, since spec tests don't have a witness to reconstruct what it needs). So the situation here is that for spec tests, we can't let the StateDB allocations live in the same arena as the block... since that will corrupt the statedb information when the arena for the executed block is reset. This isn't a problem for a stateless client since we'll build a statedb on each block from the witness -- but that isn't the case for most tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear since my message maybe is a bit messy. We could remove the deinitis, but that would require all tests to always use arenas so the spec-tests can work, since:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Final comment: I'll take some note to change all tests to use arenas (if that isn't the case already), and we can simply remove the deinit stuff. Not the ideal thing to use arenas in test for everything since we won't be detecting any leak, but considering "the main case" will use arenas probably it's fine. |
||
self.db.deinit(); | ||
|
||
self.accessed_accounts.deinit(); | ||
self.accessed_storage_keys.deinit(); | ||
|
||
if (self.original_db) |*original_db| { | ||
original_db.deinit(); | ||
} | ||
|
@@ -130,6 +136,17 @@ pub const StateDB = struct { | |
_ = self.db.remove(addr); | ||
} | ||
|
||
pub fn setContractCode(self: *StateDB, addr: Address, code: []const u8) !void { | ||
var account = self.db.getPtr(addr); | ||
if (account) |acc| { | ||
if (acc.code.len > 0) | ||
return error.AccountAlreadyHasCode; | ||
acc.code = try acc.allocator.dupe(u8, code); | ||
return; | ||
} | ||
try self.db.put(addr, try AccountState.init(self.allocator, addr, 0, 0, code)); | ||
} | ||
|
||
pub fn accountExistsAndIsEmpty(self: *StateDB, addr: Address) bool { | ||
const account = self.db.get(addr) orelse return false; | ||
return account.nonce == 0 and account.balance == 0 and account.code.len == 0; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
const std = @import("std"); | ||
const config = @import("../config/config.zig"); | ||
const common = @import("../common/common.zig"); | ||
const vm = @import("../blockchain/vm.zig"); | ||
const state = @import("../state/state.zig"); | ||
const blockchain = @import("../blockchain/blockchain.zig"); | ||
const blockchain_types = @import("../blockchain/types.zig"); | ||
const Environment = blockchain_types.Environment; | ||
const types = @import("../types/types.zig"); | ||
const Hash32 = types.Hash32; | ||
const Address = types.Address; | ||
const StateDB = state.StateDB; | ||
const ChainID = config.ChainId; | ||
|
||
test "create contract" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A test that checks that contract creation and later execution works correctly. |
||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
defer arena.deinit(); | ||
const allocator = arena.allocator(); | ||
|
||
const coinbase = common.hexToAddress("0x1000000000000000000000000000000000000001"); | ||
var coinbase_state = try state.AccountState.init(allocator, coinbase, 1, 0, &[_]u8{}); | ||
var sdb = try StateDB.init(allocator, &[_]state.AccountState{coinbase_state}); | ||
defer sdb.deinit(); | ||
Comment on lines
+20
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Init a coinbase in our statedb. |
||
|
||
// Configure an EVM execution enviroment for a block from this coinbase. | ||
const env: Environment = .{ | ||
.block_hashes = [_]Hash32{std.mem.zeroes(Hash32)} ** 256, | ||
.origin = coinbase, | ||
.coinbase = coinbase, | ||
.number = 100, | ||
.base_fee_per_gas = 1, | ||
.gas_limit = 15_000_000, | ||
.gas_price = 100, | ||
.time = 100, | ||
.prev_randao = [_]u8{42} ** 32, | ||
.state = &sdb, | ||
.chain_id = ChainID.SpecTest, | ||
}; | ||
|
||
var vmi = vm.VM.init(env); | ||
defer vmi.deinit(); | ||
|
||
// Create contract. | ||
var contract_addr: Address = blk: { | ||
const msg = try blockchain.Blockchain.prepareMessage( | ||
allocator, | ||
coinbase, | ||
null, | ||
0, | ||
&[_]u8{ | ||
// Init | ||
0x60, 0x8, // PUSH1 8 | ||
0x60, 0x0C, // PUSH 12 | ||
0x60, 0x00, // PUSH1 0 | ||
0x39, // CODECOPY | ||
0x60, 0x8, // PUSH1 8 | ||
0x60, 0x00, // PUSH1 0 | ||
0xF3, // Return | ||
|
||
// Runtime code | ||
0x60, 0x01, // PUSH1 2 - Push 2 on the stack | ||
0x60, 0x02, // PUSH1 4 - Push 4 on the stack | ||
0x01, // ADD - Add stack[0] to stack[1] | ||
0x60, 0x00, // PUSH1 0 | ||
0x55, // SSTORE | ||
}, | ||
10_000, | ||
env, | ||
); | ||
|
||
try sdb.startTx(); | ||
var out = try vmi.processMessageCall(msg); | ||
|
||
// Check the contract creation execution was successful. | ||
try std.testing.expect(out.success); | ||
|
||
break :blk msg.current_target; | ||
}; | ||
Comment on lines
+43
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Contract creation tx, execute and check that was successful. |
||
|
||
// Run it. | ||
{ | ||
const msg = try blockchain.Blockchain.prepareMessage( | ||
allocator, | ||
coinbase, | ||
contract_addr, | ||
0, | ||
&[_]u8{}, | ||
100_000, | ||
env, | ||
); | ||
|
||
try sdb.startTx(); | ||
var out = try vmi.processMessageCall(msg); | ||
|
||
// Check that the execution didn't fail, thus the contract was found and executed correctly. | ||
try std.testing.expect(out.success); | ||
} | ||
Comment on lines
+80
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, the resulted bytecode of the contract creation was saved in the state. To double check, we run another tx calling that created code. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending if the
msg.target
is null or not, we use the message calldata orcode
which his specially prepared with the corresponding code from the contract.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh my, that you have to distinguish data from code is terrible ux :( anyhow, I was wondering if it was somehow possible to cast the pointer to a slice, so that
input_size
is no longer needed?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
input_size
is coming mostly from the library having a C interface. So in that case the concept of "slice" doesn't exist, and require the usual ptr+length API.