Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
adff6c1
update types and constants for alpha.11
guha-rahul Jun 30, 2026
d5d9743
process builder deposit and exit requests
guha-rahul Jul 1, 2026
f1c7b92
remove builder voluntary exit handling
guha-rahul Jul 1, 2026
1c2a745
process parent executionpayload
guha-rahul Jul 1, 2026
e92b837
update Gloas execution payload bid checks
guha-rahul Jul 1, 2026
9e9ec68
update Gloas churn and preset constants
guha-rahul Jul 1, 2026
11b2d37
update Gloas fork upgrade builder onboarding
guha-rahul Jul 1, 2026
45bc63f
account for builder withdrawals
guha-rahul Jul 1, 2026
77a55a0
update epoch processing and proposer lookahead
guha-rahul Jul 1, 2026
dd3bea3
only clear slashed proposer pending payment
guha-rahul Jul 1, 2026
824870f
support current gloas operation vectors
guha-rahul Jul 1, 2026
b95d6f6
clean up
guha-rahul Jul 1, 2026
144abd5
add utility helpers
guha-rahul Jul 1, 2026
4818d70
port remaining Gloas STF helpers
guha-rahul Jul 2, 2026
e0e7fab
Merge branch 'ChainSafe:main' into gloas
guha-rahul Jul 2, 2026
2b63b1c
fix compile errors
guha-rahul Jul 2, 2026
09aea6d
fix getExpectedWithdrawals call sites
guha-rahul Jul 2, 2026
b712803
enforce Capella payload parent hash
guha-rahul Jul 4, 2026
2da2377
remove stale payload envelope processor
guha-rahul Jul 4, 2026
af826e9
mirror PTC window in epoch cache
guha-rahul Jul 4, 2026
8084292
remove unused helper functions
guha-rahul Jul 4, 2026
7feecea
add doc comments as upstream
guha-rahul Jul 7, 2026
ec5fcdb
more comments and getReadonly instead of get
guha-rahul Jul 7, 2026
27def7e
make PendingDepositsLookup self-contained
guha-rahul Jul 7, 2026
8cd5adb
refactor pending deposits lookup
guha-rahul Jul 7, 2026
42264fc
nit order
guha-rahul Jul 7, 2026
83d93d2
merge main into gloas
guha-rahul Jul 7, 2026
32e4623
compute PTC from epoch shufflings
guha-rahul Jul 22, 2026
d58bfb9
merge main into gloas
guha-rahul Aug 11, 2026
70ab912
refactor(state-transition): thread std.Io through gloas paths
guha-rahul Aug 10, 2026
3085fa9
fix(state-transition): correct error-path cleanup in shuffling and co…
guha-rahul Aug 11, 2026
3b465d1
test(state-transition): add gloas block generator
guha-rahul Aug 11, 2026
7bb374f
test(state-transition): add gloas OOM/double-free fault injection suite
guha-rahul Aug 11, 2026
8920763
merge main into gloas
guha-rahul Aug 11, 2026
63b6365
merge main into gloas
guha-rahul Aug 11, 2026
4a4aa44
Merge branch 'main' into gloas
spiral-ladder Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bench/state_transition/process_block.zig
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn ProcessWithdrawalsBench(comptime fork: ForkSeq) type {
const state = BenchState.cloned_cached_state.state.castToFork(fork);
state_transition.getExpectedWithdrawals(
fork,
allocator,
BenchState.cloned_cached_state.epoch_cache,
state,
&withdrawals_result,
Expand Down Expand Up @@ -325,6 +326,7 @@ fn ProcessBlockSegmentedBench(comptime fork: ForkSeq) type {
defer withdrawal_balances.deinit();
state_transition.getExpectedWithdrawals(
fork,
allocator,
epoch_cache,
state,
&withdrawals_result,
Expand Down
1 change: 1 addition & 0 deletions bindings/napi/BeaconStateView.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,7 @@ pub fn getExpectedWithdrawals(self: *const BeaconStateView) !js.Value {
inline .capella, .deneb, .electra, .fulu => |f| {
try st.getExpectedWithdrawals(
f,
allocator,
cached_state.epoch_cache,
cached_state.state.castToFork(f),
&withdrawals_result,
Expand Down
1 change: 1 addition & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
.metrics,
.hashing,
.time,
.testing_allocators,
},
},
.era = .{
Expand Down
40 changes: 38 additions & 2 deletions src/consensus_types/gloas.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pub const HistoricalSummary = capella.HistoricalSummary;

pub const BlobIdentifier = deneb.BlobIdentifier;
pub const BlobKzgCommitments = deneb.BlobKzgCommitments;
pub const LogsBloom = bellatrix.LogsBloom;
pub const ExtraData = bellatrix.ExtraData;
pub const Transactions = bellatrix.Transactions;

// Reuse Electra types
pub const PendingDeposit = electra.PendingDeposit;
Expand All @@ -66,7 +69,6 @@ pub const PendingConsolidation = electra.PendingConsolidation;
pub const DepositRequest = electra.DepositRequest;
pub const WithdrawalRequest = electra.WithdrawalRequest;
pub const ConsolidationRequest = electra.ConsolidationRequest;
pub const ExecutionRequests = electra.ExecutionRequests;
pub const SingleAttestation = electra.SingleAttestation;
pub const Attestation = electra.Attestation;
pub const Attestations = electra.Attestations;
Expand Down Expand Up @@ -151,6 +153,39 @@ pub const BuilderPendingWithdrawal = ssz.FixedContainerType(struct {
pub const BuilderPendingPayment = ssz.FixedContainerType(struct {
weight: p.Uint64,
withdrawal: BuilderPendingWithdrawal,
proposer_index: p.ValidatorIndex,
});

pub const BuilderDepositRequest = ssz.FixedContainerType(struct {
pubkey: p.BLSPubkey,
withdrawal_credentials: p.Bytes32,
amount: p.Gwei,
signature: p.BLSSignature,
});

pub const BuilderDepositRequests = ssz.FixedListType(
BuilderDepositRequest,
preset.MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
.{},
);

pub const BuilderExitRequest = ssz.FixedContainerType(struct {
source_address: p.ExecutionAddress,
pubkey: p.BLSPubkey,
});

pub const BuilderExitRequests = ssz.FixedListType(
BuilderExitRequest,
preset.MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
.{},
);

pub const ExecutionRequests = ssz.VariableContainerType(struct {
deposits: ssz.FixedListType(DepositRequest, preset.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, .{}),
withdrawals: ssz.FixedListType(WithdrawalRequest, preset.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, .{}),
consolidations: ssz.FixedListType(ConsolidationRequest, preset.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, .{}),
builder_deposits: BuilderDepositRequests,
builder_exits: BuilderExitRequests,
});

pub const PayloadAttestationData = ssz.FixedContainerType(struct {
Expand Down Expand Up @@ -179,10 +214,11 @@ pub const IndexedPayloadAttestation = ssz.VariableContainerType(struct {
});

pub const ProposerPreferences = ssz.FixedContainerType(struct {
dependent_root: p.Root,
proposal_slot: p.Slot,
validator_index: p.ValidatorIndex,
fee_recipient: p.ExecutionAddress,
gas_limit: p.Uint64,
target_gas_limit: p.Uint64,
});

pub const SignedProposerPreferences = ssz.FixedContainerType(struct {
Expand Down
24 changes: 23 additions & 1 deletion src/constants/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,41 @@ pub const DOMAIN_BLS_TO_EXECUTION_CHANGE = [_]u8{ 10, 0, 0, 0 };
pub const DOMAIN_BEACON_BUILDER = [_]u8{ 11, 0, 0, 0 }; // 0x0B000000
pub const DOMAIN_PTC_ATTESTER = [_]u8{ 12, 0, 0, 0 }; // 0x0C000000
pub const DOMAIN_PROPOSER_PREFERENCES = [_]u8{ 13, 0, 0, 0 }; // 0x0D000000
pub const DOMAIN_BUILDER_DEPOSIT = [_]u8{ 14, 0, 0, 0 }; // 0x0E000000

// Application specific domains

pub const DOMAIN_APPLICATION_MASK = [_]u8{ 0, 0, 0, 1 };
pub const DOMAIN_APPLICATION_BUILDER = [_]u8{ 0, 0, 0, 1 };

// need to be updated when new domain is added
pub const ALL_DOMAINS = [_][4]u8{ DOMAIN_BEACON_PROPOSER, DOMAIN_BEACON_ATTESTER, DOMAIN_RANDAO, DOMAIN_DEPOSIT, DOMAIN_VOLUNTARY_EXIT, DOMAIN_SELECTION_PROOF, DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_CONTRIBUTION_AND_PROOF, DOMAIN_BLS_TO_EXECUTION_CHANGE, DOMAIN_BEACON_BUILDER, DOMAIN_PTC_ATTESTER, DOMAIN_PROPOSER_PREFERENCES, DOMAIN_APPLICATION_MASK, DOMAIN_APPLICATION_BUILDER };
pub const ALL_DOMAINS = [_][4]u8{
DOMAIN_BEACON_PROPOSER,
DOMAIN_BEACON_ATTESTER,
DOMAIN_RANDAO,
DOMAIN_DEPOSIT,
DOMAIN_VOLUNTARY_EXIT,
DOMAIN_SELECTION_PROOF,
DOMAIN_AGGREGATE_AND_PROOF,
DOMAIN_SYNC_COMMITTEE,
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF,
DOMAIN_CONTRIBUTION_AND_PROOF,
DOMAIN_BLS_TO_EXECUTION_CHANGE,
DOMAIN_BEACON_BUILDER,
DOMAIN_PTC_ATTESTER,
DOMAIN_PROPOSER_PREFERENCES,
DOMAIN_BUILDER_DEPOSIT,
DOMAIN_APPLICATION_MASK,
DOMAIN_APPLICATION_BUILDER,
};

// Gloas (EIP-7732) misc

pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; // 2**40
pub const BUILDER_INDEX_SELF_BUILD: u64 = std.math.maxInt(u64);
pub const BUILDER_PAYMENT_THRESHOLD_NUMERATOR = 6;
pub const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR = 10;
pub const PAYLOAD_BUILDER_VERSION = 0;

// Participation flag indices

Expand Down Expand Up @@ -122,6 +142,8 @@ pub const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA = 23;
pub const DEPOSIT_REQUEST_TYPE = 0x00;
pub const WITHDRAWAL_REQUEST_TYPE = 0x01;
pub const CONSOLIDATION_REQUEST_TYPE = 0x02;
pub const BUILDER_DEPOSIT_REQUEST_TYPE = 0x03;
pub const BUILDER_EXIT_REQUEST_TYPE = 0x04;

pub const CURRENT_SYNC_COMMITTEE_GINDEX = 54;
pub const EXECUTION_PAYLOAD_GINDEX = 25;
Expand Down
6 changes: 5 additions & 1 deletion src/preset/preset.zig
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const PresetMainnet = struct {
pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16;
pub const PTC_SIZE = 512;
pub const MAX_PAYLOAD_ATTESTATIONS = 4;
pub const MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD = 256;
pub const MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD = 16;
pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776;
pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576;
pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16384;
Expand Down Expand Up @@ -161,8 +163,10 @@ const PresetMinimal = struct {
pub const KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH = 4;
pub const MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 2;
pub const MAX_PENDING_DEPOSITS_PER_EPOCH = PresetMainnet.MAX_PENDING_DEPOSITS_PER_EPOCH;
pub const PTC_SIZE = 2;
pub const PTC_SIZE = 16;
pub const MAX_PAYLOAD_ATTESTATIONS = 4;
pub const MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD = 256;
pub const MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD = 16;
pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776;
pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576;
pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const std = @import("std");
const types = @import("consensus_types");
const ValidatorIndex = types.primitive.ValidatorIndex.Type;
const BeaconConfig = @import("config").BeaconConfig;
const EpochCache = @import("../cache/epoch_cache.zig").EpochCache;
const getIndexedPayloadAttestationSignatureSet = @import("../signature_sets/indexed_payload_attestation.zig").getIndexedPayloadAttestationSignatureSet;
const verifyAggregatedSignatureSet = @import("../utils/signature_sets.zig").verifyAggregatedSignatureSet;

/// Validate an IndexedPayloadAttestation: check that attesting indices are non-empty,
/// sorted, and (optionally) that the aggregate BLS signature is valid.
pub fn isValidIndexedPayloadAttestation(
allocator: std.mem.Allocator,
io: std.Io,
config: *const BeaconConfig,
epoch_cache: *const EpochCache,
indexed_payload_attestation: *const types.gloas.IndexedPayloadAttestation.Type,
verify_signature: bool,
) !bool {
const attesting_indices = indexed_payload_attestation.attesting_indices.items;

if (attesting_indices.len == 0) return false;

var prev: ValidatorIndex = 0;
for (attesting_indices, 0..) |index, i| {
if (i >= 1 and index < prev) {
return false;
}
prev = index;
}

if (!verify_signature) return true;

const sig_set = try getIndexedPayloadAttestationSignatureSet(allocator, io, config, epoch_cache, indexed_payload_attestation);
defer allocator.free(sig_set.pubkeys);
return verifyAggregatedSignatureSet(&sig_set);
}
91 changes: 85 additions & 6 deletions src/state_transition/block/process_attestation_altair.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const getBeaconProposer = @import("../cache/get_beacon_proposer.zig").getBeaconP
const Checkpoint = types.phase0.Checkpoint.Type;
const isTimelyTarget = @import("./process_attestation_phase0.zig").isTimelyTarget;
const increaseBalance = @import("../utils/balance.zig").increaseBalance;
const gloas_utils = @import("../utils/gloas.zig");
const isAttestationSameSlot = gloas_utils.isAttestationSameSlot;
const isAttestationSameSlotRootCache = gloas_utils.isAttestationSameSlotRootCache;

const PROPOSER_REWARD_DOMINATOR = ((c.WEIGHT_DENOMINATOR - c.PROPOSER_WEIGHT) * c.WEIGHT_DENOMINATOR) / c.PROPOSER_WEIGHT;

Expand Down Expand Up @@ -50,10 +53,18 @@ pub fn processAttestationsAltair(
defer root_cache.deinit();

// Process all attestations first and then increase the balance of the proposer once
// let newSeenAttesters = 0;
// let newSeenAttestersEffectiveBalance = 0;

// TODO: metrics — newSeenAttesters, newSeenAttestersEffectiveBalance, attestationsPerBlock
var proposer_reward: u64 = 0;

var builder_weight_map = std.AutoHashMap(u64, u64).init(allocator);
defer builder_weight_map.deinit();

// Get executionPayloadAvailability for gloas
const execution_payload_availability = if (fork.gte(.gloas))
try state.inner.get("execution_payload_availability")
else
@as(?void, null);

for (attestations) |*attestation| {
const data = &attestation.data;
try validateAttestation(fork, epoch_cache, state, attestation);
Expand Down Expand Up @@ -83,7 +94,17 @@ pub fn processAttestationsAltair(

const in_current_epoch = data.target.epoch == current_epoch;
var epoch_participation = if (in_current_epoch) try state.currentEpochParticipation() else try state.previousEpochParticipation();
const flags_attestation = try getAttestationParticipationStatus(fork, data, state_slot - data.slot, current_epoch, root_cache);
// Count how much additional weight added to current or previous epoch's builder pending payment (in ETH increment)
var payment_weight_to_add: u64 = 0;

const flags_attestation = try getAttestationParticipationStatus(
fork,
data,
state_slot - data.slot,
current_epoch,
root_cache,
execution_payload_availability,
);

// For each participant, update their participation
// In epoch processing, this participation info is used to calculate balance updates
Expand Down Expand Up @@ -124,21 +145,59 @@ pub fn processAttestationsAltair(
}
}
}

if (fork.gte(.gloas) and flags_new_set != 0 and (try isAttestationSameSlot(state, data))) {
payment_weight_to_add += effective_balance_increments[validator_index];
}
}

// Do the discrete math inside the loop to ensure a deterministic result
const total_increments = total_balance_increments_with_weight;
const proposer_reward_numerator = total_increments * epoch_cache.base_reward_per_increment;
proposer_reward += @divFloor(proposer_reward_numerator, PROPOSER_REWARD_DOMINATOR);

if (fork.gte(.gloas)) {
const builder_pending_payment_index: u64 = if (in_current_epoch)
preset.SLOTS_PER_EPOCH + (data.slot % preset.SLOTS_PER_EPOCH)
else
data.slot % preset.SLOTS_PER_EPOCH;

var builder_pending_payments = try state.inner.get("builder_pending_payments");
const existing_weight = builder_weight_map.get(builder_pending_payment_index) orelse blk: {
var payment: types.gloas.BuilderPendingPayment.Type = undefined;
try builder_pending_payments.getValue(allocator, builder_pending_payment_index, &payment);
break :blk payment.weight;
};
const updated_weight = existing_weight + payment_weight_to_add * preset.EFFECTIVE_BALANCE_INCREMENT;
try builder_weight_map.put(builder_pending_payment_index, updated_weight);
}
}

// Batch write builder weights after all attestations
if (fork.gte(.gloas)) {
var builder_pending_payments = try state.inner.get("builder_pending_payments");
var it = builder_weight_map.iterator();
while (it.next()) |entry| {
var payment: types.gloas.BuilderPendingPayment.Type = undefined;
try builder_pending_payments.getValue(allocator, entry.key_ptr.*, &payment);
if (payment.withdrawal.amount > 0) {
payment.weight = entry.value_ptr.*;
try builder_pending_payments.setValue(entry.key_ptr.*, &payment);
}
}
}

try increaseBalance(fork, state, try getBeaconProposer(fork, epoch_cache, state, state_slot), proposer_reward);
// TODO: state.proposerRewards.attestations = proposerReward and metrics
}

pub fn getAttestationParticipationStatus(
comptime fork: ForkSeq,
data: *const types.phase0.AttestationData.Type,
inclusion_delay: u64,
current_epoch: Epoch,
root_cache: *RootCache(fork),
root_cache: anytype,
execution_payload_availability: anytype,
) !u8 {
const justified_checkpoint = if (data.target.epoch == current_epoch)
&root_cache.current_justified_checkpoint
Expand All @@ -150,9 +209,29 @@ pub fn getAttestationParticipationStatus(
const is_matching_target = std.mem.eql(u8, &data.target.root, try root_cache.getBlockRoot(data.target.epoch));

// a timely head is only be set if the target is _also_ matching
const is_matching_head =
var is_matching_head =
is_matching_target and std.mem.eql(u8, &data.beacon_block_root, try root_cache.getBlockRootAtSlot(data.slot));

if (fork.gte(.gloas)) {
var is_matching_payload = false;

if (try isAttestationSameSlotRootCache(root_cache, data)) {
if (data.index != 0) {
return error.AttestingSameSlotMustIndicateEmptyPayload;
}
is_matching_payload = true;
} else {
if (data.index != 0 and data.index != 1) {
return error.DataIndexMustBe0Or1;
}

is_matching_payload =
(data.index != 0) == try execution_payload_availability.get(data.slot % preset.SLOTS_PER_HISTORICAL_ROOT);
}

is_matching_head = is_matching_head and is_matching_payload;
}

var flags: u8 = 0;
if (is_matching_source and inclusion_delay <= SLOTS_PER_EPOCH_SQRT) flags |= TIMELY_SOURCE;
if (is_matching_target and isTimelyTarget(fork, inclusion_delay)) flags |= TIMELY_TARGET;
Expand Down
10 changes: 8 additions & 2 deletions src/state_transition/block/process_attestation_phase0.zig
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,14 @@ pub fn validateAttestation(
}

if (fork.gte(.electra)) {
if (data.index != 0) {
return error.InvalidAttestationNonZeroDataIndex;
if (fork.gte(.gloas)) {
if (data.index >= 2) {
return error.InvalidAttestationDataIndexOutOfRange;
}
} else {
if (data.index != 0) {
return error.InvalidAttestationNonZeroDataIndex;
}
}
var committee_indices_buffer: [preset.MAX_COMMITTEES_PER_SLOT]usize = undefined;
const committee_indices_len = try attestation.committee_bits.getTrueBitIndexes(committee_indices_buffer[0..]);
Expand Down
Loading
Loading