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

implement deserialization of newPayloadV2 request #12

Merged
merged 1 commit into from
Dec 8, 2023

Conversation

gballet
Copy link
Collaborator

@gballet gballet commented Oct 30, 2023

This PR adds an RPC server (based on a totally overkill library, but that's the only one I could find that supports modules and works). It will decode the execution data payload and turn it into a block.

I have left the execution witness out for the moment.

@gballet gballet requested a review from jsign October 30, 2023 10:55
Comment on lines +21 to +26
const zap = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
const mod_zap = zap.module("zap");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

zap is a full web framework. I looked at other, simpler libs, but they either didn't work or weren't offering module support.

Comment on lines 7 to 40
fn prefixedhex2hash(dst: []u8, src: []const u8) !void {
if (src.len < 2 or src.len % 2 != 0) {
return error.InvalidInput;
}
var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;
if (src[skip0x..].len != 2 * dst.len) {
return error.InvalidOutputLength;
}
_ = try fmt.hexToBytes(dst[0..], src[skip0x..]);
}

fn prefixedhex2byteslice(allocator: Allocator, src: []const u8) ![]u8 {
// TODO catch the 0x0 corner case
if (src.len < 2 or src.len % 2 != 0) {
return error.InvalidInput;
}
var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;
var dst: []u8 = try allocator.alloc(u8, src[skip0x..].len / 2);

_ = try fmt.hexToBytes(dst[0..], src[skip0x..]);

return dst;
}

fn prefixedhex2u64(src: []const u8) !u64 {
// execution engine integers can be odd-length :facepalm:
if (src.len < 3) {
return error.InvalidInput;
}

var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;

return std.fmt.parseInt(u64, src[skip0x..], 16);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to try to turn this into a single hex2bin function but that will be the role of a subsequent PR

Copy link
Owner

Choose a reason for hiding this comment

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

Worth also consider if these kind of helper funcs are worth putting in a separate package, since sounds can be useful for other things.

Comment on lines +35 to +41
.block_number = @intCast(self.blockNumber),
.gas_limit = @intCast(self.gasLimit),
.gas_used = self.gasUsed,
.timestamp = @intCast(self.timestamp),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

for some reason, your Block type uses i64 for these fields. I assume that it's imposed by evmone, I worked around it for now.

Copy link
Owner

Choose a reason for hiding this comment

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

Comment on lines 55 to 75
pub fn newPayloadV2Handler(params: *const ExecutionPayload) !void {
// TODO reconstruct the proof from the (currently undefined) execution witness
// and verify it. Then execute the block and return the result.
// vm.run_block(params.to_block(), params.transactions);

// But so far, just print the content of the payload
std.log.info("newPayloadV2Handler: {any}", .{params});

_ = params.to_block();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

currently this simply checks that I can deserialize the block. I will expand that when I am done updating zig-verkle.

const engine_api = @import("engine_api/engine_api.zig");
const json = std.json;

var allocator: std.mem.Allocator = undefined;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made the allocator public as I needed it in my handler.

Copy link
Owner

Choose a reason for hiding this comment

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

Woah, OK, that's a bit ugly but I can't see any other workaround considering the forced handler signature.

At some point we can put these handler stuff in a separate file, and make the global allocator scoped there.

Still is a bit ugly that it's implicit that somebody has to initialize that allocator before using things.

Comment on lines +115 to +138
var listener = zap.SimpleHttpListener.init(.{
.port = 8551,
.on_request = engineAPIHandler,
.log = true,
});
try listener.listen();
std.log.info("Listening on 8551", .{});
zap.start(.{
.threads = 1,
.workers = 1,
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added that at the end of the main function, but I could also move it to a different binary if you prefer.

Copy link
Owner

Choose a reason for hiding this comment

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

I think should be fine; we can spend some time in the future refactoring stuff after we introduce a bunch of features so things don't get out of control.


var allocator: std.mem.Allocator = undefined;

fn engineAPIHandler(r: zap.SimpleRequest) void {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the return type here is void and not !void because you have to report errors as HTTP error codes. Not what I'm used to if you compare it to e.g. rails in dev mode, but why not.

Copy link
Owner

Choose a reason for hiding this comment

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

Go is the same:
func handler(w http.ResponseWriter, r *http.Request){}

Copy link
Owner

@jsign jsign left a comment

Choose a reason for hiding this comment

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

LGTM. I left some comments/questions for your consideration.

build.zig Show resolved Hide resolved
build.zig.zon Outdated
Comment on lines 4 to 10
.dependencies = .{ .@"zig-rlp" = .{
.url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-0.tar.gz",
.hash = "122000e0811d6cb4758f6122b1de2d384efa32b9b2714caec2236f6b34b0529d699c",
}, .zap = .{
.url = "https://github.com/zigzap/zap/archive/refs/tags/v0.1.14-pre.tar.gz",
.hash = "122067d12bc7abb93f7ce623f61b94cadfdb180cef12da6559d092e6b374202acda3",
} },
Copy link
Owner

Choose a reason for hiding this comment

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

Formatting nit:

.{ 
   .@"zig-rlp" = .{
        .url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-0.tar.gz",
        .hash = "122000e0811d6cb4758f6122b1de2d384efa32b9b2714caec2236f6b34b0529d699c",
    }, 
   .zap = .{
        .url = "https://github.com/zigzap/zap/archive/refs/tags/v0.1.14-pre.tar.gz",
        .hash = "122067d12bc7abb93f7ce623f61b94cadfdb180cef12da6559d092e6b374202acda3",
    },
},

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah my zig formatter is broken in visual studio for some reason, thankfully helix comes to the rescue but I have to manually save the file each time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

also the module name was plain wrong

if (src[skip0x..].len != 2 * dst.len) {
return error.InvalidOutputLength;
}
_ = try fmt.hexToBytes(dst[0..], src[skip0x..]);
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: the dst[0..] is not needed, dst alone should be fine.

Comment on lines 7 to 40
fn prefixedhex2hash(dst: []u8, src: []const u8) !void {
if (src.len < 2 or src.len % 2 != 0) {
return error.InvalidInput;
}
var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;
if (src[skip0x..].len != 2 * dst.len) {
return error.InvalidOutputLength;
}
_ = try fmt.hexToBytes(dst[0..], src[skip0x..]);
}

fn prefixedhex2byteslice(allocator: Allocator, src: []const u8) ![]u8 {
// TODO catch the 0x0 corner case
if (src.len < 2 or src.len % 2 != 0) {
return error.InvalidInput;
}
var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;
var dst: []u8 = try allocator.alloc(u8, src[skip0x..].len / 2);

_ = try fmt.hexToBytes(dst[0..], src[skip0x..]);

return dst;
}

fn prefixedhex2u64(src: []const u8) !u64 {
// execution engine integers can be odd-length :facepalm:
if (src.len < 3) {
return error.InvalidInput;
}

var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;

return std.fmt.parseInt(u64, src[skip0x..], 16);
}
Copy link
Owner

Choose a reason for hiding this comment

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

Worth also consider if these kind of helper funcs are worth putting in a separate package, since sounds can be useful for other things.

return error.InvalidInput;
}
var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0;
var dst: []u8 = try allocator.alloc(u8, src[skip0x..].len / 2);
Copy link
Owner

Choose a reason for hiding this comment

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

I know that you're planning to refactor this, but this could do this allocation and call prefixedhex2hash.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added a TODO to remind me to do that, indeed I would rather do it when I refactor.

Comment on lines +35 to +41
.block_number = @intCast(self.blockNumber),
.gas_limit = @intCast(self.gasLimit),
.gas_used = self.gasUsed,
.timestamp = @intCast(self.timestamp),
Copy link
Owner

Choose a reason for hiding this comment

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

Comment on lines +115 to +138
var listener = zap.SimpleHttpListener.init(.{
.port = 8551,
.on_request = engineAPIHandler,
.log = true,
});
try listener.listen();
std.log.info("Listening on 8551", .{});
zap.start(.{
.threads = 1,
.workers = 1,
});
Copy link
Owner

Choose a reason for hiding this comment

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

I think should be fine; we can spend some time in the future refactoring stuff after we introduce a bunch of features so things don't get out of control.

const engine_api = @import("engine_api/engine_api.zig");
const json = std.json;

var allocator: std.mem.Allocator = undefined;
Copy link
Owner

Choose a reason for hiding this comment

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

Woah, OK, that's a bit ugly but I can't see any other workaround considering the forced handler signature.

At some point we can put these handler stuff in a separate file, and make the global allocator scoped there.

Still is a bit ugly that it's implicit that somebody has to initialize that allocator before using things.


var allocator: std.mem.Allocator = undefined;

fn engineAPIHandler(r: zap.SimpleRequest) void {
Copy link
Owner

Choose a reason for hiding this comment

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

Go is the same:
func handler(w http.ResponseWriter, r *http.Request){}

return;
}
const payload = json.parseFromSlice(engine_api.EngineAPIRequest, allocator, r.body.?, .{ .ignore_unknown_fields = true }) catch |err| {
std.log.err("error parsing json: {} (body={s})", .{ err, r.body.? });
Copy link
Owner

Choose a reason for hiding this comment

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

I think at some point we have to work on some log package.

@jsign
Copy link
Owner

jsign commented Oct 30, 2023

@gballet, there's a failing test. I suspect might be related to added fields in the Block.

@gballet
Copy link
Collaborator Author

gballet commented Nov 1, 2023

right, it seems that zig-rlp doesn't support optional fields the way they should work.

// TODO reconstruct the proof from the (currently undefined) execution witness
// and verify it. Then execute the block and return the result.
// vm.run_block(params.to_block(), params.transactions);

// But so far, just print the content of the payload
std.log.info("newPayloadV2Handler: {any}", .{params});

_ = params.to_block();
var block = params.to_block();
Copy link
Owner

@jsign jsign Nov 20, 2023

Choose a reason for hiding this comment

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

L63 and L64 look quite weird to me. How do you know that params.to_block() allocator is the same that we received in this method?

These kind of patterns should probably make block have the right allocator internally and make L64 be block.deinit().

This potential mismatch between the received allocation, and whatever params.to_block() allocator was used, is quite confusing and hard to know if it's doing the right thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are several issues with this: first of all, the data that is freed has been allocated long before, with params - but a reference to the allocator could be copied from params in to_block. The other problem, however, is that the block can be serialized to RLP, which would make no sense if it holds an allocator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

well actually in ssz I offer the possibility to write a custom encoder, maybe I could do the same here, and so skip the allocator if it exists.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

see https://github.com/gballet/phant/pull/1 for the current state

Copy link
Owner

Choose a reason for hiding this comment

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

I think this confusion is mostly making it clear in the to_block() method comments who is the owner of params after that call.

It looks like block is now the owner of param after to_block() or something like that? But I'm not sure what it would mean if you do two to_block() calls -- wouldn't that mean that you risk doing a double-free?

To be sure of course you can do a copy in block of everything it needs so both things aren't tied together and solve the problem at the cost of a copy.

Copy link
Owner

Choose a reason for hiding this comment

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

To be clear, I'm not sure what is the right solution.
But here's when ownership gets interesting and it's good to find good patterns to avoid remembering "who owns what" of if constructing more than one to_block() is safe or not.

Copy link
Owner

Choose a reason for hiding this comment

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

@gballet
Copy link
Collaborator Author

gballet commented Dec 8, 2023

Replaced by #2, closing.

@gballet gballet closed this Dec 8, 2023
@gballet gballet reopened this Dec 8, 2023
@gballet
Copy link
Collaborator Author

gballet commented Dec 8, 2023

That was the wrong PR to close 🤦‍♂️

@gballet gballet merged commit 4938f47 into jsign:main Dec 8, 2023
4 checks passed
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