Skip to content

Commit 284e2bc

Browse files
committed
feat: bindings to getExpectedWithdrawals and native tweaks
- add bindings to it - don't heap allocate for `withdrawals_results` since it is capped at `preset.MAX_WITHDRAWALS_PER_PAYLOAD == 16`. This is about max ~800 bytes on mainnet preset - add assertions to assert the above
1 parent 4909369 commit 284e2bc

6 files changed

Lines changed: 91 additions & 35 deletions

File tree

bench/state_transition/process_block.zig

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,17 @@ fn ProcessWithdrawalsBench(comptime fork: ForkSeq) type {
6868
allocator.destroy(cloned);
6969
}
7070

71+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined;
7172
var withdrawals_result = WithdrawalsResult{
72-
.withdrawals = Withdrawals.initCapacity(allocator, preset.MAX_WITHDRAWALS_PER_PAYLOAD) catch unreachable,
73+
.withdrawals = Withdrawals.initBuffer(&withdrawals_buf),
7374
};
74-
defer withdrawals_result.withdrawals.deinit(allocator);
7575

7676
var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator);
7777
defer withdrawal_balances.deinit();
7878

7979
const state = cloned.state.castToFork(fork);
8080
state_transition.getExpectedWithdrawals(
8181
fork,
82-
allocator,
8382
cloned.epoch_cache,
8483
state,
8584
&withdrawals_result,
@@ -339,15 +338,15 @@ fn ProcessBlockSegmentedBench(comptime fork: ForkSeq) type {
339338

340339
if (comptime fork.gte(.capella)) {
341340
const withdrawals_start = time.timestampNow(io);
341+
342+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined;
342343
var withdrawals_result = WithdrawalsResult{
343-
.withdrawals = Withdrawals.initCapacity(allocator, preset.MAX_WITHDRAWALS_PER_PAYLOAD) catch unreachable,
344+
.withdrawals = Withdrawals.initBuffer(&withdrawals_buf),
344345
};
345-
defer withdrawals_result.withdrawals.deinit(allocator);
346346
var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator);
347347
defer withdrawal_balances.deinit();
348348
state_transition.getExpectedWithdrawals(
349349
fork,
350-
allocator,
351350
epoch_cache,
352351
state,
353352
&withdrawals_result,

bindings/napi/BeaconStateView.zig

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,8 +1284,56 @@ pub fn getSyncCommitteesWitness(_: *const BeaconStateView) !js.Value {
12841284
return throwNotImpl(js.Value, "getSyncCommitteesWitness not implemented");
12851285
}
12861286

1287-
pub fn getExpectedWithdrawals(_: *const BeaconStateView) !js.Value {
1288-
return throwNotImpl(js.Value, "getExpectedWithdrawals not implemented");
1287+
/// Compute expected withdrawals for the next payload (capella+).
1288+
/// Returns: { expectedWithdrawals: Withdrawal[], processedPartialWithdrawalsCount, processedValidatorSweepCount,
1289+
/// processedBuilderWithdrawalsCount, processedBuildersSweepCount }
1290+
/// The latter two are Gloas-only — always 0 here since Zig STF doesn't process Gloas yet.
1291+
pub fn getExpectedWithdrawals(self: *const BeaconStateView) !js.Value {
1292+
const env = js.env();
1293+
const cached_state = try self.requireState();
1294+
const fork_seq = cached_state.state.forkSeq();
1295+
1296+
// We also check this within the native fn itself but this lets us avoid allocating an `AutoHashMap` early.
1297+
if (fork_seq.lt(.capella)) {
1298+
return throwNullAs(js.Value, "INVALID_FORK", "getExpectedWithdrawals only supported capella+");
1299+
}
1300+
1301+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]ct.capella.Withdrawal.Type = undefined;
1302+
var withdrawals_result = st.WithdrawalsResult{
1303+
.withdrawals = ct.capella.Withdrawals.Type.initBuffer(&withdrawals_buf),
1304+
};
1305+
1306+
var withdrawal_balances = std.AutoHashMap(ct.primitive.ValidatorIndex.Type, usize).init(allocator);
1307+
defer withdrawal_balances.deinit();
1308+
1309+
switch (fork_seq) {
1310+
inline .capella, .deneb, .electra, .fulu => |f| {
1311+
try st.getExpectedWithdrawals(
1312+
f,
1313+
cached_state.epoch_cache,
1314+
cached_state.state.castToFork(f),
1315+
&withdrawals_result,
1316+
&withdrawal_balances,
1317+
);
1318+
},
1319+
else => unreachable,
1320+
}
1321+
1322+
const obj = try env.createObject();
1323+
1324+
const withdrawals_arr = try env.createArray();
1325+
for (withdrawals_result.withdrawals.items, 0..) |*w, i| {
1326+
const w_value = try sszValueToNapiValue(env, ct.capella.Withdrawal, w);
1327+
try withdrawals_arr.setElement(@intCast(i), w_value);
1328+
}
1329+
try obj.setNamedProperty("expectedWithdrawals", withdrawals_arr);
1330+
try obj.setNamedProperty("processedPartialWithdrawalsCount", try env.createUint32(@intCast(withdrawals_result.processed_partial_withdrawals_count)));
1331+
try obj.setNamedProperty("processedValidatorSweepCount", try env.createUint32(@intCast(withdrawals_result.sampled_validators)));
1332+
// TODO(bing): Implement when we support Gloas.
1333+
try obj.setNamedProperty("processedBuilderWithdrawalsCount", try env.createUint32(0));
1334+
try obj.setNamedProperty("processedBuildersSweepCount", try env.createUint32(0));
1335+
1336+
return js_types.wrap(js.Value, obj);
12891337
}
12901338

12911339
fn requireState(self: *const BeaconStateView) !*CachedBeaconState {

bindings/src/index.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,25 @@ declare class BeaconStateView {
271271
// biome-ignore lint/suspicious/noExplicitAny: stub
272272
getExpectedWithdrawals(): any;
273273
getSingleProof(gindex: bigint): Uint8Array[];
274+
// getSyncCommitteesWitness(): any;
275+
/**
276+
* Compute expected withdrawals for the next payload (capella+).
277+
*
278+
* processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732)
279+
* processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
280+
* processedBuildersSweepCount is withdrawals from builder sweep since gloas (EIP-7732)
281+
* processedValidatorSweepCount is withdrawals coming from validator sweep
282+
283+
* TODO(bing): `processedBuilderWithdrawalsCount` and `processedBuildersSweepCount` are Gloas-only
284+
* and always 0 here since Zig STF doesn't process Gloas yet.
285+
*/
286+
getExpectedWithdrawals(): {
287+
expectedWithdrawals: {index: number; validatorIndex: number; address: Uint8Array; amount: number}[];
288+
processedBuilderWithdrawalsCount: number;
289+
processedPartialWithdrawalsCount: number;
290+
processedBuildersSweepCount: number;
291+
processedValidatorSweepCount: number;
292+
};
274293
// createMultiProof(descriptor: Uint8Array): CompactMultiProof;
275294

276295
computeUnrealizedCheckpoints(): {

src/state_transition/block/process_block.zig

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,18 @@ pub fn processBlock(
6161
// TODO Deneb: Allow to disable withdrawals for interop testing
6262
// https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals
6363
if (comptime fork.gte(.capella)) {
64-
// TODO: given max withdrawals of MAX_WITHDRAWALS_PER_PAYLOAD, can use fixed size array instead of heap alloc
65-
var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity(
66-
allocator,
67-
preset.MAX_WITHDRAWALS_PER_PAYLOAD,
68-
) };
64+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined;
65+
var withdrawals_result = WithdrawalsResult{ .withdrawals = Withdrawals.initBuffer(&withdrawals_buf) };
6966
var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator);
7067
defer withdrawal_balances.deinit();
7168

7269
try getExpectedWithdrawals(
7370
fork,
74-
allocator,
7571
epoch_cache,
7672
state,
7773
&withdrawals_result,
7874
&withdrawal_balances,
7975
);
80-
defer withdrawals_result.withdrawals.deinit(allocator);
8176

8277
const payload_withdrawals_root = switch (block_type) {
8378
.full => blk: {

src/state_transition/block/process_withdrawals.zig

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,20 @@ pub fn processWithdrawals(
8282
}
8383
}
8484

85-
// Consumer should deinit WithdrawalsResult with .deinit() after use
85+
/// Called by the block proposer to find a list of withdrawals to include in the block.
86+
///
87+
/// This list is assumed to be bounded by `preset.MAX_WITHDRAWALS_PER_PAYLOAD`.
88+
///
89+
/// Caller should deinit `withdrawal_balances` with .deinit() after use.
8690
pub fn getExpectedWithdrawals(
8791
comptime fork: ForkSeq,
88-
allocator: Allocator,
8992
epoch_cache: *const EpochCache,
9093
state: *BeaconState(fork),
9194
withdrawals_result: *WithdrawalsResult,
9295
withdrawal_balances: *std.AutoHashMap(ValidatorIndex, usize),
9396
) !void {
94-
if (comptime fork.lt(.capella)) {
95-
return error.InvalidForkSequence;
96-
}
97+
std.debug.assert(withdrawals_result.withdrawals.capacity == preset.MAX_WITHDRAWALS_PER_PAYLOAD);
98+
if (comptime fork.lt(.capella)) return error.InvalidForkSequence;
9799

98100
const epoch = epoch_cache.epoch;
99101
var withdrawal_index = try state.nextWithdrawalIndex();
@@ -134,7 +136,7 @@ pub fn getExpectedWithdrawals(
134136
const withdrawable_balance = if (balance_over_min_activation_balance < withdrawal.amount) balance_over_min_activation_balance else withdrawal.amount;
135137
var execution_address: ExecutionAddress = undefined;
136138
@memcpy(&execution_address, validator.withdrawal_credentials[12..]);
137-
try withdrawals_result.withdrawals.append(allocator, .{
139+
withdrawals_result.withdrawals.appendAssumeCapacity(.{
138140
.index = withdrawal_index,
139141
.validator_index = withdrawal.validator_index,
140142
.address = execution_address,
@@ -179,7 +181,7 @@ pub fn getExpectedWithdrawals(
179181
if (withdrawable_epoch <= epoch) {
180182
var execution_address: ExecutionAddress = undefined;
181183
@memcpy(&execution_address, withdrawal_credentials[12..]);
182-
try withdrawals_result.withdrawals.append(allocator, .{
184+
withdrawals_result.withdrawals.appendAssumeCapacity(.{
183185
.index = withdrawal_index,
184186
.validator_index = validator_index,
185187
.address = execution_address,
@@ -195,7 +197,7 @@ pub fn getExpectedWithdrawals(
195197
const partial_amount = balance - effective_balance;
196198
var execution_address: ExecutionAddress = undefined;
197199
@memcpy(&execution_address, withdrawal_credentials[12..]);
198-
try withdrawals_result.withdrawals.append(allocator, .{
200+
withdrawals_result.withdrawals.appendAssumeCapacity(.{
199201
.index = withdrawal_index,
200202
.validator_index = validator_index,
201203
.address = execution_address,
@@ -227,13 +229,10 @@ test "process withdrawals - sanity" {
227229
var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
228230
defer test_state.deinit();
229231

232+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined;
230233
var withdrawals_result = WithdrawalsResult{
231-
.withdrawals = try Withdrawals.initCapacity(
232-
allocator,
233-
preset.MAX_WITHDRAWALS_PER_PAYLOAD,
234-
),
234+
.withdrawals = Withdrawals.initBuffer(&withdrawals_buf),
235235
};
236-
defer withdrawals_result.withdrawals.deinit(allocator);
237236
var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator);
238237
defer withdrawal_balances.deinit();
239238

@@ -242,7 +241,6 @@ test "process withdrawals - sanity" {
242241

243242
try getExpectedWithdrawals(
244243
.electra,
245-
allocator,
246244
test_state.cached_state.epoch_cache,
247245
test_state.cached_state.state.castToFork(.electra),
248246
&withdrawals_result,

test/spec/runner/operations.zig

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -267,25 +267,22 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type {
267267
},
268268
.withdrawals => {
269269
const epoch_cache = cached_state.epoch_cache;
270+
271+
var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]ssz.capella.Withdrawal.Type = undefined;
270272
var withdrawals_result = WithdrawalsResult{
271-
.withdrawals = try Withdrawals.initCapacity(
272-
allocator,
273-
preset.MAX_WITHDRAWALS_PER_PAYLOAD,
274-
),
273+
.withdrawals = Withdrawals.initBuffer(&withdrawals_buf),
275274
};
276275

277276
var withdrawal_balances = std.AutoHashMap(u64, usize).init(allocator);
278277
defer withdrawal_balances.deinit();
279278

280279
try state_transition.getExpectedWithdrawals(
281280
fork,
282-
allocator,
283281
epoch_cache,
284282
state,
285283
&withdrawals_result,
286284
&withdrawal_balances,
287285
);
288-
defer withdrawals_result.withdrawals.deinit(allocator);
289286

290287
var payload_withdrawals_root: Root = undefined;
291288
// self.op is ExecutionPayload in this case

0 commit comments

Comments
 (0)