-
Notifications
You must be signed in to change notification settings - Fork 12
fix: isExecutionEnabled accepts block object #379
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -608,40 +608,43 @@ pub fn isExecutionStateType(self: *const BeaconStateView) !js.Boolean { | |
| return js.Boolean.from(fork_seq.gte(.bellatrix)); | ||
| } | ||
|
|
||
| /// Check if the merge transition is complete. | ||
| pub fn isExecutionEnabled(self: *const BeaconStateView, fork_name_value: js.String, signed_block_bytes: js.Uint8Array) !js.Boolean { | ||
| /// Check whether execution is enabled for the given Lodestar-shaped block object. | ||
| /// | ||
| /// For normal post-merge operation this short-circuits from state alone and does | ||
| /// not inspect `block`. The block object is only read for the historical pre-merge | ||
| /// Bellatrix case, where execution is enabled iff the block carries the first | ||
| /// non-default execution payload. | ||
| pub fn isExecutionEnabled(self: *const BeaconStateView, block: js.Value) !js.Boolean { | ||
| const cached_state = try self.requireState(); | ||
| const fork_seq = cached_state.state.forkSeq(); | ||
| if (fork_seq.lt(.bellatrix)) return js.Boolean.from(false); | ||
|
|
||
| var fork_name_buf: [16]u8 = undefined; | ||
| const fork_name = try fork_name_value.toSlice(&fork_name_buf); | ||
| const fork_seq = c.ForkSeq.fromName(fork_name); | ||
| const merge_complete: bool = switch (fork_seq) { | ||
| inline .bellatrix, .capella, .deneb, .electra, .fulu => |f| st.isMergeTransitionComplete(f, cached_state.state.castToFork(f)), | ||
| else => unreachable, | ||
| }; | ||
| if (merge_complete) return js.Boolean.from(true); | ||
|
|
||
| const bytes = try signed_block_bytes.toSlice(); | ||
| const signed_block = try AnySignedBeaconBlock.deserialize( | ||
| allocator, | ||
| .full, | ||
| fork_seq, | ||
| bytes, | ||
| ); | ||
| defer signed_block.deinit(allocator); | ||
| if (fork_seq != .bellatrix) return js.Boolean.from(false); | ||
|
|
||
| if (signed_block.forkSeq() != cached_state.state.forkSeq()) { | ||
| return throwNullAs(js.Boolean, "FORK_MISMATCH", "Fork of signed block does not match state fork"); | ||
| } | ||
| // After the above check, we reach the slow path: pre-merge Bellatrix. | ||
| // Walk the JS block into a native ExecutionPayload to compare against `default_value`. | ||
| const block_raw = block.toValue(); | ||
| if (try block_raw.typeof() != .object) return error.InvalidBlockObject; | ||
|
|
||
| const result = switch (cached_state.state.forkSeq()) { | ||
| inline else => |f| switch (signed_block.blockType()) { | ||
| inline else => |bt| if (comptime (bt == .blinded and f.lt(.bellatrix)) or (bt == .blinded and f.gte(.gloas))) { | ||
| return error.InvalidBlockTypeForFork; | ||
| } else st.isExecutionEnabled( | ||
| f, | ||
| cached_state.state.castToFork(f), | ||
| bt, | ||
| signed_block.beaconBlock().castToFork(bt, f), | ||
| ), | ||
| }, | ||
| }; | ||
| return js.Boolean.from(result); | ||
| const body = try (try block_raw.getNamedProperty("body")).coerceToObject(); | ||
|
|
||
| // Lodestar treats blinded pre-merge Bellatrix blocks as not-yet-merged: the state's | ||
| // execution payload header is still default, so the block doesn't kick off the transition. | ||
| if (try body.hasNamedProperty("executionPayloadHeader")) return js.Boolean.from(false); | ||
|
|
||
| const payload_js = try (try body.getNamedProperty("executionPayload")).coerceToObject(); | ||
| var payload: ct.bellatrix.ExecutionPayload.Type = ct.bellatrix.ExecutionPayload.default_value; | ||
| defer ct.bellatrix.ExecutionPayload.deinit(allocator, &payload); | ||
| try executionPayloadFromJs(payload_js, &payload); | ||
|
|
||
| const is_default = ct.bellatrix.ExecutionPayload.equals(&payload, &ct.bellatrix.ExecutionPayload.default_value); | ||
| return js.Boolean.from(!is_default); | ||
| } | ||
|
|
||
| /// Check if the merge transition is complete. | ||
|
|
@@ -1059,3 +1062,62 @@ fn optionalBool(options: ?js.Value, name: [:0]const u8, default_value: bool) !bo | |
| } | ||
| return default_value; | ||
| } | ||
|
|
||
| /// Populate a native Bellatrix `ExecutionPayload.Type` from a JS object with the Lodestar | ||
| /// shape. Caller must `ct.bellatrix.ExecutionPayload.deinit(allocator, out)` to free | ||
| /// `extra_data` and `transactions`. | ||
| fn executionPayloadFromJs(payload: napi.Value, out: *ct.bellatrix.ExecutionPayload.Type) !void { | ||
| try readByteArrayInto(payload, "parentHash", &out.parent_hash); | ||
| try readByteArrayInto(payload, "feeRecipient", &out.fee_recipient); | ||
| try readByteArrayInto(payload, "stateRoot", &out.state_root); | ||
| try readByteArrayInto(payload, "receiptsRoot", &out.receipts_root); | ||
| try readByteArrayInto(payload, "logsBloom", &out.logs_bloom); | ||
| try readByteArrayInto(payload, "prevRandao", &out.prev_randao); | ||
| try readByteArrayInto(payload, "blockHash", &out.block_hash); | ||
|
|
||
| out.block_number = @intCast(try (try payload.getNamedProperty("blockNumber")).getValueInt64()); | ||
| out.gas_limit = @intCast(try (try payload.getNamedProperty("gasLimit")).getValueInt64()); | ||
| out.gas_used = @intCast(try (try payload.getNamedProperty("gasUsed")).getValueInt64()); | ||
| out.timestamp = @intCast(try (try payload.getNamedProperty("timestamp")).getValueInt64()); | ||
|
|
||
| out.base_fee_per_gas = try readBigintU256(try payload.getNamedProperty("baseFeePerGas")); | ||
|
|
||
| const extra_data = try payload.getNamedProperty("extraData"); | ||
| const extra_data_info = try extra_data.getTypedarrayInfo(); | ||
| if (extra_data_info.array_type != .uint8) return error.InvalidExtraData; | ||
| try out.extra_data.appendSlice(allocator, extra_data_info.data); | ||
|
|
||
| const transactions = try payload.getNamedProperty("transactions"); | ||
| const tx_count = try transactions.getArrayLength(); | ||
| try out.transactions.ensureTotalCapacity(allocator, tx_count); | ||
| var i: u32 = 0; | ||
| while (i < tx_count) : (i += 1) { | ||
| const tx_value = try transactions.getElement(i); | ||
| const tx_info = try tx_value.getTypedarrayInfo(); | ||
| if (tx_info.array_type != .uint8) return error.InvalidTransaction; | ||
| var tx: std.ArrayListUnmanaged(u8) = .empty; | ||
| errdefer tx.deinit(allocator); | ||
| try tx.appendSlice(allocator, tx_info.data); | ||
| out.transactions.appendAssumeCapacity(tx); | ||
| } | ||
| } | ||
|
|
||
| fn readByteArrayInto(parent: napi.Value, comptime field: [:0]const u8, out: []u8) !void { | ||
| const value = try parent.getNamedProperty(field); | ||
| const info = try value.getTypedarrayInfo(); | ||
| if (info.array_type != .uint8) return error.InvalidByteArrayField; | ||
| if (info.data.len != out.len) return error.InvalidByteArrayLength; | ||
| @memcpy(out, info.data); | ||
| } | ||
|
|
||
| /// Read a JS bigint into u256. Consensus-types bigints are spec-defined unsigned, so we pass | ||
| /// `null` for `sign_bit` and skip the sign check entirely. | ||
| fn readBigintU256(value: napi.Value) !u256 { | ||
|
Collaborator
Author
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. this has to be fixed on the zapi level first since casting a u1 to *c_int is UB due to alignment |
||
| var words: [4]u64 = .{ 0, 0, 0, 0 }; | ||
| const got = try value.getValueBigintWords(null, &words); | ||
| var result: u256 = 0; | ||
| for (got, 0..) |word, i| { | ||
| result |= @as(u256, word) << @intCast(i * 64); | ||
| } | ||
| return result; | ||
| } | ||
|
Comment on lines
+1115
to
+1123
Contributor
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. In Because the loop condition uses |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,33 @@ interface ExecutionPayloadHeader { | |
| excessBlobGas?: number; // deneb+ | ||
| } | ||
|
|
||
| /* | ||
| * We don't need *all* the fields to check if a block | ||
| * is a pre-merge or a merge transition block, so we just | ||
| * have a minimum interface that is like a `BeaconBlock`. | ||
| */ | ||
| interface BeaconBlockLike { | ||
| body: { | ||
| executionPayload?: { | ||
| parentHash: Uint8Array; | ||
| feeRecipient: Uint8Array; | ||
| stateRoot: Uint8Array; | ||
| receiptsRoot: Uint8Array; | ||
| logsBloom: Uint8Array; | ||
| prevRandao: Uint8Array; | ||
| blockNumber: number; | ||
| gasLimit: number; | ||
| gasUsed: number; | ||
| timestamp: number; | ||
| extraData: Uint8Array; | ||
| baseFeePerGas: bigint; | ||
| blockHash: Uint8Array; | ||
| transactions: Uint8Array[]; | ||
| }; | ||
| executionPayloadHeader?: ExecutionPayloadHeader; | ||
| }; | ||
| } | ||
|
|
||
| interface Fork { | ||
| previousVersion: Uint8Array; | ||
| currentVersion: Uint8Array; | ||
|
|
@@ -184,10 +211,17 @@ declare class BeaconStateView { | |
|
|
||
| isExecutionStateType: boolean; | ||
| isMergeTransitionComplete: boolean; | ||
| // TODO remove | ||
| isExecutionEnabled(fork: string, signedBlockBytes: Uint8Array): boolean; | ||
|
|
||
| // getExpectedWithdrawals(): ExpectedWithdrawals; | ||
| /** True iff state is pre-merge AND the given block carries a non-default execution payload. Bellatrix-only. */ | ||
| isMergeTransitionBlock(signedBlockBytes: Uint8Array): boolean; | ||
|
Comment on lines
+214
to
+215
Contributor
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 method |
||
| /** | ||
| * Check whether execution is enabled for the given block at this state. | ||
| * | ||
| * For normal post-merge operation this short-circuits from state alone and does | ||
| * not inspect `block`. The block object is only read for the historical pre-merge | ||
| * Bellatrix case, where execution is enabled iff the block carries the first | ||
| * non-default execution payload. | ||
| */ | ||
| isExecutionEnabled(block: BeaconBlockLike): boolean; | ||
|
|
||
| proposerRewards: ProposerRewards; | ||
| // computeBlockRewards(block: BeaconBlock, proposerRewards: RewardsCache): BlockRewards; | ||
|
|
||
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 line exceeds the 100-column limit specified in the Repository Style Guide (Rule 400). Please wrap the switch arm to keep it under 100 columns.
References