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

Make contract creation work #26

Merged
merged 6 commits into from
Mar 28, 2024
Merged

Make contract creation work #26

merged 6 commits into from
Mar 28, 2024

Conversation

jsign
Copy link
Owner

@jsign jsign commented Mar 5, 2024

This PR makes some fixes to make contract creation work.

jsign added 6 commits March 5, 2024 19:29
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Comment on lines +73 to +74
.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,
Copy link
Owner Author

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 or code which his specially prepared with the corresponding code from the contract.

Copy link
Collaborator

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?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if it was somehow possible to cast the pointer to a slice, so that input_size is no longer needed?

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.

Comment on lines -405 to +413
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"),
};
Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

Comment on lines +426 to +431
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"),
};
}
Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

@@ -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"));
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created this new tests/custom_tests.zig to put customized tests, since the other tests in tests are mostly spec related ones.

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).

Comment on lines +48 to +51
var key_iterator = self.db.keyIterator();
while (key_iterator.next()) |addr| {
self.db.getPtr(addr.*).?.deinit();
}
Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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:

  • In spec tests, the arena to be used by StateDB and the rest of block exec can't be the same.
  • In stateless block exec, we can use the same arena for both StateDB and the rest of block exec.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

const StateDB = state.StateDB;
const ChainID = config.ChainId;

test "create contract" {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test that checks that contract creation and later execution works correctly.

Comment on lines +20 to +23
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();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Init a coinbase in our statedb.

Comment on lines +43 to +78
// 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;
};
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contract creation tx, execute and check that was successful.

Comment on lines +80 to +97
// 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);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The 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.

@jsign jsign requested a review from gballet March 23, 2024 16:17
Copy link
Collaborator

@gballet gballet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, just left a few questions/suggestions here and there, but feel free to merge if you want.

break :blk code;
},
evmc.EVMC_CREATE, evmc.EVMC_CREATE2 => msg.*.input_data[0..msg.*.input_size],
else => @panic("unkown message kind"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no support for STATICCALL ?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STATICCALL is modeled as a flag, it's not a kind.
kinds are:

pub const EVMC_CALL: c_int = 0;
pub const EVMC_DELEGATECALL: c_int = 1;
pub const EVMC_CALLCODE: c_int = 2;
pub const EVMC_CREATE: c_int = 3;
pub const EVMC_CREATE2: c_int = 4;

There's a flags field:
image

Comment on lines +48 to +51
var key_iterator = self.db.keyIterator();
while (key_iterator.next()) |addr| {
self.db.getPtr(addr.*).?.deinit();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Comment on lines +48 to +51
var key_iterator = self.db.keyIterator();
while (key_iterator.next()) |addr| {
self.db.getPtr(addr.*).?.deinit();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Comment on lines +73 to +74
.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,
Copy link
Collaborator

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?

@jsign jsign merged commit 70a1231 into main Mar 28, 2024
4 checks passed
@jsign jsign deleted the jsign-create-contract-2 branch March 28, 2024 00:10
@jsign
Copy link
Owner Author

jsign commented Mar 28, 2024

@gballet merged, but remember to read my replies since you made good questions/comments!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants