Skip to content

Commit 5dc9b65

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 5dc9b65

6 files changed

Lines changed: 79 additions & 15 deletions

File tree

bench/state_transition/process_block.zig

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ fn ProcessWithdrawalsBench(comptime fork: ForkSeq) type {
7979
const state = cloned.state.castToFork(fork);
8080
state_transition.getExpectedWithdrawals(
8181
fork,
82-
allocator,
8382
cloned.epoch_cache,
8483
state,
8584
&withdrawals_result,
@@ -347,7 +346,6 @@ fn ProcessBlockSegmentedBench(comptime fork: ForkSeq) type {
347346
defer withdrawal_balances.deinit();
348347
state_transition.getExpectedWithdrawals(
349348
fork,
350-
allocator,
351349
epoch_cache,
352350
state,
353351
&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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ pub fn processBlock(
7171

7272
try getExpectedWithdrawals(
7373
fork,
74-
allocator,
7574
epoch_cache,
7675
state,
7776
&withdrawals_result,

src/state_transition/block/process_withdrawals.zig

Lines changed: 10 additions & 9 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,
@@ -242,7 +244,6 @@ test "process withdrawals - sanity" {
242244

243245
try getExpectedWithdrawals(
244246
.electra,
245-
allocator,
246247
test_state.cached_state.epoch_cache,
247248
test_state.cached_state.state.castToFork(.electra),
248249
&withdrawals_result,

test/spec/runner/operations.zig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type {
279279

280280
try state_transition.getExpectedWithdrawals(
281281
fork,
282-
allocator,
283282
epoch_cache,
284283
state,
285284
&withdrawals_result,

0 commit comments

Comments
 (0)