From 4047ab45e0ad027559b21febc26d7b1cc400c38c Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 21 Mar 2026 21:38:18 +0530 Subject: [PATCH 01/30] add constants and configs --- src/config/BeaconConfig.zig | 12 +++++++++++- src/config/ChainConfig.zig | 3 +++ src/config/fork_seq.zig | 3 +++ src/config/networks/chiado.zig | 2 ++ src/config/networks/gnosis.zig | 2 ++ src/config/networks/hoodi.zig | 3 +++ src/config/networks/mainnet.zig | 2 ++ src/config/networks/minimal.zig | 2 ++ src/config/networks/sepolia.zig | 3 +++ src/consensus_types/root.zig | 2 ++ src/constants/root.zig | 13 ++++++++++++- src/fork_types/fork_types.zig | 1 + src/preset/preset.zig | 12 ++++++++++++ 13 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/config/BeaconConfig.zig b/src/config/BeaconConfig.zig index ba835f5c5..c1e09b63e 100644 --- a/src/config/BeaconConfig.zig +++ b/src/config/BeaconConfig.zig @@ -132,6 +132,14 @@ pub fn init(chain_config: ChainConfig, genesis_validator_root: Root) BeaconConfi .prev_fork_seq = ForkSeq.electra, }; + const gloas = ForkInfo{ + .fork_seq = ForkSeq.gloas, + .epoch = chain_config.GLOAS_FORK_EPOCH, + .version = chain_config.GLOAS_FORK_VERSION, + .prev_version = chain_config.FULU_FORK_VERSION, + .prev_fork_seq = ForkSeq.fulu, + }; + const forks_ascending_epoch_order = [ForkSeq.count]ForkInfo{ phase0, altair, @@ -140,8 +148,10 @@ pub fn init(chain_config: ChainConfig, genesis_validator_root: Root) BeaconConfi deneb, electra, fulu, + gloas, }; const forks_descending_epoch_order = [ForkSeq.count]ForkInfo{ + gloas, fulu, electra, deneb, @@ -213,7 +223,7 @@ pub fn getMaxBlobsPerBlock(self: *const BeaconConfig, epoch: Epoch) u64 { return switch (fork) { .deneb => self.chain.MAX_BLOBS_PER_BLOCK, .electra => self.chain.MAX_BLOBS_PER_BLOCK_ELECTRA, - .fulu => { + .fulu, .gloas => { for (0..self.chain.BLOB_SCHEDULE.len) |i| { const schedule = self.chain.BLOB_SCHEDULE[self.chain.BLOB_SCHEDULE.len - i - 1]; if (epoch >= schedule.EPOCH) return schedule.MAX_BLOBS_PER_BLOCK; diff --git a/src/config/ChainConfig.zig b/src/config/ChainConfig.zig index 567f862c6..b73e60e9c 100644 --- a/src/config/ChainConfig.zig +++ b/src/config/ChainConfig.zig @@ -39,6 +39,9 @@ ELECTRA_FORK_EPOCH: u64, // FULU (assuming it's a future fork, standard pattern) FULU_FORK_VERSION: [4]u8, FULU_FORK_EPOCH: u64, +// GLOAS (EIP-7732: Enshrined Proposer-Builder Separation) +GLOAS_FORK_VERSION: [4]u8, +GLOAS_FORK_EPOCH: u64, // Time parameters SECONDS_PER_SLOT: u64, diff --git a/src/config/fork_seq.zig b/src/config/fork_seq.zig index be2fee8ef..bc096f82a 100644 --- a/src/config/fork_seq.zig +++ b/src/config/fork_seq.zig @@ -13,6 +13,7 @@ pub const ForkSeq = enum(u8) { deneb = 4, electra = 5, fulu = 6, + gloas = 7, /// Total number of fork variants. pub const count: u8 = @intCast(@typeInfo(ForkSeq).@"enum".fields.len); @@ -66,6 +67,7 @@ test "fork - ForkSeq.name" { try std.testing.expectEqualSlices(u8, "deneb", ForkSeq.deneb.name()); try std.testing.expectEqualSlices(u8, "electra", ForkSeq.electra.name()); try std.testing.expectEqualSlices(u8, "fulu", ForkSeq.fulu.name()); + try std.testing.expectEqualSlices(u8, "gloas", ForkSeq.gloas.name()); } test "fork - ForkSeq.fromName" { @@ -76,4 +78,5 @@ test "fork - ForkSeq.fromName" { try std.testing.expectEqual(ForkSeq.deneb, ForkSeq.fromName("deneb")); try std.testing.expectEqual(ForkSeq.electra, ForkSeq.fromName("electra")); try std.testing.expectEqual(ForkSeq.fulu, ForkSeq.fromName("fulu")); + try std.testing.expectEqual(ForkSeq.gloas, ForkSeq.fromName("gloas")); } diff --git a/src/config/networks/chiado.zig b/src/config/networks/chiado.zig index c1be23800..23680bb3c 100644 --- a/src/config/networks/chiado.zig +++ b/src/config/networks/chiado.zig @@ -30,6 +30,8 @@ pub const chain_config = gnosis.chain_config.merge(.{ .ELECTRA_FORK_EPOCH = 948224, .FULU_FORK_VERSION = b(4, "0x0600006f"), .FULU_FORK_EPOCH = std.math.maxInt(u64), + .GLOAS_FORK_VERSION = b(4, "0x0700006f"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Deposit contract .DEPOSIT_CHAIN_ID = 10200, diff --git a/src/config/networks/gnosis.zig b/src/config/networks/gnosis.zig index 9571acbb2..d9224631f 100644 --- a/src/config/networks/gnosis.zig +++ b/src/config/networks/gnosis.zig @@ -35,6 +35,8 @@ pub const chain_config = ChainConfig{ .ELECTRA_FORK_EPOCH = 1337856, .FULU_FORK_VERSION = b(4, "0x06000064"), .FULU_FORK_EPOCH = std.math.maxInt(u64), + .GLOAS_FORK_VERSION = b(4, "0x07000064"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Time parameters .SECONDS_PER_SLOT = 5, diff --git a/src/config/networks/hoodi.zig b/src/config/networks/hoodi.zig index 593c61a0a..bf45433b6 100644 --- a/src/config/networks/hoodi.zig +++ b/src/config/networks/hoodi.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const ChainConfig = @import("../ChainConfig.zig"); const BeaconConfig = @import("../BeaconConfig.zig"); const b = @import("hex").hexToBytesComptime; @@ -29,6 +30,8 @@ pub const chain_config = mainnet.chain_config.merge(.{ .ELECTRA_FORK_EPOCH = 2048, .FULU_FORK_VERSION = b(4, "0x70000910"), .FULU_FORK_EPOCH = 50688, + .GLOAS_FORK_VERSION = b(4, "0x80000910"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Time parameters .SECONDS_PER_ETH1_BLOCK = 12, diff --git a/src/config/networks/mainnet.zig b/src/config/networks/mainnet.zig index f181bc379..155b71fa7 100644 --- a/src/config/networks/mainnet.zig +++ b/src/config/networks/mainnet.zig @@ -35,6 +35,8 @@ pub const chain_config = ChainConfig{ .ELECTRA_FORK_EPOCH = 364032, .FULU_FORK_VERSION = b(4, "0x06000000"), .FULU_FORK_EPOCH = 411392, + .GLOAS_FORK_VERSION = b(4, "0x07000000"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Time parameters .SECONDS_PER_SLOT = 12, diff --git a/src/config/networks/minimal.zig b/src/config/networks/minimal.zig index b08770c21..3f9d78056 100644 --- a/src/config/networks/minimal.zig +++ b/src/config/networks/minimal.zig @@ -35,6 +35,8 @@ pub const chain_config = ChainConfig{ .ELECTRA_FORK_EPOCH = std.math.maxInt(u64), .FULU_FORK_VERSION = b(4, "0x06000001"), .FULU_FORK_EPOCH = std.math.maxInt(u64), + .GLOAS_FORK_VERSION = b(4, "0x07000001"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Time parameters .SECONDS_PER_SLOT = 6, diff --git a/src/config/networks/sepolia.zig b/src/config/networks/sepolia.zig index e7ea2c990..5cb6de2c3 100644 --- a/src/config/networks/sepolia.zig +++ b/src/config/networks/sepolia.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const ChainConfig = @import("../ChainConfig.zig"); const BeaconConfig = @import("../BeaconConfig.zig"); const b = @import("hex").hexToBytesComptime; @@ -30,6 +31,8 @@ pub const chain_config = mainnet.chain_config.merge(.{ .ELECTRA_FORK_EPOCH = 222464, .FULU_FORK_VERSION = b(4, "0x90000075"), .FULU_FORK_EPOCH = 272640, + .GLOAS_FORK_VERSION = b(4, "0x90000076"), + .GLOAS_FORK_EPOCH = std.math.maxInt(u64), // Deposit contract .DEPOSIT_CHAIN_ID = 11155111, diff --git a/src/consensus_types/root.zig b/src/consensus_types/root.zig index 648ac6d87..a4b612388 100644 --- a/src/consensus_types/root.zig +++ b/src/consensus_types/root.zig @@ -9,6 +9,7 @@ pub const capella = @import("capella.zig"); pub const deneb = @import("deneb.zig"); pub const electra = @import("electra.zig"); pub const fulu = @import("fulu.zig"); +pub const gloas = @import("gloas.zig"); test { testing.refAllDecls(primitive); @@ -19,6 +20,7 @@ test { testing.refAllDecls(deneb); testing.refAllDecls(electra); testing.refAllDecls(fulu); + testing.refAllDecls(gloas); } const src = blk: { diff --git a/src/constants/root.zig b/src/constants/root.zig index 0038e7bf6..4c1e42bed 100644 --- a/src/constants/root.zig +++ b/src/constants/root.zig @@ -12,6 +12,7 @@ pub const GENESIS_SLOT = 0; pub const BLS_WITHDRAWAL_PREFIX = 0; pub const ETH1_ADDRESS_WITHDRAWAL_PREFIX = 1; pub const COMPOUNDING_WITHDRAWAL_PREFIX = 2; +pub const BUILDER_WITHDRAWAL_PREFIX = 3; // Domain types @@ -26,6 +27,9 @@ pub const DOMAIN_SYNC_COMMITTEE = [_]u8{ 7, 0, 0, 0 }; pub const DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF = [_]u8{ 8, 0, 0, 0 }; pub const DOMAIN_CONTRIBUTION_AND_PROOF = [_]u8{ 9, 0, 0, 0 }; pub const DOMAIN_BLS_TO_EXECUTION_CHANGE = [_]u8{ 10, 0, 0, 0 }; +pub const DOMAIN_BEACON_BUILDER = [_]u8{ 11, 0, 0, 0 }; +pub const DOMAIN_PTC_ATTESTER = [_]u8{ 12, 0, 0, 0 }; +pub const DOMAIN_PROPOSER_PREFERENCES = [_]u8{ 13, 0, 0, 0 }; // Application specific domains @@ -33,7 +37,7 @@ 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_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_APPLICATION_MASK, DOMAIN_APPLICATION_BUILDER }; // Participation flag indices @@ -115,3 +119,10 @@ pub const EXECUTION_PAYLOAD_GINDEX = 25; pub const CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA = 86; pub const G2_POINT_AT_INFINITY: [96]u8 = [_]u8{0xc0} ++ [_]u8{0} ** 95; + +// Gloas (EIP-7732) constants +pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; +pub const BUILDER_INDEX_SELF_BUILD: u64 = std.math.maxInt(u64); +pub const BUILDER_PAYMENT_THRESHOLD_NUMERATOR: u64 = 6; +pub const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR: u64 = 10; +pub const MIN_BUILDER_WITHDRAWABILITY_DELAY: u64 = 64; diff --git a/src/fork_types/fork_types.zig b/src/fork_types/fork_types.zig index e42654a1e..d9850ef2e 100644 --- a/src/fork_types/fork_types.zig +++ b/src/fork_types/fork_types.zig @@ -10,5 +10,6 @@ pub fn ForkTypes(comptime fork: ForkSeq) type { .deneb => ct.deneb, .electra => ct.electra, .fulu => ct.fulu, + .gloas => ct.gloas, }; } diff --git a/src/preset/preset.zig b/src/preset/preset.zig index 054d02aa0..3367e917e 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -83,6 +83,12 @@ const PresetMainnet = struct { pub const DEPOSIT_CONTRACT_TREE_DEPTH = 32; pub const GENESIS_SLOT = 0; pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16; + // Gloas (EIP-7732) + pub const PTC_SIZE = 512; + pub const MAX_PAYLOAD_ATTESTATIONS = 4; + pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 + pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576; // 2^20 + pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16_384; // 2^14 }; const PresetMinimal = struct { @@ -156,6 +162,12 @@ 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; + // Gloas (EIP-7732) + pub const PTC_SIZE = 512; + pub const MAX_PAYLOAD_ATTESTATIONS = 4; + pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 + pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576; // 2^20 + pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16_384; // 2^14 }; const preset_str = @import("build_options").preset; From cd7986d744843f19316bce53a3e61f54744cf1c4 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sun, 22 Mar 2026 10:55:28 +0530 Subject: [PATCH 02/30] sdd ssz types --- src/consensus_types/gloas.zig | 250 ++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/consensus_types/gloas.zig diff --git a/src/consensus_types/gloas.zig b/src/consensus_types/gloas.zig new file mode 100644 index 000000000..6f35d9049 --- /dev/null +++ b/src/consensus_types/gloas.zig @@ -0,0 +1,250 @@ +const ssz = @import("ssz"); +const p = @import("primitive.zig"); +const c = @import("constants"); +const preset = @import("preset").preset; +const phase0 = @import("phase0.zig"); +const altair = @import("altair.zig"); +const bellatrix = @import("bellatrix.zig"); +const capella = @import("capella.zig"); +const deneb = @import("deneb.zig"); +const electra = @import("electra.zig"); +const fulu = @import("fulu.zig"); + +pub const Fork = phase0.Fork; +pub const ForkData = phase0.ForkData; +pub const Checkpoint = phase0.Checkpoint; +pub const Validator = phase0.Validator; +pub const Validators = phase0.Validators; +pub const AttestationData = phase0.AttestationData; +pub const PendingAttestation = phase0.PendingAttestation; +pub const Eth1Data = phase0.Eth1Data; +pub const Eth1DataVotes = phase0.Eth1DataVotes; +pub const HistoricalBatch = phase0.HistoricalBatch; +pub const DepositMessage = phase0.DepositMessage; +pub const DepositData = phase0.DepositData; +pub const BeaconBlockHeader = phase0.BeaconBlockHeader; +pub const SigningData = phase0.SigningData; +pub const ProposerSlashing = phase0.ProposerSlashing; +pub const Deposit = phase0.Deposit; +pub const VoluntaryExit = phase0.VoluntaryExit; +pub const SignedVoluntaryExit = phase0.SignedVoluntaryExit; +pub const Eth1Block = phase0.Eth1Block; +pub const HistoricalBlockRoots = phase0.HistoricalBlockRoots; +pub const HistoricalStateRoots = phase0.HistoricalStateRoots; +pub const ProposerSlashings = phase0.ProposerSlashings; +pub const Deposits = phase0.Deposits; +pub const VoluntaryExits = phase0.VoluntaryExits; +pub const Slashings = phase0.Slashings; +pub const Balances = phase0.Balances; +pub const RandaoMixes = phase0.RandaoMixes; + +pub const SyncAggregate = altair.SyncAggregate; +pub const SyncCommittee = altair.SyncCommittee; +pub const SyncCommitteeMessage = altair.SyncCommitteeMessage; +pub const SyncCommitteeContribution = altair.SyncCommitteeContribution; +pub const ContributionAndProof = altair.ContributionAndProof; +pub const SignedContributionAndProof = altair.SignedContributionAndProof; +pub const SyncAggregatorSelectionData = altair.SyncAggregatorSelectionData; + +pub const PowBlock = bellatrix.PowBlock; + +pub const Withdrawal = capella.Withdrawal; +pub const Withdrawals = capella.Withdrawals; +pub const BLSToExecutionChange = capella.BLSToExecutionChange; +pub const SignedBLSToExecutionChange = capella.SignedBLSToExecutionChange; +pub const SignedBLSToExecutionChanges = capella.SignedBLSToExecutionChanges; +pub const HistoricalSummary = capella.HistoricalSummary; + +pub const BlobIdentifier = deneb.BlobIdentifier; +pub const BlobKzgCommitments = deneb.BlobKzgCommitments; + +pub const PendingDeposit = electra.PendingDeposit; +pub const PendingPartialWithdrawal = electra.PendingPartialWithdrawal; +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; +pub const IndexedAttestation = electra.IndexedAttestation; +pub const AttesterSlashing = electra.AttesterSlashing; +pub const AttesterSlashings = electra.AttesterSlashings; +pub const AggregateAndProof = electra.AggregateAndProof; +pub const SignedAggregateAndProof = electra.SignedAggregateAndProof; +pub const SignedBeaconBlockHeader = electra.SignedBeaconBlockHeader; + +pub const ExecutionPayload = electra.ExecutionPayload; +pub const ExecutionPayloadHeader = electra.ExecutionPayloadHeader; + +pub const RowIndex = fulu.RowIndex; +pub const ColumnIndex = fulu.ColumnIndex; +pub const CustodyIndex = fulu.CustodyIndex; +pub const Cell = fulu.Cell; +pub const DataColumnSidecar = fulu.DataColumnSidecar; +pub const MatrixEntry = fulu.MatrixEntry; + +pub const LightClientHeader = electra.LightClientHeader; +pub const LightClientBootstrap = electra.LightClientBootstrap; +pub const LightClientUpdate = electra.LightClientUpdate; +pub const LightClientFinalityUpdate = electra.LightClientFinalityUpdate; +pub const LightClientOptimisticUpdate = electra.LightClientOptimisticUpdate; + +pub const ProposerLookahead = fulu.ProposerLookahead; + +pub const BlobSidecar = electra.BlobSidecar; + +pub const Builder = ssz.FixedContainerType(struct { + pubkey: p.BLSPubkey, + version: p.Uint8, + execution_address: p.ExecutionAddress, + balance: p.Gwei, + deposit_epoch: p.Epoch, + withdrawable_epoch: p.Epoch, +}); + +pub const BuilderPendingWithdrawal = ssz.FixedContainerType(struct { + fee_recipient: p.ExecutionAddress, + amount: p.Gwei, + builder_index: p.Uint64, +}); + +pub const BuilderPendingPayment = ssz.FixedContainerType(struct { + weight: p.Gwei, + withdrawal: BuilderPendingWithdrawal, +}); + +pub const PayloadAttestationData = ssz.FixedContainerType(struct { + beacon_block_root: p.Root, + slot: p.Slot, + payload_present: p.Boolean, + blob_data_available: p.Boolean, +}); + +pub const PayloadAttestation = ssz.FixedContainerType(struct { + aggregation_bits: ssz.BitVectorType(preset.PTC_SIZE), + data: PayloadAttestationData, + signature: p.BLSSignature, +}); + +pub const PayloadAttestationMessage = ssz.FixedContainerType(struct { + validator_index: p.ValidatorIndex, + data: PayloadAttestationData, + signature: p.BLSSignature, +}); + +pub const IndexedPayloadAttestation = ssz.VariableContainerType(struct { + attesting_indices: ssz.FixedListType(p.ValidatorIndex, preset.PTC_SIZE), + data: PayloadAttestationData, + signature: p.BLSSignature, +}); + +pub const ExecutionPayloadBid = ssz.VariableContainerType(struct { + parent_block_hash: p.Bytes32, + parent_block_root: p.Root, + block_hash: p.Bytes32, + prev_randao: p.Bytes32, + fee_recipient: p.ExecutionAddress, + gas_limit: p.Uint64, + builder_index: p.Uint64, + slot: p.Slot, + value: p.Gwei, + execution_payment: p.Gwei, + blob_kzg_commitments: ssz.FixedListType(p.KZGCommitment, preset.MAX_BLOB_COMMITMENTS_PER_BLOCK), +}); + +pub const SignedExecutionPayloadBid = ssz.VariableContainerType(struct { + message: ExecutionPayloadBid, + signature: p.BLSSignature, +}); + +pub const ExecutionPayloadEnvelope = ssz.VariableContainerType(struct { + payload: ExecutionPayload, + execution_requests: ExecutionRequests, + builder_index: p.Uint64, + beacon_block_root: p.Root, + slot: p.Slot, + state_root: p.Root, +}); + +pub const SignedExecutionPayloadEnvelope = ssz.VariableContainerType(struct { + message: ExecutionPayloadEnvelope, + signature: p.BLSSignature, +}); + +pub const BeaconBlockBody = ssz.VariableContainerType(struct { + randao_reveal: p.BLSSignature, + eth1_data: Eth1Data, + graffiti: p.Bytes32, + proposer_slashings: ProposerSlashings, + attester_slashings: AttesterSlashings, + attestations: Attestations, + deposits: Deposits, + voluntary_exits: VoluntaryExits, + sync_aggregate: SyncAggregate, + bls_to_execution_changes: SignedBLSToExecutionChanges, + signed_execution_payload_bid: SignedExecutionPayloadBid, + payload_attestations: ssz.FixedListType(PayloadAttestation, preset.MAX_PAYLOAD_ATTESTATIONS), +}); + +pub const BeaconBlock = ssz.VariableContainerType(struct { + slot: p.Slot, + proposer_index: p.ValidatorIndex, + parent_root: p.Root, + state_root: p.Root, + body: BeaconBlockBody, +}); + +pub const SignedBeaconBlock = ssz.VariableContainerType(struct { + message: BeaconBlock, + signature: p.BLSSignature, +}); + +pub const BeaconState = ssz.VariableContainerType(struct { + genesis_time: p.Uint64, + genesis_validators_root: p.Root, + slot: p.Slot, + fork: Fork, + latest_block_header: BeaconBlockHeader, + block_roots: HistoricalBlockRoots, + state_roots: HistoricalStateRoots, + historical_roots: ssz.FixedListType(p.Root, preset.HISTORICAL_ROOTS_LIMIT), + eth1_data: Eth1Data, + eth1_data_votes: phase0.Eth1DataVotes, + eth1_deposit_index: p.Uint64, + validators: ssz.FixedListType(Validator, preset.VALIDATOR_REGISTRY_LIMIT), + balances: ssz.FixedListType(p.Gwei, preset.VALIDATOR_REGISTRY_LIMIT), + randao_mixes: ssz.FixedVectorType(p.Bytes32, preset.EPOCHS_PER_HISTORICAL_VECTOR), + slashings: ssz.FixedVectorType(p.Gwei, preset.EPOCHS_PER_SLASHINGS_VECTOR), + previous_epoch_participation: ssz.FixedListType(p.Uint8, preset.VALIDATOR_REGISTRY_LIMIT), + current_epoch_participation: ssz.FixedListType(p.Uint8, preset.VALIDATOR_REGISTRY_LIMIT), + justification_bits: ssz.BitVectorType(c.JUSTIFICATION_BITS_LENGTH), + previous_justified_checkpoint: Checkpoint, + current_justified_checkpoint: Checkpoint, + finalized_checkpoint: Checkpoint, + inactivity_scores: ssz.FixedListType(p.Uint64, preset.VALIDATOR_REGISTRY_LIMIT), + current_sync_committee: SyncCommittee, + next_sync_committee: SyncCommittee, + latest_execution_payload_bid: ExecutionPayloadBid, + next_withdrawal_index: p.WithdrawalIndex, + next_withdrawal_validator_index: p.ValidatorIndex, + historical_summaries: ssz.FixedListType(HistoricalSummary, preset.HISTORICAL_ROOTS_LIMIT), + deposit_requests_start_index: p.Uint64, + deposit_balance_to_consume: p.Gwei, + exit_balance_to_consume: p.Gwei, + earliest_exit_epoch: p.Epoch, + consolidation_balance_to_consume: p.Gwei, + earliest_consolidation_epoch: p.Epoch, + pending_deposits: ssz.FixedListType(PendingDeposit, preset.PENDING_DEPOSITS_LIMIT), + pending_partial_withdrawals: ssz.FixedListType(PendingPartialWithdrawal, preset.PENDING_PARTIAL_WITHDRAWALS_LIMIT), + pending_consolidations: ssz.FixedListType(PendingConsolidation, preset.PENDING_CONSOLIDATIONS_LIMIT), + proposer_lookahead: ProposerLookahead, + builders: ssz.FixedListType(Builder, preset.BUILDER_REGISTRY_LIMIT), + next_withdrawal_builder_index: p.Uint64, + execution_payload_availability: ssz.BitVectorType(preset.SLOTS_PER_HISTORICAL_ROOT), + builder_pending_payments: ssz.FixedVectorType(BuilderPendingPayment, 2 * preset.SLOTS_PER_EPOCH), + builder_pending_withdrawals: ssz.FixedListType(BuilderPendingWithdrawal, preset.BUILDER_PENDING_WITHDRAWALS_LIMIT), + latest_block_hash: p.Bytes32, + payload_expected_withdrawals: ssz.FixedListType(Withdrawal, preset.MAX_WITHDRAWALS_PER_PAYLOAD), +}); From bbf5e83eec957f38175e226b4d4d105fd5c8a2b9 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 23 Mar 2026 21:05:44 +0530 Subject: [PATCH 03/30] add fork upgrade, utils, and shared helpers --- src/fork_types/any_beacon_state.zig | 33 ++++- src/fork_types/beacon_state.zig | 6 +- .../block/process_deposit.zig | 11 ++ .../block/process_deposit_request.zig | 71 +++++++++ .../slot/upgrade_state_to_gloas.zig | 101 +++++++++++++ src/state_transition/state_transition.zig | 9 ++ src/state_transition/utils/gloas.zig | 140 ++++++++++++++++++ 7 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 src/state_transition/slot/upgrade_state_to_gloas.zig create mode 100644 src/state_transition/utils/gloas.zig diff --git a/src/fork_types/any_beacon_state.zig b/src/fork_types/any_beacon_state.zig index fc5577a40..7c241e7e0 100644 --- a/src/fork_types/any_beacon_state.zig +++ b/src/fork_types/any_beacon_state.zig @@ -29,6 +29,7 @@ pub const AnyBeaconState = union(ForkSeq) { deneb: *ct.deneb.BeaconState.TreeView, electra: *ct.electra.BeaconState.TreeView, fulu: *ct.fulu.BeaconState.TreeView, + gloas: *ct.gloas.BeaconState.TreeView, pub fn fromValue(allocator: Allocator, pool: *Node.Pool, comptime fork_seq: ForkSeq, value: anytype) !AnyBeaconState { return switch (fork_seq) { @@ -53,6 +54,9 @@ pub const AnyBeaconState = union(ForkSeq) { .fulu => .{ .fulu = try ct.fulu.BeaconState.TreeView.fromValue(allocator, pool, value), }, + .gloas => .{ + .gloas = try ct.gloas.BeaconState.TreeView.fromValue(allocator, pool, value), + }, }; } @@ -79,6 +83,9 @@ pub const AnyBeaconState = union(ForkSeq) { .fulu => .{ .fulu = try ct.fulu.BeaconState.TreeView.deserialize(allocator, pool, bytes), }, + .gloas => .{ + .gloas = try ct.gloas.BeaconState.TreeView.deserialize(allocator, pool, bytes), + }, }; } @@ -127,6 +134,12 @@ pub const AnyBeaconState = union(ForkSeq) { _ = try state.serializeIntoBytes(out); return out; }, + .gloas => |state| { + const out = try allocator.alloc(u8, try state.serializedSize()); + errdefer allocator.free(out); + _ = try state.serializeIntoBytes(out); + return out; + }, } } @@ -154,6 +167,7 @@ pub const AnyBeaconState = union(ForkSeq) { .deneb => |state| .{ .deneb = try state.clone(opts) }, .electra => |state| .{ .electra = try state.clone(opts) }, .fulu => |state| .{ .fulu = try state.clone(opts) }, + .gloas => |state| .{ .gloas = try state.clone(opts) }, }; } @@ -180,7 +194,7 @@ pub const AnyBeaconState = union(ForkSeq) { /// Get a Merkle proof for the finalized root in the beacon state. pub fn getFinalizedRootProof(self: *AnyBeaconState, allocator: Allocator) !SingleProof { const gindex_value: u64 = switch (self.*) { - .electra, .fulu => constants.FINALIZED_ROOT_GINDEX_ELECTRA, + .electra, .fulu, .gloas => constants.FINALIZED_ROOT_GINDEX_ELECTRA, else => constants.FINALIZED_ROOT_GINDEX, }; return self.getSingleProof(allocator, gindex_value); @@ -627,12 +641,17 @@ pub const AnyBeaconState = union(ForkSeq) { out.* = .{ .deneb = undefined }; try state.getValue(allocator, "latest_execution_payload_header", &out.deneb); }, + .gloas => return error.InvalidAtFork, }; } pub fn latestExecutionPayloadHeaderBlockHash(self: *AnyBeaconState) !*const [32]u8 { return switch (self.*) { .phase0, .altair => error.InvalidAtFork, + .gloas => |state| { + var bid = try state.get("latest_execution_payload_bid"); + return try bid.getFieldRoot("block_hash"); + }, inline else => |state| { var header = try state.get("latest_execution_payload_header"); return try header.getFieldRoot("block_hash"); @@ -647,6 +666,7 @@ pub const AnyBeaconState = union(ForkSeq) { .deneb => |state| try state.setValue("latest_execution_payload_header", &header.deneb), .electra => |state| try state.setValue("latest_execution_payload_header", &header.deneb), .fulu => |state| try state.setValue("latest_execution_payload_header", &header.deneb), + .gloas => return error.InvalidAtFork, else => return error.InvalidAtFork, } } @@ -937,7 +957,16 @@ pub const AnyBeaconState = union(ForkSeq) { state, ), }, - .fulu => error.InvalidAtFork, + .fulu => |state| .{ + .gloas = try populateFields( + ct.fulu.BeaconState, + ct.gloas.BeaconState, + state.allocator, + state.pool, + state, + ), + }, + .gloas => error.InvalidAtFork, }; } }; diff --git a/src/fork_types/beacon_state.zig b/src/fork_types/beacon_state.zig index b716bf429..b4caccc78 100644 --- a/src/fork_types/beacon_state.zig +++ b/src/fork_types/beacon_state.zig @@ -481,7 +481,8 @@ pub fn BeaconState(comptime f: ForkSeq) type { .capella => .deneb, .deneb => .electra, .electra => .fulu, - .fulu => .fulu, + .fulu => .gloas, + .gloas => .gloas, }) { const cur = self.inner; const allocator = cur.allocator; @@ -494,7 +495,8 @@ pub fn BeaconState(comptime f: ForkSeq) type { .capella => .{ .inner = try populateFields(ForkTypes(.capella).BeaconState, ForkTypes(.deneb).BeaconState, allocator, pool, cur) }, .deneb => .{ .inner = try populateFields(ForkTypes(.deneb).BeaconState, ForkTypes(.electra).BeaconState, allocator, pool, cur) }, .electra => .{ .inner = try populateFields(ForkTypes(.electra).BeaconState, ForkTypes(.fulu).BeaconState, allocator, pool, cur) }, - .fulu => return error.InvalidAtFork, + .fulu => .{ .inner = try populateFields(ForkTypes(.fulu).BeaconState, ForkTypes(.gloas).BeaconState, allocator, pool, cur) }, + .gloas => return error.InvalidAtFork, }; } }; diff --git a/src/state_transition/block/process_deposit.zig b/src/state_transition/block/process_deposit.zig index a12c980d7..517515304 100644 --- a/src/state_transition/block/process_deposit.zig +++ b/src/state_transition/block/process_deposit.zig @@ -226,3 +226,14 @@ pub fn validateDepositSignature( try signature.validate(true); try verify(&signing_root, &public_key, &signature, null, null); } + +pub fn isValidDepositSignature( + config: *const BeaconConfig, + pubkey: *const BLSPubkey, + withdrawal_credentials: *const WithdrawalCredentials, + amount: u64, + signature: BLSSignature, +) bool { + validateDepositSignature(config, pubkey, withdrawal_credentials, amount, signature) catch return false; + return true; +} diff --git a/src/state_transition/block/process_deposit_request.zig b/src/state_transition/block/process_deposit_request.zig index 175fb5b2c..2b75385ba 100644 --- a/src/state_transition/block/process_deposit_request.zig +++ b/src/state_transition/block/process_deposit_request.zig @@ -1,9 +1,18 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; const ForkSeq = @import("config").ForkSeq; const BeaconState = @import("fork_types").BeaconState; const types = @import("consensus_types"); const DepositRequest = types.electra.DepositRequest.Type; const PendingDeposit = types.electra.PendingDeposit.Type; +const Builder = types.gloas.Builder; +const BLSPubkey = types.primitive.BLSPubkey.Type; +const BLSSignature = types.primitive.BLSSignature.Type; const c = @import("constants"); +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const findBuilderIndexByPubkey = @import("../utils/gloas.zig").findBuilderIndexByPubkey; +const isValidDepositSignature = @import("./process_deposit.zig").isValidDepositSignature; pub fn processDepositRequest(comptime fork: ForkSeq, state: *BeaconState(fork), deposit_request: *const DepositRequest) !void { const deposit_requests_start_index = try state.depositRequestsStartIndex(); @@ -22,3 +31,65 @@ pub fn processDepositRequest(comptime fork: ForkSeq, state: *BeaconState(fork), var pending_deposits = try state.pendingDeposits(); try pending_deposits.pushValue(&pending_deposit); } + +pub fn applyDepositForBuilder( + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(.gloas), + pubkey: *const BLSPubkey, + withdrawal_credentials: *const [32]u8, + amount: u64, + signature: BLSSignature, + slot: u64, +) !void { + const builderIndex = try findBuilderIndexByPubkey(allocator, state, pubkey); + + if (builderIndex) |idx| { + var builders = try state.inner.get("builders"); + var existing: Builder.Type = undefined; + try builders.getValue(allocator, idx, &existing); + existing.balance += amount; + try builders.setValue(idx, &existing); + } else { + if (!isValidDepositSignature(config, pubkey, withdrawal_credentials, amount, signature)) return; + try addBuilderToRegistry(allocator, state, pubkey, withdrawal_credentials, amount, slot); + } +} + +fn addBuilderToRegistry( + allocator: Allocator, + state: *BeaconState(.gloas), + pubkey: *const BLSPubkey, + withdrawal_credentials: *const [32]u8, + amount: u64, + slot: u64, +) !void { + var builders = try state.inner.get("builders"); + const len = try builders.length(); + const current_epoch = computeEpochAtSlot(try state.slot()); + + var builderIndex: ?usize = null; + var it = builders.iteratorReadonly(0); + for (0..len) |i| { + const b = try it.nextValue(allocator); + if (b.withdrawable_epoch <= current_epoch and b.balance == 0) { + builderIndex = i; + break; + } + } + + const new_builder = Builder.Type{ + .pubkey = pubkey.*, + .version = withdrawal_credentials[0], + .execution_address = withdrawal_credentials[12..32].*, + .balance = amount, + .deposit_epoch = computeEpochAtSlot(slot), + .withdrawable_epoch = c.FAR_FUTURE_EPOCH, + }; + + if (builderIndex) |idx| { + try builders.setValue(idx, &new_builder); + } else { + try builders.pushValue(&new_builder); + } +} diff --git a/src/state_transition/slot/upgrade_state_to_gloas.zig b/src/state_transition/slot/upgrade_state_to_gloas.zig new file mode 100644 index 000000000..7546320e5 --- /dev/null +++ b/src/state_transition/slot/upgrade_state_to_gloas.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const BeaconState = @import("fork_types").BeaconState; +const ct = @import("consensus_types"); +const preset = @import("preset").preset; + +const ExecutionPayloadBid = ct.gloas.ExecutionPayloadBid; +const PendingDeposit = ct.electra.PendingDeposit.Type; +const BLSPubkey = ct.primitive.BLSPubkey.Type; +const BitVector = @import("ssz").BitVector; +const ExecPayloadAvailability = BitVector(preset.SLOTS_PER_HISTORICAL_ROOT); +const isValidatorKnown = @import("../utils/electra.zig").isValidatorKnown; +const isValidDepositSignature = @import("../block/process_deposit.zig").isValidDepositSignature; +const applyDepositForBuilder = @import("../block/process_deposit_request.zig").applyDepositForBuilder; +const gloas_utils = @import("../utils/gloas.zig"); +const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; +const isBuilderWithdrawalCredential = gloas_utils.isBuilderWithdrawalCredential; +const isPubkeyInList = gloas_utils.isPubkeyInList; + +pub fn upgradeStateToGloas( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + fulu_state: *BeaconState(.fulu), +) !BeaconState(.gloas) { + const block_hash_ptr = try fulu_state.latestExecutionPayloadHeaderBlockHash(); + var block_hash: [32]u8 = undefined; + @memcpy(&block_hash, block_hash_ptr); + + var state = try fulu_state.upgradeUnsafe(); + errdefer state.deinit(); + + const new_fork = ct.phase0.Fork.Type{ + .previous_version = try fulu_state.forkCurrentVersion(), + .current_version = config.chain.GLOAS_FORK_VERSION, + .epoch = epoch_cache.epoch, + }; + try state.setFork(&new_fork); + + var bid = ExecutionPayloadBid.default_value; + bid.block_hash = block_hash; + try state.inner.setValue("latest_execution_payload_bid", &bid); + + try state.inner.setValue("latest_block_hash", &block_hash); + + const availability = ExecPayloadAvailability{ .data = [_]u8{0xFF} ** @divExact(ExecPayloadAvailability.length, 8) }; + try state.inner.setValue("execution_payload_availability", &availability); + + try onboardBuildersFromPendingDeposits(allocator, config, epoch_cache, &state); + + fulu_state.deinit(); + return state; +} + +fn onboardBuildersFromPendingDeposits( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), +) !void { + var remaining_pending_deposits = std.ArrayList(PendingDeposit).init(allocator); + defer remaining_pending_deposits.deinit(); + + var new_validator_pubkeys = std.ArrayList(BLSPubkey).init(allocator); + defer new_validator_pubkeys.deinit(); + + var pending_deposits = try state.pendingDeposits(); + const pending_deposits_len = try pending_deposits.length(); + var pending_it = pending_deposits.iteratorReadonly(0); + + for (0..pending_deposits_len) |_| { + const deposit = try pending_it.nextValue(allocator); + + const validator_index = epoch_cache.getValidatorIndex(&deposit.pubkey); + if ((try isValidatorKnown(.gloas, state, validator_index)) or + isPubkeyInList(new_validator_pubkeys.items, &deposit.pubkey)) + { + try remaining_pending_deposits.append(deposit); + continue; + } + + const is_existing_builder = (try findBuilderIndexByPubkey(allocator, state, &deposit.pubkey)) != null; + if (is_existing_builder or isBuilderWithdrawalCredential(&deposit.withdrawal_credentials)) { + try applyDepositForBuilder(allocator, config, state, &deposit.pubkey, &deposit.withdrawal_credentials, deposit.amount, deposit.signature, deposit.slot); + continue; + } + + if (isValidDepositSignature(config, &deposit.pubkey, &deposit.withdrawal_credentials, deposit.amount, deposit.signature)) { + try new_validator_pubkeys.append(deposit.pubkey); + try remaining_pending_deposits.append(deposit); + } + } + + var new_pending = try pending_deposits.sliceFrom(pending_deposits_len); + for (remaining_pending_deposits.items) |dep| { + try new_pending.pushValue(&dep); + } + try state.setPendingDeposits(new_pending); +} diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index 638e3c84c..0d70e1241 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -30,6 +30,7 @@ const upgradeStateToCapella = @import("slot/upgrade_state_to_capella.zig").upgra const upgradeStateToDeneb = @import("slot/upgrade_state_to_deneb.zig").upgradeStateToDeneb; const upgradeStateToElectra = @import("slot/upgrade_state_to_electra.zig").upgradeStateToElectra; const upgradeStateToFulu = @import("slot/upgrade_state_to_fulu.zig").upgradeStateToFulu; +const upgradeStateToGloas = @import("slot/upgrade_state_to_gloas.zig").upgradeStateToGloas; pub const ExecutionPayloadStatus = enum(u8) { pre_merge, @@ -145,6 +146,14 @@ pub fn processSlots( try state.tryCastToFork(.electra), )).inner }; } + if (state_epoch == config.chain.GLOAS_FORK_EPOCH) { + state.* = .{ .gloas = (try upgradeStateToGloas( + allocator, + config, + epoch_cache, + try state.tryCastToFork(.fulu), + )).inner }; + } try epoch_cache.finalProcessEpoch(state); metrics.state_transition.epoch_transition.observe(readSeconds(&epoch_transition_timer)); diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig new file mode 100644 index 000000000..1fd2377d7 --- /dev/null +++ b/src/state_transition/utils/gloas.zig @@ -0,0 +1,140 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconState = @import("fork_types").BeaconState; +const ct = @import("consensus_types"); +const preset = @import("preset").preset; +const c = @import("constants"); +const getBlockRootAtSlot = @import("./block_root.zig").getBlockRootAtSlot; +const computeEpochAtSlot = @import("./epoch.zig").computeEpochAtSlot; +const RootCache = @import("../cache/root_cache.zig").RootCache; + +const BLSPubkey = ct.primitive.BLSPubkey.Type; + +pub fn isBuilderWithdrawalCredential(withdrawal_credentials: *const [32]u8) bool { + return withdrawal_credentials[0] == c.BUILDER_WITHDRAWAL_PREFIX; +} + +pub fn getBuilderPaymentQuorumThreshold(total_active_balance_increments: u64) u64 { + const quorum = (total_active_balance_increments * preset.EFFECTIVE_BALANCE_INCREMENT / preset.SLOTS_PER_EPOCH) * + c.BUILDER_PAYMENT_THRESHOLD_NUMERATOR; + return quorum / c.BUILDER_PAYMENT_THRESHOLD_DENOMINATOR; +} + +fn hasBuilderIndexFlag(index: u64) bool { + return (index & c.BUILDER_INDEX_FLAG) != 0; +} + +pub fn isBuilderIndex(validator_index: u64) bool { + return hasBuilderIndexFlag(validator_index); +} + +pub fn convertBuilderIndexToValidatorIndex(builder_index: u64) u64 { + return if (hasBuilderIndexFlag(builder_index)) builder_index else builder_index | c.BUILDER_INDEX_FLAG; +} + +pub fn convertValidatorIndexToBuilderIndex(validator_index: u64) u64 { + return if (hasBuilderIndexFlag(validator_index)) validator_index & ~c.BUILDER_INDEX_FLAG else validator_index; +} + +pub fn isActiveBuilder(builder: *const ct.gloas.Builder.Type, finalized_epoch: u64) bool { + return builder.deposit_epoch < finalized_epoch and builder.withdrawable_epoch == c.FAR_FUTURE_EPOCH; +} + +pub fn getPendingBalanceToWithdrawForBuilder(allocator: Allocator, state: *BeaconState(.gloas), builder_index: u64) !u64 { + var pending_balance: u64 = 0; + + var withdrawals = try state.inner.get("builder_pending_withdrawals"); + const withdrawals_len = try withdrawals.length(); + var w_it = withdrawals.iteratorReadonly(0); + for (0..withdrawals_len) |_| { + const w = try w_it.nextValue(allocator); + if (w.builder_index == builder_index) { + pending_balance += w.amount; + } + } + + var payments = try state.inner.get("builder_pending_payments"); + const payments_len = try payments.length(); + var p_it = payments.iteratorReadonly(0); + for (0..payments_len) |_| { + const p = try p_it.nextValue(allocator); + if (p.withdrawal.builder_index == builder_index) { + pending_balance += p.withdrawal.amount; + } + } + + return pending_balance; +} + +pub fn canBuilderCoverBid(allocator: Allocator, state: *BeaconState(.gloas), builder_index: u64, bid_amount: u64) !bool { + var builders = try state.inner.get("builders"); + var builder: ct.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + const pending_balance = try getPendingBalanceToWithdrawForBuilder(allocator, state, builder_index); + const min_balance = preset.MIN_DEPOSIT_AMOUNT + pending_balance; + + if (builder.balance < min_balance) return false; + return builder.balance - min_balance >= bid_amount; +} + +pub fn initiateBuilderExit(state: *BeaconState(.gloas), allocator: Allocator, builder_index: u64) !void { + var builders = try state.inner.get("builders"); + var builder: ct.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + if (builder.withdrawable_epoch != c.FAR_FUTURE_EPOCH) return; + + const current_epoch = computeEpochAtSlot(try state.slot()); + builder.withdrawable_epoch = current_epoch + c.MIN_BUILDER_WITHDRAWABILITY_DELAY; + try builders.setValue(builder_index, &builder); +} + +pub fn findBuilderIndexByPubkey(allocator: Allocator, state: *BeaconState(.gloas), pubkey: *const BLSPubkey) !?usize { + var builders = try state.inner.get("builders"); + const len = try builders.length(); + var it = builders.iteratorReadonly(0); + for (0..len) |i| { + const b = try it.nextValue(allocator); + if (std.mem.eql(u8, &b.pubkey, pubkey)) return i; + } + return null; +} + +pub fn isAttestationSameSlot(state: *BeaconState(.gloas), data: *const ct.phase0.AttestationData.Type) !bool { + if (data.slot == 0) return true; + + const block_root = try getBlockRootAtSlot(.gloas, state, data.slot); + const is_matching = std.mem.eql(u8, &data.beacon_block_root, block_root); + + const prev_block_root = try getBlockRootAtSlot(.gloas, state, data.slot - 1); + const is_current = !std.mem.eql(u8, &data.beacon_block_root, prev_block_root); + + return is_matching and is_current; +} + +pub fn isAttestationSameSlotRootCache(root_cache: *RootCache(.gloas), data: *const ct.phase0.AttestationData.Type) !bool { + if (data.slot == 0) return true; + + const block_root = try root_cache.getBlockRootAtSlot(data.slot); + const is_matching = std.mem.eql(u8, &data.beacon_block_root, block_root); + + const prev_block_root = try root_cache.getBlockRootAtSlot(data.slot - 1); + const is_current = !std.mem.eql(u8, &data.beacon_block_root, prev_block_root); + + return is_matching and is_current; +} + +pub fn isParentBlockFull(state: *BeaconState(.gloas)) !bool { + var bid = try state.inner.get("latest_execution_payload_bid"); + const bid_block_hash = try bid.getFieldRoot("block_hash"); + const latest_block_hash = try state.inner.getFieldRoot("latest_block_hash"); + return std.mem.eql(u8, bid_block_hash, latest_block_hash); +} + +pub fn isPubkeyInList(list: []const BLSPubkey, pubkey: *const BLSPubkey) bool { + for (list) |p| { + if (std.mem.eql(u8, &p, pubkey)) return true; + } + return false; +} From 542f373313785b660073eb739281fa7a5a1c1bb3 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 24 Mar 2026 13:26:36 +0530 Subject: [PATCH 04/30] fix gloas wiring --- src/fork_types/any_beacon_block.zig | 44 +++++++++++++++---- src/fork_types/beacon_block.zig | 12 +++-- src/fork_types/beacon_state.zig | 6 +-- src/state_transition/block/process_block.zig | 30 ++++++++----- .../block/process_operations.zig | 9 +++- .../block/slash_validator.zig | 4 +- src/state_transition/epoch/process_epoch.zig | 7 +++ src/state_transition/slot/process_slot.zig | 12 +++++ src/state_transition/state_transition.zig | 2 +- src/state_transition/utils/execution.zig | 1 + src/state_transition/utils/gloas.zig | 9 ++-- 11 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/fork_types/any_beacon_block.zig b/src/fork_types/any_beacon_block.zig index 9208f9a33..d0caefd13 100644 --- a/src/fork_types/any_beacon_block.zig +++ b/src/fork_types/any_beacon_block.zig @@ -34,6 +34,7 @@ pub const AnySignedBeaconBlock = union(enum) { blinded_electra: *ct.electra.SignedBlindedBeaconBlock.Type, full_fulu: *ct.fulu.SignedBeaconBlock.Type, blinded_fulu: *ct.fulu.SignedBlindedBeaconBlock.Type, + full_gloas: *ct.gloas.SignedBeaconBlock.Type, pub fn deserialize(allocator: std.mem.Allocator, block_type: BlockType, fork_seq: ForkSeq, bytes: []const u8) !AnySignedBeaconBlock { switch (fork_seq) { @@ -128,6 +129,14 @@ pub const AnySignedBeaconBlock = union(enum) { return .{ .blinded_fulu = signed_block }; } }, + .gloas => { + if (block_type != .full) return error.InvalidBlockTypeForFork; + const signed_block = try allocator.create(ct.gloas.SignedBeaconBlock.Type); + errdefer allocator.destroy(signed_block); + signed_block.* = ct.gloas.SignedBeaconBlock.default_value; + try ct.gloas.SignedBeaconBlock.deserializeFromBytes(allocator, bytes, signed_block); + return .{ .full_gloas = signed_block }; + }, } } @@ -181,6 +190,10 @@ pub const AnySignedBeaconBlock = union(enum) { ct.fulu.SignedBlindedBeaconBlock.deinit(allocator, signed_block); allocator.destroy(signed_block); }, + .full_gloas => |signed_block| { + ct.gloas.SignedBeaconBlock.deinit(allocator, signed_block); + allocator.destroy(signed_block); + }, } } @@ -258,12 +271,18 @@ pub const AnySignedBeaconBlock = union(enum) { _ = ct.fulu.SignedBlindedBeaconBlock.serializeIntoBytes(signed_block, out); return out; }, + .full_gloas => |signed_block| { + const out = try allocator.alloc(u8, ct.gloas.SignedBeaconBlock.serializedSize(signed_block)); + errdefer allocator.free(out); + _ = ct.gloas.SignedBeaconBlock.serializeIntoBytes(signed_block, out); + return out; + }, } } pub fn blockType(self: *const AnySignedBeaconBlock) BlockType { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu => .full, + .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu, .full_gloas => .full, .blinded_bellatrix, .blinded_capella, .blinded_deneb, .blinded_electra, .blinded_fulu => .blinded, }; } @@ -277,6 +296,7 @@ pub const AnySignedBeaconBlock = union(enum) { .full_deneb, .blinded_deneb => .deneb, .full_electra, .blinded_electra => .electra, .full_fulu, .blinded_fulu => .fulu, + .full_gloas => .gloas, }; } @@ -312,10 +332,11 @@ pub const AnyBeaconBlock = union(enum) { blinded_electra: *ct.electra.BlindedBeaconBlock.Type, full_fulu: *ct.fulu.BeaconBlock.Type, blinded_fulu: *ct.fulu.BlindedBeaconBlock.Type, + full_gloas: *ct.gloas.BeaconBlock.Type, pub fn blockType(self: *const AnyBeaconBlock) BlockType { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu => .full, + .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu, .full_gloas => .full, .blinded_bellatrix, .blinded_capella, .blinded_deneb, .blinded_electra, .blinded_fulu => .blinded, }; } @@ -329,6 +350,7 @@ pub const AnyBeaconBlock = union(enum) { .full_deneb, .blinded_deneb => .deneb, .full_electra, .blinded_electra => .electra, .full_fulu, .blinded_fulu => .fulu, + .full_gloas => .gloas, }; } @@ -366,6 +388,7 @@ pub const AnyBeaconBlock = union(enum) { @ptrCast(self.full_fulu) else @ptrCast(self.blinded_fulu), + .gloas => @ptrCast(self.full_gloas), }; } @@ -383,6 +406,7 @@ pub const AnyBeaconBlock = union(enum) { .blinded_electra => |block| try ct.electra.BlindedBeaconBlock.hashTreeRoot(allocator, block, out), .full_fulu => |block| try ct.fulu.BeaconBlock.hashTreeRoot(allocator, block, out), .blinded_fulu => |block| try ct.fulu.BlindedBeaconBlock.hashTreeRoot(allocator, block, out), + .full_gloas => |block| try ct.gloas.BeaconBlock.hashTreeRoot(allocator, block, out), } } @@ -436,10 +460,11 @@ pub const AnyBeaconBlockBody = union(enum) { blinded_electra: *ct.electra.BlindedBeaconBlockBody.Type, full_fulu: *ct.fulu.BeaconBlockBody.Type, blinded_fulu: *ct.fulu.BlindedBeaconBlockBody.Type, + full_gloas: *ct.gloas.BeaconBlockBody.Type, pub fn blockType(self: *const AnyBeaconBlockBody) BlockType { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu => .full, + .phase0, .altair, .full_bellatrix, .full_capella, .full_deneb, .full_electra, .full_fulu, .full_gloas => .full, .blinded_bellatrix, .blinded_capella, .blinded_deneb, .blinded_electra, .blinded_fulu => .blinded, }; } @@ -453,6 +478,7 @@ pub const AnyBeaconBlockBody = union(enum) { .full_deneb, .blinded_deneb => .deneb, .full_electra, .blinded_electra => .electra, .full_fulu, .blinded_fulu => .fulu, + .full_gloas => .gloas, }; } @@ -484,6 +510,7 @@ pub const AnyBeaconBlockBody = union(enum) { @ptrCast(self.full_fulu) else @ptrCast(self.blinded_fulu), + .gloas => @ptrCast(self.full_gloas), }; } @@ -501,6 +528,7 @@ pub const AnyBeaconBlockBody = union(enum) { .blinded_electra => |body| try ct.electra.BlindedBeaconBlockBody.hashTreeRoot(allocator, body, out), .full_fulu => |body| try ct.fulu.BeaconBlockBody.hashTreeRoot(allocator, body, out), .blinded_fulu => |body| try ct.fulu.BlindedBeaconBlockBody.hashTreeRoot(allocator, body, out), + .full_gloas => |body| try ct.gloas.BeaconBlockBody.hashTreeRoot(allocator, body, out), }; } @@ -607,7 +635,7 @@ pub const AnyBeaconBlockBody = union(enum) { // deneb fields pub fn blobKzgCommitments(self: *const AnyBeaconBlockBody) !*const ct.deneb.BlobKzgCommitments.Type { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella => error.InvalidFork, + .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_gloas => error.InvalidFork, inline else => |body| &body.blob_kzg_commitments, }; } @@ -615,28 +643,28 @@ pub const AnyBeaconBlockBody = union(enum) { // electra fields pub fn executionRequests(self: *const AnyBeaconBlockBody) !*const ct.electra.ExecutionRequests.Type { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb => error.InvalidFork, + .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb, .full_gloas => error.InvalidFork, inline else => |body| &body.execution_requests, }; } pub fn depositRequests(self: *const AnyBeaconBlockBody) ![]DepositRequest { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb => error.InvalidFork, + .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb, .full_gloas => error.InvalidFork, inline else => |body| body.execution_requests.deposits.items, }; } pub fn withdrawalRequests(self: *const AnyBeaconBlockBody) ![]WithdrawalRequest { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb => error.InvalidFork, + .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb, .full_gloas => error.InvalidFork, inline else => |body| body.execution_requests.withdrawals.items, }; } pub fn consolidationRequests(self: *const AnyBeaconBlockBody) ![]ConsolidationRequest { return switch (self.*) { - .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb => error.InvalidFork, + .phase0, .altair, .full_bellatrix, .blinded_bellatrix, .full_capella, .blinded_capella, .full_deneb, .blinded_deneb, .full_gloas => error.InvalidFork, inline else => |body| body.execution_requests.consolidations.items, }; } diff --git a/src/fork_types/beacon_block.zig b/src/fork_types/beacon_block.zig index 6a764eaa4..ac7df55af 100644 --- a/src/fork_types/beacon_block.zig +++ b/src/fork_types/beacon_block.zig @@ -12,7 +12,7 @@ pub fn SignedBeaconBlock(comptime bt: BlockType, comptime f: ForkSeq) type { inner: switch (bt) { .full => ForkTypes(f).SignedBeaconBlock.Type, - .blinded => ForkTypes(f).SignedBlindedBeaconBlock.Type, + .blinded => if (f == .gloas) @compileError("gloas doesn't have blinded blocks") else ForkTypes(f).SignedBlindedBeaconBlock.Type, }, pub const block_type = bt; @@ -30,7 +30,7 @@ pub fn BeaconBlock(comptime bt: BlockType, comptime f: ForkSeq) type { inner: switch (bt) { .full => ForkTypes(f).BeaconBlock.Type, - .blinded => ForkTypes(f).BlindedBeaconBlock.Type, + .blinded => if (f == .gloas) ForkTypes(f).BeaconBlock.Type else ForkTypes(f).BlindedBeaconBlock.Type, }, pub const block_type = bt; @@ -60,7 +60,7 @@ pub fn BeaconBlockBody(comptime bt: BlockType, comptime f: ForkSeq) type { inner: switch (bt) { .full => ForkTypes(f).BeaconBlockBody.Type, - .blinded => ForkTypes(f).BlindedBeaconBlockBody.Type, + .blinded => if (f == .gloas) ForkTypes(f).BeaconBlockBody.Type else ForkTypes(f).BlindedBeaconBlockBody.Type, }, pub const block_type = bt; @@ -81,6 +81,9 @@ pub fn BeaconBlockBody(comptime bt: BlockType, comptime f: ForkSeq) type { if (bt != .full) { @compileError("executionPayload is only available for full blocks"); } + if (comptime f == .gloas) { + @compileError("gloas blocks don't have execution_payload"); + } return @ptrCast(&self.inner.execution_payload); } @@ -89,6 +92,9 @@ pub fn BeaconBlockBody(comptime bt: BlockType, comptime f: ForkSeq) type { if (bt != .blinded) { @compileError("executionPayloadHeader is only available for blinded blocks"); } + if (comptime f == .gloas) { + @compileError("gloas blocks don't have execution_payload_header"); + } return @ptrCast(&self.inner.execution_payload_header); } diff --git a/src/fork_types/beacon_state.zig b/src/fork_types/beacon_state.zig index b4caccc78..b363e0f4c 100644 --- a/src/fork_types/beacon_state.zig +++ b/src/fork_types/beacon_state.zig @@ -298,18 +298,18 @@ pub fn BeaconState(comptime f: ForkSeq) type { } pub fn latestExecutionPayloadHeader(self: *Self, allocator: std.mem.Allocator, out: *ForkTypes(f).ExecutionPayloadHeader.Type) !void { - if (comptime (f.lt(.bellatrix))) return error.InvalidAtFork; + if (comptime (f.lt(.bellatrix) or f == .gloas)) return error.InvalidAtFork; try self.inner.getValue(allocator, "latest_execution_payload_header", out); } pub fn latestExecutionPayloadHeaderBlockHash(self: *Self) !*const [32]u8 { - if (comptime (f.lt(.bellatrix))) return error.InvalidAtFork; + if (comptime (f.lt(.bellatrix) or f == .gloas)) return error.InvalidAtFork; var header = try self.inner.get("latest_execution_payload_header"); return try header.getFieldRoot("block_hash"); } pub fn setLatestExecutionPayloadHeader(self: *Self, header: *const ForkTypes(f).ExecutionPayloadHeader.Type) !void { - if (comptime (f.lt(.bellatrix))) return error.InvalidAtFork; + if (comptime (f.lt(.bellatrix) or f == .gloas)) return error.InvalidAtFork; try self.inner.setValue("latest_execution_payload_header", header); } diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index 342c61df3..fadadf62a 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -24,6 +24,7 @@ const processRandao = @import("./process_randao.zig").processRandao; const processSyncAggregate = @import("./process_sync_committee.zig").processSyncAggregate; const processWithdrawals = @import("./process_withdrawals.zig").processWithdrawals; const getExpectedWithdrawals = @import("./process_withdrawals.zig").getExpectedWithdrawals; +const processExecutionPayloadBid = @import("./process_execution_payload_bid.zig").processExecutionPayloadBid; const isExecutionEnabled = @import("../utils/execution.zig").isExecutionEnabled; // TODO: proposer reward api // const ProposerRewardType = @import("../types/proposer_reward.zig").ProposerRewardType; @@ -60,7 +61,7 @@ pub fn processBlock( if (isExecutionEnabled(fork, state, block_type, block)) { // TODO Deneb: Allow to disable withdrawals for interop testing // https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals - if (comptime fork.gte(.capella)) { + if (comptime fork.gte(.capella) and fork.lt(.gloas)) { // TODO: given max withdrawals of MAX_WITHDRAWALS_PER_PAYLOAD, can use fixed size array instead of heap alloc var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( allocator, @@ -92,19 +93,26 @@ pub fn processBlock( try processWithdrawals(fork, allocator, state, withdrawals_result, payload_withdrawals_root); } - try processExecutionPayload( - fork, - allocator, - config, - state, - current_epoch, - block_type, - body, - external_data, - ); + if (comptime fork.lt(.gloas)) { + try processExecutionPayload( + fork, + allocator, + config, + state, + current_epoch, + block_type, + body, + external_data, + ); + } } } + // process_execution_payload_bid replaces process_execution_payload + if (comptime fork.gte(.gloas)) { + try processExecutionPayloadBid(allocator, config, epoch_cache, state, block_type, block); + } + try processRandao(fork, config, epoch_cache, state, block_type, body, block.proposerIndex(), opts.verify_signature); try processEth1Data(fork, state, body.eth1Data()); try processOperations(fork, allocator, config, epoch_cache, state, slashings_cache, block_type, body, opts); diff --git a/src/state_transition/block/process_operations.zig b/src/state_transition/block/process_operations.zig index 2cda2eb8a..620e1967b 100644 --- a/src/state_transition/block/process_operations.zig +++ b/src/state_transition/block/process_operations.zig @@ -75,7 +75,7 @@ pub fn processOperations( } } - if (comptime fork.gte(.electra)) { + if (comptime fork.gte(.electra) and fork.lt(.gloas)) { const execution_requests = &body.inner.execution_requests; for (execution_requests.deposits.items) |*deposit_request| { try processDepositRequest(fork, state, deposit_request); @@ -89,6 +89,13 @@ pub fn processOperations( try processConsolidationRequest(fork, config, epoch_cache, state, consolidation_request); } } + + if (comptime fork.gte(.gloas)) { + for (body.inner.payload_attestations.items) |*payload_attestation| { + _ = payload_attestation; + // TODO: call processPayloadAttestation + } + } } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; diff --git a/src/state_transition/block/slash_validator.zig b/src/state_transition/block/slash_validator.zig index 46dad3e9f..dce4d3fd1 100644 --- a/src/state_transition/block/slash_validator.zig +++ b/src/state_transition/block/slash_validator.zig @@ -64,7 +64,7 @@ pub fn slashValidator( .phase0 => preset.MIN_SLASHING_PENALTY_QUOTIENT, .altair => preset.MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR, .bellatrix, .capella, .deneb => preset.MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX, - .electra, .fulu => preset.MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA, + .electra, .fulu, .gloas => preset.MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA, }; try decreaseBalance(fork, state, slashed_index, @divFloor(effective_balance, min_slashing_penalty_quotient)); @@ -72,7 +72,7 @@ pub fn slashValidator( // apply proposer and whistleblower rewards // TODO(ct): define WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA const whistleblower_reward = switch (fork) { - .electra, .fulu => @divFloor(effective_balance, preset.WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA), + .electra, .fulu, .gloas => @divFloor(effective_balance, preset.WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA), else => @divFloor(effective_balance, preset.WHISTLEBLOWER_REWARD_QUOTIENT), }; diff --git a/src/state_transition/epoch/process_epoch.zig b/src/state_transition/epoch/process_epoch.zig index 84647c7b7..6fc850bd2 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -24,6 +24,7 @@ const processHistoricalRootsUpdate = @import("./process_historical_roots_update. const processParticipationRecordUpdates = @import("./process_participation_record_updates.zig").processParticipationRecordUpdates; const processParticipationFlagUpdates = @import("./process_participation_flag_updates.zig").processParticipationFlagUpdates; const processSyncCommitteeUpdates = @import("./process_sync_committee_updates.zig").processSyncCommitteeUpdates; +const processBuilderPendingPayments = @import("./process_builder_pending_payments.zig").processBuilderPendingPayments; const processProposerLookahead = @import("./process_proposer_lookahead.zig").processProposerLookahead; const Node = @import("persistent_merkle_tree").Node; @@ -69,6 +70,12 @@ pub fn processEpoch( try observeEpochTransitionStep(.{ .step = .process_pending_consolidations }, timer.read()); } + if (comptime fork.gte(.gloas)) { + timer = try Timer.start(); + try processBuilderPendingPayments(allocator, state, epoch_cache.total_active_balance_increments); + try observeEpochTransitionStep(.{ .step = .process_builder_pending_payments }, timer.read()); + } + // const numUpdate = processEffectiveBalanceUpdates(fork, state, cache); timer = try Timer.start(); _ = try processEffectiveBalanceUpdates(fork, allocator, epoch_cache, state, cache); diff --git a/src/state_transition/slot/process_slot.zig b/src/state_transition/slot/process_slot.zig index 17652306c..d204a23a5 100644 --- a/src/state_transition/slot/process_slot.zig +++ b/src/state_transition/slot/process_slot.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const ForkSeq = @import("config").ForkSeq; const AnyBeaconState = @import("fork_types").AnyBeaconState; const preset = @import("preset").preset; const ZERO_HASH = @import("constants").ZERO_HASH; @@ -22,4 +23,15 @@ pub fn processSlot(state: *AnyBeaconState) !void { const previous_block_root = try latest_block_header.hashTreeRoot(); var block_roots = try state.blockRoots(); try block_roots.setValue(try state.slot() % preset.SLOTS_PER_HISTORICAL_ROOT, previous_block_root[0..]); + + if (state.forkSeq().gte(.gloas)) { + const nextSlotIndex = (try state.slot() + 1) % preset.SLOTS_PER_HISTORICAL_ROOT; + switch (state.*) { + .gloas => |s| { + var executionPayloadAvailability = try s.get("execution_payload_availability"); + try executionPayloadAvailability.set(nextSlotIndex, false); + }, + else => {}, + } + } } diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index 0d70e1241..695cbeeca 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -231,7 +231,7 @@ pub fn stateTransition( inline else => |f| { switch (block.blockType()) { inline else => |bt| { - if (comptime bt == .blinded and f.lt(.bellatrix)) { + if (comptime bt == .blinded and (f.lt(.bellatrix) or f == .gloas)) { return error.InvalidBlockTypeForFork; } try processBlock( diff --git a/src/state_transition/utils/execution.zig b/src/state_transition/utils/execution.zig index 7be154abf..72f0569b3 100644 --- a/src/state_transition/utils/execution.zig +++ b/src/state_transition/utils/execution.zig @@ -10,6 +10,7 @@ const ZERO_HASH = @import("constants").ZERO_HASH; pub fn isExecutionEnabled(comptime fork: ForkSeq, state: *BeaconState(fork), comptime block_type: BlockType, block: *const BeaconBlock(block_type, fork)) bool { if (comptime fork.lt(.bellatrix)) return false; + if (comptime fork == .gloas) return true; if (isMergeTransitionComplete(fork, state)) return true; switch (block_type) { diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig index 1fd2377d7..36a5078b6 100644 --- a/src/state_transition/utils/gloas.zig +++ b/src/state_transition/utils/gloas.zig @@ -54,10 +54,10 @@ pub fn getPendingBalanceToWithdrawForBuilder(allocator: Allocator, state: *Beaco } var payments = try state.inner.get("builder_pending_payments"); - const payments_len = try payments.length(); - var p_it = payments.iteratorReadonly(0); - for (0..payments_len) |_| { - const p = try p_it.nextValue(allocator); + const payments_len = ct.gloas.BeaconState.getFieldType("builder_pending_payments").length; + for (0..payments_len) |i| { + var p: ct.gloas.BuilderPendingPayment.Type = undefined; + try payments.getValue(allocator, i, &p); if (p.withdrawal.builder_index == builder_index) { pending_balance += p.withdrawal.amount; } @@ -138,3 +138,4 @@ pub fn isPubkeyInList(list: []const BLSPubkey, pubkey: *const BLSPubkey) bool { } return false; } + From 7a570c199c81a21a9c9ec76747d1c5c04ef405cf Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 11:27:30 +0530 Subject: [PATCH 05/30] add PTC computaton and indexed payload attestation --- .../is_valid_indexed_payload_attestation.zig | 35 ++++++ src/state_transition/cache/epoch_cache.zig | 61 ++++++++++ .../indexed_payload_attestation.zig | 42 +++++++ src/state_transition/utils/seed.zig | 111 ++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/state_transition/block/is_valid_indexed_payload_attestation.zig create mode 100644 src/state_transition/signature_sets/indexed_payload_attestation.zig diff --git a/src/state_transition/block/is_valid_indexed_payload_attestation.zig b/src/state_transition/block/is_valid_indexed_payload_attestation.zig new file mode 100644 index 000000000..50d08c032 --- /dev/null +++ b/src/state_transition/block/is_valid_indexed_payload_attestation.zig @@ -0,0 +1,35 @@ +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, + 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, config, epoch_cache, indexed_payload_attestation); + defer allocator.free(sig_set.pubkeys); + return verifyAggregatedSignatureSet(&sig_set); +} diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index ce0d0f13b..da9ffcbf9 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -136,6 +136,12 @@ pub const EpochCache = struct { epoch: Epoch, + /// PTC for current epoch, computed eagerly at epoch transition. + payload_timeliness_committees: ?[preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex, + + /// PTC for previous epoch, required for slot N block validating slot N-1 attestations. + previous_payload_timeliness_committees: ?[preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex, + fn initEffectiveBalanceIncrementsRc(allocator: Allocator, validator_count: usize) !*EffectiveBalanceIncrementsRc { var effective_balance_increments = try effectiveBalanceIncrementsInit(allocator, validator_count); errdefer effective_balance_increments.deinit(); @@ -879,4 +885,59 @@ pub const EpochCache = struct { pub fn isPostElectra(self: *const EpochCache) bool { return self.epoch >= self.config.chain.ELECTRA_FORK_EPOCH; } + + /// Convert a PayloadAttestation into an IndexedPayloadAttestation by resolving + /// aggregation bits against the Payload Timeliness Committee for the attestation's slot. + pub fn getPayloadTimelinessCommittee(self: *const EpochCache, slot: Slot) ![]const ValidatorIndex { + const epoch = computeEpochAtSlot(slot); + + if (epoch < self.config.GLOAS_FORK_EPOCH) { + return error.PtcNotAvailableBeforeGloas; + } + + if (epoch == self.epoch) { + if (self.payload_timeliness_committees) |*committees| { + return &committees[slot % preset.SLOTS_PER_EPOCH]; + } + } + + if (epoch == self.epoch -| 1) { + if (self.previous_payload_timeliness_committees) |*committees| { + return &committees[slot % preset.SLOTS_PER_EPOCH]; + } + } + + return error.PtcNotAvailableForSlot; + } + + pub fn getIndexedPayloadAttestation( + self: *const EpochCache, + allocator: Allocator, + slot: Slot, + payload_attestation: *const types.gloas.PayloadAttestation.Type, + ) !types.gloas.IndexedPayloadAttestation.Type { + const payload_timeliness_committee = try self.getPayloadTimelinessCommittee(slot); + + var attesting_indices = std.ArrayList(ValidatorIndex).init(allocator); + errdefer attesting_indices.deinit(); + + for (0..payload_timeliness_committee.len) |i| { + if (try payload_attestation.aggregation_bits.get(i)) { + try attesting_indices.append(payload_timeliness_committee[i]); + } + } + + const sortFn = struct { + pub fn sort(_: void, a: ValidatorIndex, b: ValidatorIndex) bool { + return a < b; + } + }.sort; + std.mem.sort(ValidatorIndex, attesting_indices.items, {}, sortFn); + + return .{ + .attesting_indices = attesting_indices.moveToUnmanaged(), + .data = payload_attestation.data, + .signature = payload_attestation.signature, + }; + } }; diff --git a/src/state_transition/signature_sets/indexed_payload_attestation.zig b/src/state_transition/signature_sets/indexed_payload_attestation.zig new file mode 100644 index 000000000..aa0f17ec8 --- /dev/null +++ b/src/state_transition/signature_sets/indexed_payload_attestation.zig @@ -0,0 +1,42 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const bls = @import("bls"); +const PublicKey = bls.PublicKey; +const types = @import("consensus_types"); +const Root = types.primitive.Root.Type; +const BLSSignature = types.primitive.BLSSignature.Type; +const ValidatorIndex = types.primitive.ValidatorIndex.Type; +const BeaconConfig = @import("config").BeaconConfig; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const c = @import("constants"); +const computeSigningRoot = @import("../utils/signing_root.zig").computeSigningRoot; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const AggregatedSignatureSet = @import("../utils/signature_sets.zig").AggregatedSignatureSet; +const createAggregateSignatureSetFromComponents = @import("../utils/signature_sets.zig").createAggregateSignatureSetFromComponents; + +pub fn getPayloadAttestationDataSigningRoot(config: *const BeaconConfig, data: *const types.gloas.PayloadAttestationData.Type, out: *[32]u8) !void { + const domain = try config.getDomain(computeEpochAtSlot(data.slot), c.DOMAIN_PTC_ATTESTER, null); + + try computeSigningRoot(types.gloas.PayloadAttestationData, data, domain, out); +} + +/// Consumer needs to free the returned pubkeys array. +pub fn getIndexedPayloadAttestationSignatureSet( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + indexed_payload_attestation: *const types.gloas.IndexedPayloadAttestation.Type, +) !AggregatedSignatureSet { + const attesting_indices = indexed_payload_attestation.attesting_indices.items; + + const pubkeys = try allocator.alloc(PublicKey, attesting_indices.len); + errdefer allocator.free(pubkeys); + for (attesting_indices, 0..) |index, i| { + pubkeys[i] = epoch_cache.index_to_pubkey.items[index]; + } + + var signing_root: Root = undefined; + try getPayloadAttestationDataSigningRoot(config, &indexed_payload_attestation.data, &signing_root); + + return createAggregateSignatureSetFromComponents(pubkeys, signing_root, indexed_payload_attestation.signature); +} \ No newline at end of file diff --git a/src/state_transition/utils/seed.zig b/src/state_transition/utils/seed.zig index abd7638e9..692022168 100644 --- a/src/state_transition/utils/seed.zig +++ b/src/state_transition/utils/seed.zig @@ -77,6 +77,117 @@ pub fn getNextSyncCommitteeIndices(comptime fork: ForkSeq, allocator: Allocator, try computeSyncCommitteeIndices(allocator, &seed, active_indices, effective_balance_increments.items, rand_byte_count, max_effective_balance, preset.EFFECTIVE_BALANCE_INCREMENT, preset.SHUFFLE_ROUND_COUNT, out); } +/// Select PTC_SIZE validators from the given indices using balance-weighted acceptance sampling. +pub fn computePayloadTimelinessCommitteeIndices( + effective_balance_increments: []const u16, + indices: []const ValidatorIndex, + seed: *const [32]u8, +) ![preset.PTC_SIZE]ValidatorIndex { + if (indices.len == 0) return error.EmptyIndices; + + const MAX_RANDOM_VALUE: u64 = 0xFFFF; + const max_effective_balance_increment: u64 = preset.MAX_EFFECTIVE_BALANCE_ELECTRA / preset.EFFECTIVE_BALANCE_INCREMENT; + + var result: [preset.PTC_SIZE]ValidatorIndex = undefined; + var result_len: usize = 0; + + // Pre-allocate hash input buffer: seed (32 bytes) + block index (8 bytes) + var hash_input: [40]u8 = undefined; + @memcpy(hash_input[0..32], seed); + + var i: u64 = 0; + var random_bytes: [32]u8 = undefined; + var last_block: u64 = std.math.maxInt(u64); + + while (result_len < preset.PTC_SIZE) { + const candidate_index = indices[@intCast(i % indices.len)]; + + // Only recompute hash every 16 iterations + const block = i / 16; + if (block != last_block) { + std.mem.writeInt(u64, hash_input[32..][0..8], block, .little); + Sha256.hash(&hash_input, &random_bytes, .{}); + last_block = block; + } + + const offset: usize = @intCast((i % 16) * 2); + const random_value: u64 = std.mem.readInt(u16, random_bytes[offset..][0..2], .little); + + const candidate_effective_balance_increment: u64 = effective_balance_increments[@intCast(candidate_index)]; + if (candidate_effective_balance_increment * MAX_RANDOM_VALUE >= max_effective_balance_increment * random_value) { + result[result_len] = candidate_index; + result_len += 1; + } + i += 1; + } + + return result; +} + +/// Compute the Payload Timeliness Committee for a single slot by concatenating all +/// beacon committees for that slot and selecting PTC_SIZE members via balance-weighted sampling. +pub fn computePayloadTimelinessCommitteeForSlot( + allocator: Allocator, + slot_seed: *const [32]u8, + slot_committees: []const []const ValidatorIndex, + effective_balance_increments: []const u16, +) ![preset.PTC_SIZE]ValidatorIndex { + var total_len: usize = 0; + for (slot_committees) |committee| { + total_len += committee.len; + } + + const all_indices = try allocator.alloc(ValidatorIndex, total_len); + defer allocator.free(all_indices); + + var offset: usize = 0; + for (slot_committees) |committee| { + @memcpy(all_indices[offset .. offset + committee.len], committee); + offset += committee.len; + } + + return computePayloadTimelinessCommitteeIndices(effective_balance_increments, all_indices, slot_seed); +} + +/// Compute the Payload Timeliness Committee for every slot in an epoch. +pub fn computePayloadTimelinessCommitteesForEpoch( + comptime fork: ForkSeq, + allocator: Allocator, + state: *BeaconState(fork), + epoch: Epoch, + epoch_cache: *const @import("../cache/epoch_cache.zig").EpochCache, +) ![preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex { + var epoch_seed: [32]u8 = undefined; + try getSeed(fork, state, epoch, c.DOMAIN_PTC_ATTESTER, &epoch_seed); + + const start_slot = computeStartSlotAtEpoch(epoch); + + var slot_seed_input: [40]u8 = undefined; + @memcpy(slot_seed_input[0..32], &epoch_seed); + + var result: [preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex = undefined; + + for (0..preset.SLOTS_PER_EPOCH) |i| { + const slot = start_slot + i; + std.mem.writeInt(u64, slot_seed_input[32..][0..8], slot, .little); + + var slot_seed: [32]u8 = undefined; + Sha256.hash(&slot_seed_input, &slot_seed, .{}); + + const committees_per_slot = try epoch_cache.getCommitteeCountPerSlot(epoch); + const slot_committees = try allocator.alloc([]const ValidatorIndex, committees_per_slot); + defer allocator.free(slot_committees); + + for (0..committees_per_slot) |ci| { + slot_committees[ci] = try epoch_cache.getBeaconCommittee(slot, ci); + } + + result[i] = try computePayloadTimelinessCommitteeForSlot(allocator, &slot_seed, slot_committees, epoch_cache.effective_balance_increments.get().items); + } + + return result; +} + pub fn getRandaoMix(comptime fork: ForkSeq, state: *BeaconState(fork), epoch: Epoch) !*const [32]u8 { var randao_mixes = try state.randaoMixes(); return try randao_mixes.getFieldRoot(epoch % EPOCHS_PER_HISTORICAL_VECTOR); From 3b9eb466f9719e284af64c555575fb5cbd84f1f2 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 13:58:32 +0530 Subject: [PATCH 06/30] add process_builder_pending_payments --- src/state_transition/cache/epoch_cache.zig | 4 +++ .../process_builder_pending_payments.zig | 35 +++++++++++++++++++ src/state_transition/epoch/process_epoch.zig | 2 +- src/state_transition/metrics.zig | 1 + src/state_transition/utils/gloas.zig | 5 +-- 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/state_transition/epoch/process_builder_pending_payments.zig diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index da9ffcbf9..7576cd282 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -445,6 +445,8 @@ pub const EpochCache = struct { .next_sync_committee_indexed = next_sync_committee_indexed, .sync_period = computeSyncPeriodAtEpoch(current_epoch), .epoch = current_epoch, + .payload_timeliness_committees = null, + .previous_payload_timeliness_committees = null, }; return epoch_cache_ptr; @@ -504,6 +506,8 @@ pub const EpochCache = struct { .next_sync_committee_indexed = self.next_sync_committee_indexed.acquire(), .sync_period = self.sync_period, .epoch = self.epoch, + .payload_timeliness_committees = self.payload_timeliness_committees, + .previous_payload_timeliness_committees = self.previous_payload_timeliness_committees, }; const epoch_cache_ptr = try allocator.create(EpochCache); diff --git a/src/state_transition/epoch/process_builder_pending_payments.zig b/src/state_transition/epoch/process_builder_pending_payments.zig new file mode 100644 index 000000000..9cafdeb46 --- /dev/null +++ b/src/state_transition/epoch/process_builder_pending_payments.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ForkSeq = @import("config").ForkSeq; +const BeaconState = @import("fork_types").BeaconState; +const ct = @import("consensus_types"); +const preset = @import("preset").preset; +const getBuilderPaymentQuorumThreshold = @import("../utils/gloas.zig").getBuilderPaymentQuorumThreshold; + +/// Processes the builder pending payments from the previous epoch. +pub fn processBuilderPendingPayments(allocator: Allocator, state: *BeaconState(.gloas), epoch_cache: *const @import("../cache/epoch_cache.zig").EpochCache) !void { + const quorum = getBuilderPaymentQuorumThreshold(epoch_cache); + + var builderPendingPayments = try state.inner.get("builder_pending_payments"); + var builderPendingWithdrawals = try state.inner.get("builder_pending_withdrawals"); + + for (0..preset.SLOTS_PER_EPOCH) |i| { + var payment: ct.gloas.BuilderPendingPayment.Type = undefined; + try builderPendingPayments.getValue(allocator, i, &payment); + if (payment.weight >= quorum) { + try builderPendingWithdrawals.pushValue(&payment.withdrawal); + } + } + // TODO: Gloas - Optimization needed + const total_payments = @TypeOf(builderPendingPayments.*).length; + for (0..total_payments) |i| { + if (i < preset.SLOTS_PER_EPOCH) { + var nextEpochPayment: ct.gloas.BuilderPendingPayment.Type = undefined; + try builderPendingPayments.getValue(allocator, i + preset.SLOTS_PER_EPOCH, &nextEpochPayment); + try builderPendingPayments.setValue(i, &nextEpochPayment); + } else { + const defaultPayment = ct.gloas.BuilderPendingPayment.default_value; + try builderPendingPayments.setValue(i, &defaultPayment); + } + } +} diff --git a/src/state_transition/epoch/process_epoch.zig b/src/state_transition/epoch/process_epoch.zig index 6fc850bd2..ad7dce66b 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -72,7 +72,7 @@ pub fn processEpoch( if (comptime fork.gte(.gloas)) { timer = try Timer.start(); - try processBuilderPendingPayments(allocator, state, epoch_cache.total_active_balance_increments); + try processBuilderPendingPayments(allocator, state, epoch_cache); try observeEpochTransitionStep(.{ .step = .process_builder_pending_payments }, timer.read()); } diff --git a/src/state_transition/metrics.zig b/src/state_transition/metrics.zig index e31c5d5a5..81751865e 100644 --- a/src/state_transition/metrics.zig +++ b/src/state_transition/metrics.zig @@ -35,6 +35,7 @@ pub const EpochTransitionStepKind = enum { process_sync_committee_updates, process_pending_deposits, process_pending_consolidations, + process_builder_pending_payments, process_proposer_lookahead, }; diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig index 36a5078b6..ab835ab48 100644 --- a/src/state_transition/utils/gloas.zig +++ b/src/state_transition/utils/gloas.zig @@ -7,6 +7,7 @@ const c = @import("constants"); const getBlockRootAtSlot = @import("./block_root.zig").getBlockRootAtSlot; const computeEpochAtSlot = @import("./epoch.zig").computeEpochAtSlot; const RootCache = @import("../cache/root_cache.zig").RootCache; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const BLSPubkey = ct.primitive.BLSPubkey.Type; @@ -14,8 +15,8 @@ pub fn isBuilderWithdrawalCredential(withdrawal_credentials: *const [32]u8) bool return withdrawal_credentials[0] == c.BUILDER_WITHDRAWAL_PREFIX; } -pub fn getBuilderPaymentQuorumThreshold(total_active_balance_increments: u64) u64 { - const quorum = (total_active_balance_increments * preset.EFFECTIVE_BALANCE_INCREMENT / preset.SLOTS_PER_EPOCH) * +pub fn getBuilderPaymentQuorumThreshold(epoch_cache: *const EpochCache) u64 { + const quorum = (epoch_cache.total_active_balance_increments * preset.EFFECTIVE_BALANCE_INCREMENT / preset.SLOTS_PER_EPOCH) * c.BUILDER_PAYMENT_THRESHOLD_NUMERATOR; return quorum / c.BUILDER_PAYMENT_THRESHOLD_DENOMINATOR; } From d48edb974ac7bc862f42d39100de4565c03dd084 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 14:23:07 +0530 Subject: [PATCH 07/30] add exeution_payload_bid and validate builder bids --- src/state_transition/block/process_block.zig | 2 +- .../block/process_execution_payload_bid.zig | 91 +++++++++++++++++++ .../signature_sets/execution_payload_bid.zig | 20 ++++ src/state_transition/utils/signing_root.zig | 12 +++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/state_transition/block/process_execution_payload_bid.zig create mode 100644 src/state_transition/signature_sets/execution_payload_bid.zig diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index fadadf62a..1270d62ea 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -110,7 +110,7 @@ pub fn processBlock( // process_execution_payload_bid replaces process_execution_payload if (comptime fork.gte(.gloas)) { - try processExecutionPayloadBid(allocator, config, epoch_cache, state, block_type, block); + try processExecutionPayloadBid(allocator, config, state, block); } try processRandao(fork, config, epoch_cache, state, block_type, body, block.proposerIndex(), opts.verify_signature); diff --git a/src/state_transition/block/process_execution_payload_bid.zig b/src/state_transition/block/process_execution_payload_bid.zig new file mode 100644 index 000000000..88e784b71 --- /dev/null +++ b/src/state_transition/block/process_execution_payload_bid.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const BeaconState = @import("fork_types").BeaconState; +const BeaconBlock = @import("fork_types").BeaconBlock; +const types = @import("consensus_types"); +const preset = @import("preset").preset; +const c = @import("constants"); +const bls = @import("bls"); +const getRandaoMix = @import("../utils/seed.zig").getRandaoMix; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const gloas_utils = @import("../utils/gloas.zig"); +const isActiveBuilder = gloas_utils.isActiveBuilder; +const canBuilderCoverBid = gloas_utils.canBuilderCoverBid; +const verify = @import("../utils/bls.zig").verify; +const getExecutionPayloadBidSigningRoot = @import("../signature_sets/execution_payload_bid.zig").getExecutionPayloadBidSigningRoot; + +pub fn processExecutionPayloadBid( + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(.gloas), + block: *const BeaconBlock(.full, .gloas), +) !void { + const signed_bid = &block.body().inner.signed_execution_payload_bid; + const bid = &signed_bid.message; + const builder_index = bid.builder_index; + const amount = bid.value; + + if (builder_index == c.BUILDER_INDEX_SELF_BUILD) { + if (amount != 0) return error.SelfBuildNonZeroAmount; + if (!std.mem.eql(u8, &signed_bid.signature, &c.G2_POINT_AT_INFINITY)) return error.SelfBuildNonZeroSignature; + } else { + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + const finalized_epoch = try state.finalizedEpoch(); + if (!isActiveBuilder(&builder, finalized_epoch)) return error.BuilderNotActive; + + if (!(try canBuilderCoverBid(allocator, state, builder_index, amount))) return error.BuilderInsufficientBalance; + + const state_slot = try state.slot(); + if (!(try verifyExecutionPayloadBidSignature(allocator, config, state_slot, &builder.pubkey, signed_bid))) return error.InvalidBidSignature; + } + + if (bid.slot != block.slot()) return error.BidSlotMismatch; + + const latest_block_hash = try state.inner.getFieldRoot("latest_block_hash"); + if (!std.mem.eql(u8, &bid.parent_block_hash, latest_block_hash)) return error.BidParentBlockHashMismatch; + + if (!std.mem.eql(u8, &bid.parent_block_root, block.parentRoot())) return error.BidParentBlockRootMismatch; + + const current_epoch = computeEpochAtSlot(try state.slot()); + const state_randao = try getRandaoMix(.gloas, state, current_epoch); + if (!std.mem.eql(u8, &bid.prev_randao, state_randao)) return error.BidPrevRandaoMismatch; + + // Verify commitments are under limit + const max_blobs_per_block = config.getMaxBlobsPerBlock(current_epoch); + if (bid.blob_kzg_commitments.items.len > max_blobs_per_block) return error.TooManyBlobCommitments; + + if (amount > 0) { + const pending_payment = types.gloas.BuilderPendingPayment.Type{ + .weight = 0, + .withdrawal = .{ + .fee_recipient = bid.fee_recipient, + .amount = amount, + .builder_index = builder_index, + }, + }; + var builder_pending_payments = try state.inner.get("builder_pending_payments"); + const payment_index = preset.SLOTS_PER_EPOCH + (bid.slot % preset.SLOTS_PER_EPOCH); + try builder_pending_payments.setValue(payment_index, &pending_payment); + } + + try state.inner.setValue("latest_execution_payload_bid", bid); +} + +fn verifyExecutionPayloadBidSignature( + allocator: Allocator, + config: *const BeaconConfig, + state_slot: u64, + pubkey: *const [48]u8, + signed_bid: *const types.gloas.SignedExecutionPayloadBid.Type, +) !bool { + const signing_root = try getExecutionPayloadBidSigningRoot(allocator, config, state_slot, &signed_bid.message); + + const public_key = bls.PublicKey.uncompress(pubkey) catch return false; + const signature = bls.Signature.uncompress(&signed_bid.signature) catch return false; + verify(&signing_root, &public_key, &signature, null, null) catch return false; + return true; +} diff --git a/src/state_transition/signature_sets/execution_payload_bid.zig b/src/state_transition/signature_sets/execution_payload_bid.zig new file mode 100644 index 000000000..cd0a83a61 --- /dev/null +++ b/src/state_transition/signature_sets/execution_payload_bid.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const types = @import("consensus_types"); +const BeaconConfig = @import("config").BeaconConfig; +const c = @import("constants"); +const computeSigningRootVariable = @import("../utils/signing_root.zig").computeSigningRootVariable; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; + +pub fn getExecutionPayloadBidSigningRoot( + allocator: Allocator, + config: *const BeaconConfig, + state_slot: u64, + bid: *const types.gloas.ExecutionPayloadBid.Type, +) ![32]u8 { + const domain = try config.getDomain(computeEpochAtSlot(state_slot), c.DOMAIN_BEACON_BUILDER, null); + + var out: [32]u8 = undefined; + try computeSigningRootVariable(types.gloas.ExecutionPayloadBid, allocator, bid, domain, &out); + return out; +} diff --git a/src/state_transition/utils/signing_root.zig b/src/state_transition/utils/signing_root.zig index 27cfb0697..c1834a7ef 100644 --- a/src/state_transition/utils/signing_root.zig +++ b/src/state_transition/utils/signing_root.zig @@ -19,6 +19,18 @@ pub fn computeSigningRoot(comptime T: type, ssz_object: *const T.Type, domain: * try types.phase0.SigningData.hashTreeRoot(&domain_wrapped_object, out); } +/// Return the signing root for a variable-size SSZ object +pub fn computeSigningRootVariable(comptime T: type, allocator: Allocator, ssz_object: *const T.Type, domain: *const Domain, out: *[32]u8) !void { + var object_root: Root = undefined; + try T.hashTreeRoot(allocator, ssz_object, &object_root); + const domain_wrapped_object: SigningData = .{ + .object_root = object_root, + .domain = domain.*, + }; + + try types.phase0.SigningData.hashTreeRoot(&domain_wrapped_object, out); +} + pub fn computeBlockSigningRoot(allocator: Allocator, block: AnyBeaconBlock, domain: *const Domain, out: *[32]u8) !void { var object_root: Root = undefined; try block.hashTreeRoot(allocator, &object_root); From d1e7623c39e261827f235db11674be0be65257f9 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 17:35:51 +0530 Subject: [PATCH 08/30] add builder withdrawal support --- .../block/process_withdrawals.zig | 480 ++++++++++++++---- src/state_transition/utils/validator.zig | 26 + 2 files changed, 414 insertions(+), 92 deletions(-) diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 1030bbe51..641f5b0f4 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -13,13 +13,25 @@ const ExecutionAddress = types.primitive.ExecutionAddress.Type; const hasExecutionWithdrawalCredential = @import("../utils/electra.zig").hasExecutionWithdrawalCredential; const hasEth1WithdrawalCredential = @import("../utils/capella.zig").hasEth1WithdrawalCredential; const getMaxEffectiveBalance = @import("../utils/validator.zig").getMaxEffectiveBalance; +const isPartiallyWithdrawableValidator = @import("../utils/validator.zig").isPartiallyWithdrawableValidator; const decreaseBalance = @import("../utils/balance.zig").decreaseBalance; +const gloas_utils = @import("../utils/gloas.zig"); +const isBuilderIndex = gloas_utils.isBuilderIndex; +const convertBuilderIndexToValidatorIndex = gloas_utils.convertBuilderIndexToValidatorIndex; +const convertValidatorIndexToBuilderIndex = gloas_utils.convertValidatorIndexToBuilderIndex; +const isParentBlockFull = gloas_utils.isParentBlockFull; const Node = @import("persistent_merkle_tree").Node; +const Withdrawal = types.capella.Withdrawal.Type; +const Epoch = types.primitive.Epoch.Type; pub const WithdrawalsResult = struct { withdrawals: Withdrawals, - sampled_validators: usize = 0, + processed_validator_sweep_count: usize = 0, processed_partial_withdrawals_count: usize = 0, + // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since EIP-7732 + processed_builder_withdrawals_count: usize = 0, + // processedBuildersSweepCount is withdrawals from builder sweep since EIP-7732 + processed_builders_sweep_count: usize = 0, }; /// right now for the implementation we pass in processBlock() @@ -33,45 +45,72 @@ pub fn processWithdrawals( expected_withdrawals_result: WithdrawalsResult, payload_withdrawals_root: Root, ) !void { - // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - const processed_partial_withdrawals_count = expected_withdrawals_result.processed_partial_withdrawals_count; - const expected_withdrawals = expected_withdrawals_result.withdrawals.items; - const num_withdrawals = expected_withdrawals.len; + // [New in EIP-7732] Return early if parent block is empty + if (comptime fork.gte(.gloas)) { + if (!(try isParentBlockFull(state))) return; + } - var expected_withdrawals_root: [32]u8 = undefined; - try types.capella.Withdrawals.hashTreeRoot(allocator, &expected_withdrawals_result.withdrawals, &expected_withdrawals_root); + const expected_withdrawals = expected_withdrawals_result.withdrawals.items; - if (!std.mem.eql(u8, &expected_withdrawals_root, &payload_withdrawals_root)) { - return error.WithdrawalsRootMismatch; + // After EIP-7732, withdrawals are verified later in processExecutionPayloadEnvelope + if (comptime fork.lt(.gloas)) { + var expected_withdrawals_root: [32]u8 = undefined; + try types.capella.Withdrawals.hashTreeRoot(allocator, &expected_withdrawals_result.withdrawals, &expected_withdrawals_root); + if (!std.mem.eql(u8, &expected_withdrawals_root, &payload_withdrawals_root)) { + return error.WithdrawalsRootMismatch; + } } - for (0..num_withdrawals) |i| { - const withdrawal = expected_withdrawals[i]; - try decreaseBalance(fork, state, withdrawal.validator_index, withdrawal.amount); + try applyWithdrawals(fork, allocator, state, expected_withdrawals); + // Update pending_partial_withdrawals (electra+) + if (comptime fork.gte(.electra)) { + var pending_partial_withdrawals = try state.pendingPartialWithdrawals(); + const truncated = try pending_partial_withdrawals.sliceFrom(expected_withdrawals_result.processed_partial_withdrawals_count); + try state.setPendingPartialWithdrawals(truncated); } - if (comptime fork.gte(.electra)) { - if (processed_partial_withdrawals_count > 0) { - var pending_partial_withdrawals = try state.pendingPartialWithdrawals(); - const truncated = try pending_partial_withdrawals.sliceFrom(processed_partial_withdrawals_count); + if (comptime fork.gte(.gloas)) { + // Store expected withdrawals for verification in processExecutionPayloadEnvelope + var payload_expected_withdrawals = try state.inner.get("payload_expected_withdrawals"); + const current_len = try payload_expected_withdrawals.length(); + var new_list = try payload_expected_withdrawals.sliceFrom(current_len); + for (expected_withdrawals) |w| { + try new_list.pushValue(&w); + } + try state.inner.set("payload_expected_withdrawals", new_list); - try state.setPendingPartialWithdrawals(truncated); + // Update builder pending withdrawals queue + const processed_builder_withdrawals_count = expected_withdrawals_result.processed_builder_withdrawals_count; + if (processed_builder_withdrawals_count > 0) { + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + const truncated = try builder_pending_withdrawals.sliceFrom(processed_builder_withdrawals_count); + try state.inner.set("builder_pending_withdrawals", truncated); + } + + // Update next builder index for sweep + var builders = try state.inner.get("builders"); + const builders_len: u64 = try builders.length(); + if (builders_len > 0) { + const current_builder_index: u64 = try state.inner.get("next_withdrawal_builder_index"); + const processed_builders_sweep_count: u64 = @intCast(expected_withdrawals_result.processed_builders_sweep_count); + const next_builder_index = (current_builder_index + processed_builders_sweep_count) % builders_len; + try state.inner.set("next_withdrawal_builder_index", next_builder_index); } } // Update the nextWithdrawalIndex - if (expected_withdrawals.len > 0) { - const latest_withdrawal = expected_withdrawals[expected_withdrawals.len - 1]; - try state.setNextWithdrawalIndex(latest_withdrawal.index + 1); + const latest_withdrawal = if (expected_withdrawals.len > 0) expected_withdrawals[expected_withdrawals.len - 1] else null; + if (latest_withdrawal) |lw| { + try state.setNextWithdrawalIndex(lw.index + 1); } // Update the next_withdrawal_validator_index const validators_len: u64 = @intCast(try state.validatorsCount()); const next_withdrawal_validator_index = try state.nextWithdrawalValidatorIndex(); - if (expected_withdrawals.len == preset.MAX_WITHDRAWALS_PER_PAYLOAD) { + if (latest_withdrawal != null and expected_withdrawals.len == preset.MAX_WITHDRAWALS_PER_PAYLOAD) { // All slots filled, next_withdrawal_validator_index should be validatorIndex having next turn try state.setNextWithdrawalValidatorIndex( - (expected_withdrawals[expected_withdrawals.len - 1].validator_index + 1) % validators_len, + (latest_withdrawal.?.validator_index + 1) % validators_len, ); } else { // expected withdrawals came up short in the bound, so we move next_withdrawal_validator_index to @@ -97,73 +136,227 @@ pub fn getExpectedWithdrawals( const epoch = epoch_cache.epoch; var withdrawal_index = try state.nextWithdrawalIndex(); - var validators = try state.validators(); - var balances = try state.balances(); - const next_withdrawal_validator_index = try state.nextWithdrawalValidatorIndex(); - // partial_withdrawals_count is withdrawals coming from EL since electra (EIP-7002) + // Separate maps to track balances after applying withdrawals + var builder_balance_after_withdrawals = std.AutoHashMap(u64, u64).init(allocator); + defer builder_balance_after_withdrawals.deinit(); + + // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) var processed_partial_withdrawals_count: u64 = 0; + // builderWithdrawalsCount is withdrawals coming from builder payments since EIP-7732 + var processed_builder_withdrawals_count: u64 = 0; + // buildersSweepCount is withdrawals from builder sweep since EIP-7732 + var processed_builders_sweep_count: u64 = 0; + // [New in EIP-7732] get_builder_withdrawals + if (comptime fork.gte(.gloas)) { + const result = try getBuilderWithdrawals( + allocator, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + &builder_balance_after_withdrawals, + ); + defer result.withdrawals.deinit(); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); + } + processed_builder_withdrawals_count = result.processed_count; + } + + // get_pending_partial_withdrawals (electra+) if (comptime fork.gte(.electra)) { - // MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 so we should just lazily iterate thru state.pending_partial_withdrawals. - // pending_partial_withdrawals comes from EIP-7002 smart contract where it takes fee so it's more likely than not validator is in correct condition to withdraw - // also we may break early if withdrawableEpoch > epoch - var pending_partial_withdrawals = try state.pendingPartialWithdrawals(); - var pending_partial_withdrawals_it = pending_partial_withdrawals.iteratorReadonly(0); - const pending_partial_withdrawals_len = try pending_partial_withdrawals.length(); - - for (0..pending_partial_withdrawals_len) |_| { - const withdrawal = try pending_partial_withdrawals_it.nextValue(undefined); - if (withdrawal.withdrawable_epoch > epoch or withdrawals_result.withdrawals.items.len == preset.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) { - break; - } - - var validator: types.phase0.Validator.Type = undefined; - try validators.getValue(undefined, withdrawal.validator_index, &validator); - - const total_withdrawn_gop = try withdrawal_balances.getOrPut(withdrawal.validator_index); - - const total_withdrawn: u64 = if (total_withdrawn_gop.found_existing) total_withdrawn_gop.value_ptr.* else 0; - const balance = try balances.get(withdrawal.validator_index) - total_withdrawn; - - if (validator.exit_epoch == c.FAR_FUTURE_EPOCH and - validator.effective_balance >= preset.MIN_ACTIVATION_BALANCE and - balance > preset.MIN_ACTIVATION_BALANCE) - { - const balance_over_min_activation_balance = balance - preset.MIN_ACTIVATION_BALANCE; - const withdrawable_balance = if (balance_over_min_activation_balance < withdrawal.amount) balance_over_min_activation_balance else withdrawal.amount; - var execution_address: ExecutionAddress = undefined; - @memcpy(&execution_address, validator.withdrawal_credentials[12..]); - try withdrawals_result.withdrawals.append(allocator, .{ - .index = withdrawal_index, - .validator_index = withdrawal.validator_index, - .address = execution_address, - .amount = withdrawable_balance, - }); - withdrawal_index += 1; - try withdrawal_balances.put(withdrawal.validator_index, total_withdrawn + withdrawable_balance); - } - processed_partial_withdrawals_count += 1; + const result = try getPendingPartialWithdrawals( + fork, + allocator, + epoch, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + withdrawal_balances, + ); + defer result.withdrawals.deinit(); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); + } + processed_partial_withdrawals_count = result.processed_count; + } + + // [New in EIP-7732] get_builders_sweep_withdrawals + if (comptime fork.gte(.gloas)) { + const result = try getBuildersSweepWithdrawals( + allocator, + state, + epoch, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + &builder_balance_after_withdrawals, + ); + defer result.withdrawals.deinit(); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); + } + processed_builders_sweep_count = result.processed_count; + } + + // get_validators_sweep_withdrawals + { + const result = try getValidatorsSweepWithdrawals( + fork, + allocator, + epoch, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + withdrawal_balances, + ); + defer result.withdrawals.deinit(); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); } + withdrawals_result.processed_validator_sweep_count = result.processed_count; } + withdrawals_result.processed_partial_withdrawals_count = processed_partial_withdrawals_count; + withdrawals_result.processed_builder_withdrawals_count = @intCast(processed_builder_withdrawals_count); + withdrawals_result.processed_builders_sweep_count = @intCast(processed_builders_sweep_count); +} + +/// [Modified in EIP-7732] apply_withdrawals handles builder indices +fn applyWithdrawals( + comptime fork: ForkSeq, + allocator: Allocator, + state: *BeaconState(fork), + withdrawals: []const Withdrawal, +) !void { + for (withdrawals) |withdrawal| { + if (comptime fork.gte(.gloas) and isBuilderIndex(withdrawal.validator_index)) { + // Handle builder withdrawal + const builder_index = convertValidatorIndexToBuilderIndex(withdrawal.validator_index); + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + builder.balance -= @min(withdrawal.amount, builder.balance); + try builders.setValue(builder_index, &builder); + } else { + // Handle validator withdrawal + try decreaseBalance(fork, state, withdrawal.validator_index, withdrawal.amount); + } + } +} + +fn getPendingPartialWithdrawals( + comptime fork: ForkSeq, + allocator: Allocator, + epoch: Epoch, + state: *BeaconState(fork), + withdrawal_index: *u64, + num_prior_withdrawals: usize, + validator_balance_after_withdrawals: *std.AutoHashMap(ValidatorIndex, usize), +) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { + var pending_partial_withdrawals_result = std.ArrayList(Withdrawal).init(allocator); + errdefer pending_partial_withdrawals_result.deinit(); + + var validators = try state.validators(); + var balances = try state.balances(); + + // MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 + // so we lazily iterate. pendingPartialWithdrawals comes from EIP-7002 smart contract + // where it takes fee so it's more likely than not validator is in correct condition to withdraw. + // Also we may break early if withdrawableEpoch > epoch. + var pending_partial_withdrawals = try state.pendingPartialWithdrawals(); + var pending_partial_withdrawals_it = pending_partial_withdrawals.iteratorReadonly(0); + const pending_partial_withdrawals_len = try pending_partial_withdrawals.length(); + + // In pre-EIP-7732, partialWithdrawalBound == MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + const partial_withdrawal_bound = @min( + num_prior_withdrawals + preset.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + preset.MAX_WITHDRAWALS_PER_PAYLOAD - 1, + ); + // There must be at least one space reserved for validator sweep withdrawals + if (num_prior_withdrawals > partial_withdrawal_bound) { + return error.PriorWithdrawalsExceedLimit; + } + + // EIP-7002: Execution layer triggerable withdrawals + var processed_count: usize = 0; + for (0..pending_partial_withdrawals_len) |_| { + const withdrawal = try pending_partial_withdrawals_it.nextValue(undefined); + if (withdrawal.withdrawable_epoch > epoch or pending_partial_withdrawals_result.items.len + num_prior_withdrawals >= partial_withdrawal_bound) { + break; + } + var validator: types.phase0.Validator.Type = undefined; + try validators.getValue(undefined, withdrawal.validator_index, &validator); + + const balance_gop = try validator_balance_after_withdrawals.getOrPut(withdrawal.validator_index); + if (!balance_gop.found_existing) { + balance_gop.value_ptr.* = try balances.get(withdrawal.validator_index); + } + const balance: u64 = balance_gop.value_ptr.*; + + if (validator.exit_epoch == c.FAR_FUTURE_EPOCH and + validator.effective_balance >= preset.MIN_ACTIVATION_BALANCE and + balance > preset.MIN_ACTIVATION_BALANCE) + { + const balance_over_min_activation_balance = balance - preset.MIN_ACTIVATION_BALANCE; + const withdrawable_balance = if (balance_over_min_activation_balance < withdrawal.amount) balance_over_min_activation_balance else withdrawal.amount; + var execution_address: ExecutionAddress = undefined; + @memcpy(&execution_address, validator.withdrawal_credentials[12..]); + try pending_partial_withdrawals_result.append(.{ + .index = withdrawal_index.*, + .validator_index = withdrawal.validator_index, + .address = execution_address, + .amount = withdrawable_balance, + }); + withdrawal_index.* += 1; + try validator_balance_after_withdrawals.put(withdrawal.validator_index, balance - withdrawable_balance); + } + processed_count += 1; + } + + return .{ .withdrawals = pending_partial_withdrawals_result, .processed_count = processed_count }; +} + +fn getValidatorsSweepWithdrawals( + comptime fork: ForkSeq, + allocator: Allocator, + epoch: Epoch, + state: *BeaconState(fork), + withdrawal_index: *u64, + num_prior_withdrawals: usize, + validator_balance_after_withdrawals: *std.AutoHashMap(ValidatorIndex, usize), +) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { + // There must be at least one space reserved for validator sweep withdrawals + if (num_prior_withdrawals >= preset.MAX_WITHDRAWALS_PER_PAYLOAD) { + return error.PriorWithdrawalsExceedLimit; + } + + var sweep_withdrawals = std.ArrayList(Withdrawal).init(allocator); + errdefer sweep_withdrawals.deinit(); + + var validators = try state.validators(); + var balances = try state.balances(); + const next_withdrawal_validator_index = try state.nextWithdrawalValidatorIndex(); const validators_count = try validators.length(); const bound = @min(validators_count, preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); + // Just run a bounded loop max iterating over all withdrawals // however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD var n: usize = 0; while (n < bound) : (n += 1) { + if (sweep_withdrawals.items.len + num_prior_withdrawals >= preset.MAX_WITHDRAWALS_PER_PAYLOAD) { + break; + } + // Get next validator in turn const validator_index = (next_withdrawal_validator_index + n) % validators_count; var validator = try validators.get(validator_index); - const withdraw_balance_gop = try withdrawal_balances.getOrPut(validator_index); - const withdraw_balance: u64 = if (withdraw_balance_gop.found_existing) withdraw_balance_gop.value_ptr.* else 0; - const val_balance = try balances.get(validator_index); - const balance = if (comptime fork.gte(.electra)) - // Deduct partially withdrawn balance already queued above - if (val_balance > withdraw_balance) val_balance - withdraw_balance else 0 - else - val_balance; + + const balance_gop = try validator_balance_after_withdrawals.getOrPut(validator_index); + if (!balance_gop.found_existing) { + balance_gop.value_ptr.* = try balances.get(validator_index); + } + const balance: u64 = balance_gop.value_ptr.*; const withdrawable_epoch = try validator.get("withdrawable_epoch"); const withdrawal_credentials = try validator.getFieldRoot("withdrawal_credentials"); @@ -179,42 +372,145 @@ pub fn getExpectedWithdrawals( if (withdrawable_epoch <= epoch) { var execution_address: ExecutionAddress = undefined; @memcpy(&execution_address, withdrawal_credentials[12..]); - try withdrawals_result.withdrawals.append(allocator, .{ - .index = withdrawal_index, + try sweep_withdrawals.append(.{ + .index = withdrawal_index.*, .validator_index = validator_index, .address = execution_address, .amount = balance, }); - withdrawal_index += 1; - } else if ((effective_balance == if (comptime fork.gte(.electra)) - getMaxEffectiveBalance(withdrawal_credentials) - else - preset.MAX_EFFECTIVE_BALANCE) and balance > effective_balance) - { + withdrawal_index.* += 1; + balance_gop.value_ptr.* = 0; + } else if (isPartiallyWithdrawableValidator(fork, withdrawal_credentials, effective_balance, balance)) { // capella partial withdrawal - const partial_amount = balance - effective_balance; + const max_effective_balance = if (comptime fork.gte(.electra)) getMaxEffectiveBalance(withdrawal_credentials) else preset.MAX_EFFECTIVE_BALANCE; + const partial_amount = balance - max_effective_balance; var execution_address: ExecutionAddress = undefined; @memcpy(&execution_address, withdrawal_credentials[12..]); - try withdrawals_result.withdrawals.append(allocator, .{ - .index = withdrawal_index, + try sweep_withdrawals.append(.{ + .index = withdrawal_index.*, .validator_index = validator_index, .address = execution_address, .amount = partial_amount, }); - withdrawal_index += 1; - try withdrawal_balances.put(validator_index, withdraw_balance + partial_amount); + withdrawal_index.* += 1; + balance_gop.value_ptr.* = balance - partial_amount; } + } - // Break if we have enough to pack the block - if (withdrawals_result.withdrawals.items.len >= preset.MAX_WITHDRAWALS_PER_PAYLOAD) { - break; + return .{ .withdrawals = sweep_withdrawals, .processed_count = n }; +} + +fn getBuilderWithdrawals( + allocator: Allocator, + state: *BeaconState(.gloas), + withdrawal_index: *u64, + prior_withdrawals_len: usize, + builder_balance_after_withdrawals: *std.AutoHashMap(u64, u64), +) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { + const withdrawals_limit = preset.MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if (prior_withdrawals_len > withdrawals_limit) { + return error.PriorWithdrawalsExceedLimit; + } + var builder_withdrawals = std.ArrayList(Withdrawal).init(allocator); + errdefer builder_withdrawals.deinit(); + + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + const builder_pending_withdrawals_len = try builder_pending_withdrawals.length(); + var bw_it = builder_pending_withdrawals.iteratorReadonly(0); + + var processed_count: usize = 0; + for (0..builder_pending_withdrawals_len) |_| { + // Check combined length against limit + const all_withdrawals = prior_withdrawals_len + builder_withdrawals.items.len; + if (all_withdrawals >= withdrawals_limit) break; + + const bw = try bw_it.nextValue(allocator); + const builder_index = bw.builder_index; + + // Get builder balance (from builder.balance, not state.balances) + const balance_gop = try builder_balance_after_withdrawals.getOrPut(builder_index); + if (!balance_gop.found_existing) { + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + balance_gop.value_ptr.* = builder.balance; } + + // Use the withdrawal amount directly as specified in the spec + try builder_withdrawals.append(.{ + .index = withdrawal_index.*, + .validator_index = convertBuilderIndexToValidatorIndex(builder_index), + .address = bw.fee_recipient, + .amount = bw.amount, + }); + withdrawal_index.* += 1; + balance_gop.value_ptr.* -= bw.amount; + + processed_count += 1; } - try state.setNextWithdrawalIndex(withdrawal_index); + return .{ .withdrawals = builder_withdrawals, .processed_count = processed_count }; +} - withdrawals_result.sampled_validators = n; - withdrawals_result.processed_partial_withdrawals_count = processed_partial_withdrawals_count; +fn getBuildersSweepWithdrawals( + allocator: Allocator, + state: *BeaconState(.gloas), + epoch: u64, + withdrawal_index: *u64, + num_prior_withdrawals: usize, + builder_balance_after_withdrawals: *std.AutoHashMap(u64, u64), +) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { + const withdrawals_limit = preset.MAX_WITHDRAWALS_PER_PAYLOAD - 1; + if (num_prior_withdrawals > withdrawals_limit) { + return error.PriorWithdrawalsExceedLimit; + } + var builders_sweep_withdrawals = std.ArrayList(Withdrawal).init(allocator); + errdefer builders_sweep_withdrawals.deinit(); + + var builders = try state.inner.get("builders"); + const builders_len: u64 = try builders.length(); + + // Return early if no builders + if (builders_len == 0) { + return .{ .withdrawals = builders_sweep_withdrawals, .processed_count = 0 }; + } + + const builders_limit = @min(builders_len, preset.MAX_BUILDERS_PER_WITHDRAWALS_SWEEP); + const next_withdrawal_builder_index: u64 = try state.inner.get("next_withdrawal_builder_index"); + var processed_count: usize = 0; + + for (0..builders_limit) |n| { + if (builders_sweep_withdrawals.items.len + num_prior_withdrawals >= withdrawals_limit) break; + + // Get next builder in turn + const builder_index: u64 = (next_withdrawal_builder_index + n) % builders_len; + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + // Get builder balance (may have been decremented by builder withdrawals above) + const balance_gop = try builder_balance_after_withdrawals.getOrPut(builder_index); + if (!balance_gop.found_existing) { + balance_gop.value_ptr.* = builder.balance; + } + const balance = balance_gop.value_ptr.*; + + // Check if builder is withdrawable and has balance + if (builder.withdrawable_epoch <= epoch and balance > 0) { + // Withdraw full balance to builder's execution address + try builders_sweep_withdrawals.append(.{ + .index = withdrawal_index.*, + .validator_index = convertBuilderIndexToValidatorIndex(builder_index), + .address = builder.execution_address, + .amount = balance, + }); + withdrawal_index.* += 1; + balance_gop.value_ptr.* = 0; + } + + processed_count += 1; + } + + return .{ .withdrawals = builders_sweep_withdrawals, .processed_count = processed_count }; } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; diff --git a/src/state_transition/utils/validator.zig b/src/state_transition/utils/validator.zig index 1be48da44..196d68128 100644 --- a/src/state_transition/utils/validator.zig +++ b/src/state_transition/utils/validator.zig @@ -12,6 +12,8 @@ const ForkSeq = @import("config").ForkSeq; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const WithdrawalCredentials = types.primitive.Root.Type; const hasCompoundingWithdrawalCredential = @import("./electra.zig").hasCompoundingWithdrawalCredential; +const hasExecutionWithdrawalCredential = @import("./electra.zig").hasExecutionWithdrawalCredential; +const hasEth1WithdrawalCredential = @import("./capella.zig").hasEth1WithdrawalCredential; pub fn isActiveValidator(validator: *const Validator.Type, epoch: Epoch) bool { return validator.activation_epoch <= epoch and epoch < validator.exit_epoch; @@ -84,6 +86,30 @@ pub fn getMaxEffectiveBalance(withdrawal_credentials: *const WithdrawalCredentia return preset.MIN_ACTIVATION_BALANCE; } +pub fn isPartiallyWithdrawableValidator(comptime fork: ForkSeq, withdrawal_credentials: *const WithdrawalCredentials, effective_balance: u64, balance: u64) bool { + // Check withdrawal credentials + const has_withdrawable_credentials = if (comptime fork.gte(.electra)) + hasExecutionWithdrawalCredential(withdrawal_credentials) + else + hasEth1WithdrawalCredential(withdrawal_credentials); + + if (!has_withdrawable_credentials) { + return false; + } + + // Get max effective balance based on fork + const max_effective_balance = if (comptime fork.gte(.electra)) + getMaxEffectiveBalance(withdrawal_credentials) + else + preset.MAX_EFFECTIVE_BALANCE; + + // Check if at max effective balance and has excess balance + const has_max_effective_balance = effective_balance == max_effective_balance; + const has_excess_balance = balance > max_effective_balance; + + return has_max_effective_balance and has_excess_balance; +} + pub fn getPendingBalanceToWithdraw(comptime fork: ForkSeq, state: *BeaconState(fork), validator_index: ValidatorIndex) !u64 { var total: u64 = 0; From 92fe8f2a6e0070a1129bc8a8844d93d1a425973d Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:08:40 +0530 Subject: [PATCH 09/30] fix withdrawals and ExecutionPayloadBid for 7732 --- src/state_transition/block/process_block.zig | 97 ++++++++++--------- .../block/process_withdrawals.zig | 2 +- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index 1270d62ea..691629b5e 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -55,56 +55,59 @@ pub fn processBlock( const body = block.body(); const current_epoch = epoch_cache.epoch; - // The call to the process_execution_payload must happen before the call to the process_randao as the former depends - // on the randao_mix computed with the reveal of the previous block. - if (comptime fork.gte(.bellatrix)) { - if (isExecutionEnabled(fork, state, block_type, block)) { - // TODO Deneb: Allow to disable withdrawals for interop testing - // https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals - if (comptime fork.gte(.capella) and fork.lt(.gloas)) { - // TODO: given max withdrawals of MAX_WITHDRAWALS_PER_PAYLOAD, can use fixed size array instead of heap alloc - var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( - allocator, - preset.MAX_WITHDRAWALS_PER_PAYLOAD, - ) }; - var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); - defer withdrawal_balances.deinit(); + // TODO Deneb: Allow to disable withdrawals for interop testing + // https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals + if (comptime fork.gte(.capella)) { + // TODO: given max withdrawals of MAX_WITHDRAWALS_PER_PAYLOAD, can use fixed size array instead of heap alloc + var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( + allocator, + preset.MAX_WITHDRAWALS_PER_PAYLOAD, + ) }; + var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); + defer withdrawal_balances.deinit(); - try getExpectedWithdrawals( - fork, - allocator, - epoch_cache, - state, - &withdrawals_result, - &withdrawal_balances, - ); - defer withdrawals_result.withdrawals.deinit(allocator); + try getExpectedWithdrawals( + fork, + allocator, + epoch_cache, + state, + &withdrawals_result, + &withdrawal_balances, + ); + defer withdrawals_result.withdrawals.deinit(allocator); - const payload_withdrawals_root = switch (block_type) { - .full => blk: { - const actual_withdrawals = block.body().executionPayload().inner.withdrawals; - std.debug.assert(withdrawals_result.withdrawals.items.len == actual_withdrawals.items.len); - var root: Root = undefined; - try types.capella.Withdrawals.hashTreeRoot(allocator, &actual_withdrawals, &root); - break :blk root; - }, - .blinded => block.body().executionPayloadHeader().inner.withdrawals_root, - }; - try processWithdrawals(fork, allocator, state, withdrawals_result, payload_withdrawals_root); - } + if (comptime fork.gte(.gloas)) { + // After EIP-7732, processWithdrawals does not take a payload parameter + // Withdrawals are verified later in processExecutionPayloadEnvelope + const empty_root: Root = .{0} ** 32; + try processWithdrawals(fork, allocator, state, withdrawals_result, empty_root); + } else { + const payload_withdrawals_root = switch (block_type) { + .full => blk: { + const actual_withdrawals = block.body().executionPayload().inner.withdrawals; + std.debug.assert(withdrawals_result.withdrawals.items.len == actual_withdrawals.items.len); + var root: Root = undefined; + try types.capella.Withdrawals.hashTreeRoot(allocator, &actual_withdrawals, &root); + break :blk root; + }, + .blinded => block.body().executionPayloadHeader().inner.withdrawals_root, + }; + try processWithdrawals(fork, allocator, state, withdrawals_result, payload_withdrawals_root); + } + } - if (comptime fork.lt(.gloas)) { - try processExecutionPayload( - fork, - allocator, - config, - state, - current_epoch, - block_type, - body, - external_data, - ); - } + if (comptime fork.gte(.bellatrix) and fork.lt(.gloas)) { + if (isExecutionEnabled(fork, state, block_type, block)) { + try processExecutionPayload( + fork, + allocator, + config, + state, + current_epoch, + block_type, + body, + external_data, + ); } } diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 641f5b0f4..b7b7b6129 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -230,7 +230,7 @@ fn applyWithdrawals( withdrawals: []const Withdrawal, ) !void { for (withdrawals) |withdrawal| { - if (comptime fork.gte(.gloas) and isBuilderIndex(withdrawal.validator_index)) { + if (fork.gte(.gloas) and isBuilderIndex(withdrawal.validator_index)) { // Handle builder withdrawal const builder_index = convertValidatorIndexToBuilderIndex(withdrawal.validator_index); var builders = try state.inner.get("builders"); From 51456473424f17b20ccb84e797f6e80175586c23 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:13:53 +0530 Subject: [PATCH 10/30] add payload attestation processing to process operations --- .../block/process_operations.zig | 4 +-- .../block/process_payload_attestation.zig | 33 +++++++++++++++++++ src/state_transition/cache/epoch_cache.zig | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/state_transition/block/process_payload_attestation.zig diff --git a/src/state_transition/block/process_operations.zig b/src/state_transition/block/process_operations.zig index 620e1967b..de7f3c9dd 100644 --- a/src/state_transition/block/process_operations.zig +++ b/src/state_transition/block/process_operations.zig @@ -21,6 +21,7 @@ const processVoluntaryExit = @import("./process_voluntary_exit.zig").processVolu const processWithdrawalRequest = @import("./process_withdrawal_request.zig").processWithdrawalRequest; const Node = @import("persistent_merkle_tree").Node; const ProcessBlockOpts = @import("./process_block.zig").ProcessBlockOpts; +const processPayloadAttestation = @import("./process_payload_attestation.zig").processPayloadAttestation; pub fn processOperations( comptime fork: ForkSeq, @@ -92,8 +93,7 @@ pub fn processOperations( if (comptime fork.gte(.gloas)) { for (body.inner.payload_attestations.items) |*payload_attestation| { - _ = payload_attestation; - // TODO: call processPayloadAttestation + try processPayloadAttestation(allocator, config, epoch_cache, state, payload_attestation); } } } diff --git a/src/state_transition/block/process_payload_attestation.zig b/src/state_transition/block/process_payload_attestation.zig new file mode 100644 index 000000000..31081063d --- /dev/null +++ b/src/state_transition/block/process_payload_attestation.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const types = @import("consensus_types"); +const BeaconState = @import("fork_types").BeaconState; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const BeaconConfig = @import("config").BeaconConfig; +const isValidIndexedPayloadAttestation = @import("./is_valid_indexed_payload_attestation.zig").isValidIndexedPayloadAttestation; + +pub fn processPayloadAttestation( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + payload_attestation: *const types.gloas.PayloadAttestation.Type, +) !void { + const data = &payload_attestation.data; + + var latest_block_header = try state.latestBlockHeader(); + const parent_root = try latest_block_header.getFieldRoot("parent_root"); + if (!std.mem.eql(u8, &data.beacon_block_root, parent_root)) { + return error.PayloadAttestationWrongBlock; + } + + if (data.slot + 1 != try state.slot()) { + return error.PayloadAttestationNotFromPreviousSlot; + } + + const indexed_payload_attestation = try epoch_cache.getIndexedPayloadAttestation(allocator, data.slot, payload_attestation); + + if (!(try isValidIndexedPayloadAttestation(allocator, config, epoch_cache, &indexed_payload_attestation, true))) { + return error.InvalidPayloadAttestation; + } +} diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index 7576cd282..9a014b845 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -895,7 +895,7 @@ pub const EpochCache = struct { pub fn getPayloadTimelinessCommittee(self: *const EpochCache, slot: Slot) ![]const ValidatorIndex { const epoch = computeEpochAtSlot(slot); - if (epoch < self.config.GLOAS_FORK_EPOCH) { + if (epoch < self.config.chain.GLOAS_FORK_EPOCH) { return error.PtcNotAvailableBeforeGloas; } From 28dd2df721692186a3aaf5e204935f63bee16c50 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:39:08 +0530 Subject: [PATCH 11/30] epbs attestation changes --- .../block/process_attestation_altair.zig | 91 +++++++++++++++++-- .../slot/upgrade_state_to_altair.zig | 2 +- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index 515600f0f..3f9f27e1a 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -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; @@ -49,10 +52,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); @@ -81,7 +92,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 @@ -122,13 +143,50 @@ 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( @@ -136,7 +194,8 @@ pub fn getAttestationParticipationStatus( 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 @@ -148,9 +207,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; diff --git a/src/state_transition/slot/upgrade_state_to_altair.zig b/src/state_transition/slot/upgrade_state_to_altair.zig index 5733440e5..f67ccb5c1 100644 --- a/src/state_transition/slot/upgrade_state_to_altair.zig +++ b/src/state_transition/slot/upgrade_state_to_altair.zig @@ -107,7 +107,7 @@ fn translateParticipation( for (pending_attestations) |*attestation| { const data = &attestation.data; - const attestation_flag = try getAttestationParticipationStatus(.phase0, data, attestation.inclusion_delay, epoch_cache.epoch, root_cache); + const attestation_flag = try getAttestationParticipationStatus(.phase0, data, attestation.inclusion_delay, epoch_cache.epoch, root_cache, null); const committee_indices = try epoch_cache.getBeaconCommittee(data.slot, data.index); const attesting_indices = try attestation.aggregation_bits.intersectValues(ValidatorIndex, allocator, committee_indices); defer attesting_indices.deinit(); From 641b589801f9e6409f9f4936fa61ec088d5d844d Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:51:18 +0530 Subject: [PATCH 12/30] add builder voluntary exit support --- bindings/napi/BeaconStateView.zig | 2 + .../block/process_operations.zig | 2 +- .../block/process_voluntary_exit.zig | 95 +++++++++++++++++-- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index a92dd04cd..f37e1ebcd 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -676,6 +676,7 @@ pub fn BeaconStateView_getVoluntaryExitValidity(env: napi.Env, cb: napi.Callback const result = switch (cached_state.state.forkSeq()) { inline else => |f| st.getVoluntaryExitValidity( f, + allocator, cached_state.config, cached_state.epoch_cache, cached_state.state.castToFork(f), @@ -707,6 +708,7 @@ pub fn BeaconStateView_isValidVoluntaryExit(env: napi.Env, cb: napi.CallbackInfo const result = switch (cached_state.state.forkSeq()) { inline else => |f| st.isValidVoluntaryExit( f, + allocator, cached_state.config, cached_state.epoch_cache, cached_state.state.castToFork(f), diff --git a/src/state_transition/block/process_operations.zig b/src/state_transition/block/process_operations.zig index de7f3c9dd..e33d6e446 100644 --- a/src/state_transition/block/process_operations.zig +++ b/src/state_transition/block/process_operations.zig @@ -67,7 +67,7 @@ pub fn processOperations( } for (body.inner.voluntary_exits.items) |*voluntary_exit| { - try processVoluntaryExit(fork, config, epoch_cache, state, voluntary_exit, opts.verify_signature); + try processVoluntaryExit(fork, allocator, config, epoch_cache, state, voluntary_exit, opts.verify_signature); } if (comptime fork.gte(.capella)) { diff --git a/src/state_transition/block/process_voluntary_exit.zig b/src/state_transition/block/process_voluntary_exit.zig index e8ca67969..8440af6b9 100644 --- a/src/state_transition/block/process_voluntary_exit.zig +++ b/src/state_transition/block/process_voluntary_exit.zig @@ -1,3 +1,5 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; const BeaconConfig = @import("config").BeaconConfig; const ForkSeq = @import("config").ForkSeq; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; @@ -9,21 +11,36 @@ const getPendingBalanceToWithdraw = @import("../utils/validator.zig").getPending const isActiveValidatorView = @import("../utils/validator.zig").isActiveValidatorView; const verifyVoluntaryExitSignature = @import("../signature_sets/voluntary_exits.zig").verifyVoluntaryExitSignature; const initiateValidatorExit = @import("./initiate_validator_exit.zig").initiateValidatorExit; +const gloas_utils = @import("../utils/gloas.zig"); +const isBuilderIndex = gloas_utils.isBuilderIndex; +const convertValidatorIndexToBuilderIndex = gloas_utils.convertValidatorIndexToBuilderIndex; +const isActiveBuilder = gloas_utils.isActiveBuilder; +const getPendingBalanceToWithdrawForBuilder = gloas_utils.getPendingBalanceToWithdrawForBuilder; +const initiateBuilderExit = gloas_utils.initiateBuilderExit; const FAR_FUTURE_EPOCH = c.FAR_FUTURE_EPOCH; pub fn processVoluntaryExit( comptime fork: ForkSeq, + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *EpochCache, state: *BeaconState(fork), signed_voluntary_exit: *const SignedVoluntaryExit, verify_signature: bool, ) !void { - if (!try isValidVoluntaryExit(fork, config, epoch_cache, state, signed_voluntary_exit, verify_signature)) { + const voluntary_exit = signed_voluntary_exit.message; + + const validity = try getVoluntaryExitValidity(fork, allocator, config, epoch_cache, state, signed_voluntary_exit, verify_signature); + if (validity != .valid) { return error.InvalidVoluntaryExit; } + if (fork.gte(.gloas) and isBuilderIndex(voluntary_exit.validator_index)) { + try initiateBuilderExit(state, allocator, convertValidatorIndexToBuilderIndex(voluntary_exit.validator_index)); + return; + } + var validators = try state.validators(); const validator = try validators.get(@intCast(signed_voluntary_exit.message.validator_index)); try initiateValidatorExit(fork, config, epoch_cache, state, validator); @@ -31,13 +48,14 @@ pub fn processVoluntaryExit( pub fn isValidVoluntaryExit( comptime fork: ForkSeq, + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *const EpochCache, state: *BeaconState(fork), signed_voluntary_exit: *const SignedVoluntaryExit, verify_signature: bool, ) !bool { - return try getVoluntaryExitValidity(fork, config, epoch_cache, state, signed_voluntary_exit, verify_signature) == .valid; + return try getVoluntaryExitValidity(fork, allocator, config, epoch_cache, state, signed_voluntary_exit, verify_signature) == .valid; } pub const VoluntaryExitValidity = enum { @@ -51,6 +69,74 @@ pub const VoluntaryExitValidity = enum { }; pub fn getVoluntaryExitValidity( + comptime fork: ForkSeq, + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(fork), + signed_voluntary_exit: *const SignedVoluntaryExit, + verify_signature: bool, +) !VoluntaryExitValidity { + const current_epoch = epoch_cache.epoch; + const voluntary_exit = signed_voluntary_exit.message; + + // Exits must specify an epoch when they become valid; they are not valid before then + if (current_epoch < voluntary_exit.epoch) { + return .early_epoch; + } + + // Check if this is a builder exit + if (fork.gte(.gloas) and isBuilderIndex(voluntary_exit.validator_index)) { + return getBuilderVoluntaryExitValidity(allocator, config, epoch_cache, state, signed_voluntary_exit, verify_signature); + } + + return getValidatorVoluntaryExitValidity(fork, config, epoch_cache, state, signed_voluntary_exit, verify_signature); +} + +fn getBuilderVoluntaryExitValidity( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_voluntary_exit: *const SignedVoluntaryExit, + verify_signature: bool, +) !VoluntaryExitValidity { + const builder_index = convertValidatorIndexToBuilderIndex(signed_voluntary_exit.message.validator_index); + + var builders = try state.inner.get("builders"); + const builders_len = try builders.length(); + if (builder_index >= builders_len) { + return .inactive; + } + + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + // Verify the builder is active + const finalized_epoch = try state.finalizedEpoch(); + if (!isActiveBuilder(&builder, finalized_epoch)) { + return if (builder.withdrawable_epoch != FAR_FUTURE_EPOCH) + VoluntaryExitValidity.already_exited + else + VoluntaryExitValidity.inactive; + } + + // Only exit builder if it has no pending withdrawals in the queue + if (try getPendingBalanceToWithdrawForBuilder(allocator, state, builder_index) != 0) { + return .pending_withdrawals; + } + + // Verify signature + if (verify_signature) { + if (!try verifyVoluntaryExitSignature(config, epoch_cache, signed_voluntary_exit)) { + return .invalid_signature; + } + } + + return .valid; +} + +fn getValidatorVoluntaryExitValidity( comptime fork: ForkSeq, config: *const BeaconConfig, epoch_cache: *const EpochCache, @@ -80,11 +166,6 @@ pub fn getVoluntaryExitValidity( return .already_exited; } - // exits must specify an epoch when they become valid; they are not valid before then - if (current_epoch < voluntary_exit.epoch) { - return .early_epoch; - } - // verify the validator had been active long enough const activation_epoch = try validator.get("activation_epoch"); if (current_epoch < activation_epoch + config.chain.SHARD_COMMITTEE_PERIOD) { From 2a9a460276e5589f890ba3aad01ef610d3a6f1ff Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:54:40 +0530 Subject: [PATCH 13/30] clear builder pending payemnt on slashing --- .../block/process_proposer_slashing.zig | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/state_transition/block/process_proposer_slashing.zig b/src/state_transition/block/process_proposer_slashing.zig index bfc1146f3..fa0f8d663 100644 --- a/src/state_transition/block/process_proposer_slashing.zig +++ b/src/state_transition/block/process_proposer_slashing.zig @@ -11,6 +11,8 @@ const isSlashableValidator = @import("../utils/validator.zig").isSlashableValida const getProposerSlashingSignatureSets = @import("../signature_sets/proposer_slashings.zig").getProposerSlashingSignatureSets; const verifySignature = @import("../utils/signature_sets.zig").verifySingleSignatureSet; const slashValidator = @import("./slash_validator.zig").slashValidator; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const preset = @import("preset").preset; pub fn processProposerSlashing( comptime fork: ForkSeq, @@ -24,6 +26,27 @@ pub fn processProposerSlashing( ) !void { try buildSlashingsCacheIfNeeded(allocator, state, slashings_cache); try assertValidProposerSlashing(fork, config, epoch_cache, state, proposer_slashing, verify_signatures); + + if (fork.gte(.gloas)) { + const slot = proposer_slashing.signed_header_1.message.slot; + const proposal_epoch = computeEpochAtSlot(slot); + const current_epoch = epoch_cache.epoch; + const previous_epoch = current_epoch - 1; + + const payment_index: ?u64 = if (proposal_epoch == current_epoch) + preset.SLOTS_PER_EPOCH + (slot % preset.SLOTS_PER_EPOCH) + else if (proposal_epoch == previous_epoch) + slot % preset.SLOTS_PER_EPOCH + else + null; + + if (payment_index) |idx| { + var builder_pending_payments = try state.inner.get("builder_pending_payments"); + const default_payment = types.gloas.BuilderPendingPayment.default_value; + try builder_pending_payments.setValue(idx, &default_payment); + } + } + const proposer_index = proposer_slashing.signed_header_1.message.proposer_index; try slashValidator(fork, config, epoch_cache, state, slashings_cache, proposer_index, null); } From dd08a131fae8f8f68557df3986671b325c5ce027 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 28 Mar 2026 18:57:32 +0530 Subject: [PATCH 14/30] add gloas deposit routing in processDepositRequest --- .../block/process_deposit_request.zig | 46 +++++++++++++++++-- .../block/process_operations.zig | 2 +- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/state_transition/block/process_deposit_request.zig b/src/state_transition/block/process_deposit_request.zig index 2b75385ba..b49fd1edd 100644 --- a/src/state_transition/block/process_deposit_request.zig +++ b/src/state_transition/block/process_deposit_request.zig @@ -11,15 +11,51 @@ const BLSPubkey = types.primitive.BLSPubkey.Type; const BLSSignature = types.primitive.BLSSignature.Type; const c = @import("constants"); const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; -const findBuilderIndexByPubkey = @import("../utils/gloas.zig").findBuilderIndexByPubkey; +const gloas_utils = @import("../utils/gloas.zig"); +const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; +const isBuilderWithdrawalCredential = gloas_utils.isBuilderWithdrawalCredential; const isValidDepositSignature = @import("./process_deposit.zig").isValidDepositSignature; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; -pub fn processDepositRequest(comptime fork: ForkSeq, state: *BeaconState(fork), deposit_request: *const DepositRequest) !void { - const deposit_requests_start_index = try state.depositRequestsStartIndex(); - if (deposit_requests_start_index == c.UNSET_DEPOSIT_REQUESTS_START_INDEX) { - try state.setDepositRequestsStartIndex(deposit_request.index); +pub fn processDepositRequest( + comptime fork: ForkSeq, + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(fork), + deposit_request: *const DepositRequest, +) !void { + const pubkey = &deposit_request.pubkey; + const withdrawal_credentials = &deposit_request.withdrawal_credentials; + const amount = deposit_request.amount; + const signature = deposit_request.signature; + + // Check if this is a builder or validator deposit + if (fork.gte(.gloas)) { + const builder_index = try findBuilderIndexByPubkey(allocator, state, pubkey); + const validator_index = epoch_cache.getValidatorIndex(pubkey); + + const is_builder = builder_index != null; + const is_validator = validator_index != null; + const is_builder_prefix = isBuilderWithdrawalCredential(withdrawal_credentials); + + // Route to builder if it's an existing builder OR has builder prefix and is not a validator + if (is_builder or (is_builder_prefix and !is_validator)) { + // Apply builder deposits immediately + try applyDepositForBuilder(allocator, config, state, pubkey, withdrawal_credentials, amount, signature, try state.slot()); + return; + } + } + + // Only set deposit_requests_start_index in Electra fork, not after EIP-7732 + if (comptime fork.lt(.gloas)) { + const deposit_requests_start_index = try state.depositRequestsStartIndex(); + if (deposit_requests_start_index == c.UNSET_DEPOSIT_REQUESTS_START_INDEX) { + try state.setDepositRequestsStartIndex(deposit_request.index); + } } + // Add validator deposits to the queue const pending_deposit = PendingDeposit{ .pubkey = deposit_request.pubkey, .withdrawal_credentials = deposit_request.withdrawal_credentials, diff --git a/src/state_transition/block/process_operations.zig b/src/state_transition/block/process_operations.zig index e33d6e446..61be4e1b7 100644 --- a/src/state_transition/block/process_operations.zig +++ b/src/state_transition/block/process_operations.zig @@ -79,7 +79,7 @@ pub fn processOperations( if (comptime fork.gte(.electra) and fork.lt(.gloas)) { const execution_requests = &body.inner.execution_requests; for (execution_requests.deposits.items) |*deposit_request| { - try processDepositRequest(fork, state, deposit_request); + try processDepositRequest(fork, allocator, config, epoch_cache, state, deposit_request); } for (execution_requests.withdrawals.items) |*withdrawal_request| { From 34e8ee411409e5215763c1547bcf43118a1cca68 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 30 Mar 2026 01:41:59 +0530 Subject: [PATCH 15/30] add processExecutionPayloadEnvelope --- .../process_execution_payload_envelope.zig | 172 ++++++++++++++++++ .../execution_payload_envelope.zig | 48 +++++ 2 files changed, 220 insertions(+) create mode 100644 src/state_transition/block/process_execution_payload_envelope.zig create mode 100644 src/state_transition/signature_sets/execution_payload_envelope.zig diff --git a/src/state_transition/block/process_execution_payload_envelope.zig b/src/state_transition/block/process_execution_payload_envelope.zig new file mode 100644 index 000000000..dc33df9ce --- /dev/null +++ b/src/state_transition/block/process_execution_payload_envelope.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const types = @import("consensus_types"); +const BeaconConfig = @import("config").BeaconConfig; +const ForkSeq = @import("config").ForkSeq; +const BeaconState = @import("fork_types").BeaconState; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const preset = @import("preset").preset; +const c = @import("constants"); +const computeTimeAtSlot = @import("../utils/epoch.zig").computeTimeAtSlot; +const processDepositRequest = @import("./process_deposit_request.zig").processDepositRequest; +const processWithdrawalRequest = @import("./process_withdrawal_request.zig").processWithdrawalRequest; +const processConsolidationRequest = @import("./process_consolidation_request.zig").processConsolidationRequest; +const getExecutionPayloadEnvelopeSignatureSet = @import("../signature_sets/execution_payload_envelope.zig").getExecutionPayloadEnvelopeSignatureSet; +const verifySingleSignatureSet = @import("../utils/signature_sets.zig").verifySingleSignatureSet; + +pub const ProcessExecutionPayloadEnvelopeOpts = struct { + verify_signature: bool = true, + verify_state_root: bool = true, +}; + +pub fn processExecutionPayloadEnvelope( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_envelope: *const types.gloas.SignedExecutionPayloadEnvelope.Type, + opts: ProcessExecutionPayloadEnvelopeOpts, +) !void { + const envelope = &signed_envelope.message; + const payload = &envelope.payload; + if (opts.verify_signature) { + if (!(try verifyExecutionPayloadEnvelopeSignature(allocator, config, epoch_cache, state, signed_envelope))) { + return error.InvalidEnvelopeSignature; + } + } + + try validateExecutionPayloadEnvelope(allocator, config, state, envelope); + + const fork = config.getForkSeqAtSlot(envelope.slot); + const requests = &envelope.execution_requests; + + for (requests.deposits.items) |*deposit| { + try processDepositRequest(fork, allocator, config, epoch_cache, state, deposit); + } + + for (requests.withdrawals.items) |*withdrawal| { + try processWithdrawalRequest(fork, config, epoch_cache, state, withdrawal); + } + + for (requests.consolidations.items) |*consolidation| { + try processConsolidationRequest(fork, config, epoch_cache, state, consolidation); + } + + // Queue the builder payment + const payment_index = preset.SLOTS_PER_EPOCH + (try state.slot() % preset.SLOTS_PER_EPOCH); + var builder_pending_payments = try state.inner.get("builder_pending_payments"); + var payment: types.gloas.BuilderPendingPayment.Type = undefined; + try builder_pending_payments.getValue(allocator, payment_index, &payment); + const amount = payment.withdrawal.amount; + + if (amount > 0) { + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + try builder_pending_withdrawals.pushValue(&payment.withdrawal); + } + + const default_payment = types.gloas.BuilderPendingPayment.default_value; + try builder_pending_payments.setValue(payment_index, &default_payment); + + // Cache the execution payload hash + var execution_payload_availability = try state.inner.get("execution_payload_availability"); + try execution_payload_availability.set(try state.slot() % preset.SLOTS_PER_HISTORICAL_ROOT, true); + try state.inner.setValue("latest_block_hash", &payload.block_hash); + + if (opts.verify_state_root) { + const state_root = try state.hashTreeRoot(); + if (!std.mem.eql(u8, &envelope.state_root, state_root)) { + return error.EnvelopeStateRootMismatch; + } + } +} + +fn validateExecutionPayloadEnvelope( + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(.gloas), + envelope: *const types.gloas.ExecutionPayloadEnvelope.Type, +) !void { + const payload = &envelope.payload; + + // Cache latest block header state root + var latest_block_header = try state.latestBlockHeader(); + const latest_header_state_root = try latest_block_header.getFieldRoot("state_root"); + if (std.mem.eql(u8, latest_header_state_root, &c.ZERO_HASH)) { + const previous_state_root = try state.hashTreeRoot(); + try latest_block_header.setValue("state_root", previous_state_root); + } + + // Verify consistency with the beacon block + const latest_block_header_root = try latest_block_header.hashTreeRoot(); + if (!std.mem.eql(u8, &envelope.beacon_block_root, latest_block_header_root)) { + return error.EnvelopeBlockRootMismatch; + } + + // Verify slot + if (envelope.slot != try state.slot()) { + return error.EnvelopeSlotMismatch; + } + + // Verify consistency with the committed bid + var committed_bid: types.gloas.ExecutionPayloadBid.Type = undefined; + try state.inner.getValue(allocator, "latest_execution_payload_bid", &committed_bid); + + if (envelope.builder_index != committed_bid.builder_index) { + return error.EnvelopeBuilderIndexMismatch; + } + + if (!std.mem.eql(u8, &committed_bid.prev_randao, &payload.prev_randao)) { + return error.EnvelopePrevRandaoMismatch; + } + + // Verify consistency with expected withdrawals + var payload_withdrawals_root: [32]u8 = undefined; + try types.capella.Withdrawals.hashTreeRoot(allocator, &payload.withdrawals, &payload_withdrawals_root); + var expected_withdrawals = try state.inner.get("payload_expected_withdrawals"); + var expected_withdrawals_root: [32]u8 = undefined; + try expected_withdrawals.hashTreeRootInto(&expected_withdrawals_root); + if (!std.mem.eql(u8, &payload_withdrawals_root, &expected_withdrawals_root)) { + return error.EnvelopeWithdrawalsMismatch; + } + + // Verify the gas_limit + if (committed_bid.gas_limit != payload.gas_limit) { + return error.EnvelopeGasLimitMismatch; + } + + // Verify the block hash + if (!std.mem.eql(u8, &committed_bid.block_hash, &payload.block_hash)) { + return error.EnvelopeBlockHashMismatch; + } + + // Verify consistency of the parent hash with respect to the previous execution payload + const latest_block_hash = try state.inner.getFieldRoot("latest_block_hash"); + if (!std.mem.eql(u8, &payload.parent_hash, latest_block_hash)) { + return error.EnvelopeParentHashMismatch; + } + + // Verify timestamp + const expected_timestamp = computeTimeAtSlot(config, try state.slot(), try state.genesisTime()); + if (payload.timestamp != expected_timestamp) { + return error.EnvelopeTimestampMismatch; + } + + // Skipped: Verify the execution payload is valid +} + +fn verifyExecutionPayloadEnvelopeSignature( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_envelope: *const types.gloas.SignedExecutionPayloadEnvelope.Type, +) !bool { + const signature_set = try getExecutionPayloadEnvelopeSignatureSet( + allocator, + config, + epoch_cache, + state, + signed_envelope, + ); + return verifySingleSignatureSet(&signature_set); +} diff --git a/src/state_transition/signature_sets/execution_payload_envelope.zig b/src/state_transition/signature_sets/execution_payload_envelope.zig new file mode 100644 index 000000000..1bf348c09 --- /dev/null +++ b/src/state_transition/signature_sets/execution_payload_envelope.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const types = @import("consensus_types"); +const BeaconConfig = @import("config").BeaconConfig; +const BeaconState = @import("fork_types").BeaconState; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const c = @import("constants"); +const bls = @import("bls"); +const computeSigningRootVariable = @import("../utils/signing_root.zig").computeSigningRootVariable; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const SingleSignatureSet = @import("../utils/signature_sets.zig").SingleSignatureSet; +const createSingleSignatureSetFromComponents = @import("../utils/signature_sets.zig").createSingleSignatureSetFromComponents; + +pub fn getExecutionPayloadEnvelopeSigningRoot( + allocator: Allocator, + config: *const BeaconConfig, + envelope: *const types.gloas.ExecutionPayloadEnvelope.Type, +) ![32]u8 { + const domain = try config.getDomain(computeEpochAtSlot(envelope.slot), c.DOMAIN_BEACON_BUILDER, null); + + var out: [32]u8 = undefined; + try computeSigningRootVariable(types.gloas.ExecutionPayloadEnvelope, allocator, envelope, domain, &out); + return out; +} + +pub fn getExecutionPayloadEnvelopeSignatureSet( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_envelope: *const types.gloas.SignedExecutionPayloadEnvelope.Type, +) !SingleSignatureSet { + const envelope = &signed_envelope.message; + + // Get the pubkey: proposer key for self-builds, builder key otherwise + const proposer_index: u64 = try state.latestBlockHeader().get("proposer_index"); + const pubkey = if (envelope.builder_index == c.BUILDER_INDEX_SELF_BUILD) + epoch_cache.index_to_pubkey.items[proposer_index] + else blk: { + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, envelope.builder_index, &builder); + break :blk bls.PublicKey.uncompress(&builder.pubkey) catch return error.InvalidBuilderPubkey; + }; + + const signing_root = try getExecutionPayloadEnvelopeSigningRoot(allocator, config, envelope); + return createSingleSignatureSetFromComponents(&pubkey, signing_root, signed_envelope.signature); +} From 2bb3d24de2bed4dc66568c499e503bb984343315 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 30 Mar 2026 02:43:05 +0530 Subject: [PATCH 16/30] add gloas fork support to spec test runners --- build.zig | 2 +- src/state_transition/test_utils/generate_state.zig | 9 +++++++++ test/spec/runner/fork.zig | 13 ++++++++++++- test/spec/runner/operations.zig | 5 ++++- test/spec/runner/sanity.zig | 1 + test/spec/test_case.zig | 14 ++++++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 6a683170b..62a3d93be 100644 --- a/build.zig +++ b/build.zig @@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void { const options_spec_test_options = b.addOptions(); const option_spec_test_url = b.option([]const u8, "spec_test_url", "") orelse "https://github.com/ethereum/consensus-specs"; options_spec_test_options.addOption([]const u8, "spec_test_url", option_spec_test_url); - const option_spec_test_version = b.option([]const u8, "spec_test_version", "") orelse "v1.6.0-beta.2"; + const option_spec_test_version = b.option([]const u8, "spec_test_version", "") orelse "v1.7.0-alpha.4"; options_spec_test_options.addOption([]const u8, "spec_test_version", option_spec_test_version); const option_spec_test_out_dir = b.option([]const u8, "spec_test_out_dir", "") orelse "test/spec/spec_tests"; options_spec_test_options.addOption([]const u8, "spec_test_out_dir", option_spec_test_out_dir); diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index bab4cadee..0ceee2a2e 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -287,6 +287,15 @@ pub fn getConfig(config: ChainConfig, fork: ForkSeq, fork_epoch: Epoch) ChainCon .ELECTRA_FORK_EPOCH = 0, .FULU_FORK_EPOCH = fork_epoch, }), + .gloas => return config.merge(.{ + .ALTAIR_FORK_EPOCH = 0, + .BELLATRIX_FORK_EPOCH = 0, + .CAPELLA_FORK_EPOCH = 0, + .DENEB_FORK_EPOCH = 0, + .ELECTRA_FORK_EPOCH = 0, + .FULU_FORK_EPOCH = 0, + .GLOAS_FORK_EPOCH = fork_epoch, + }), } } diff --git a/test/spec/runner/fork.zig b/test/spec/runner/fork.zig index 0e6c0a902..fe1a0f809 100644 --- a/test/spec/runner/fork.zig +++ b/test/spec/runner/fork.zig @@ -8,6 +8,7 @@ const upgradeStateToCapella = state_transition.upgradeStateToCapella; const upgradeStateToDeneb = state_transition.upgradeStateToDeneb; const upgradeStateToElectra = state_transition.upgradeStateToElectra; const upgradeStateToFulu = state_transition.upgradeStateToFulu; +const upgradeStateToGloas = state_transition.upgradeStateToGloas; const TestCachedBeaconState = state_transition.test_utils.TestCachedBeaconState; const AnyBeaconState = @import("fork_types").AnyBeaconState; const test_case = @import("../test_case.zig"); @@ -28,7 +29,7 @@ const Allocator = std.mem.Allocator; pub fn TestCase(comptime target_fork: ForkSeq) type { comptime { switch (target_fork) { - .altair, .bellatrix, .capella, .deneb, .electra, .fulu => {}, + .altair, .bellatrix, .capella, .deneb, .electra, .fulu, .gloas => {}, else => @compileError("fork tests are not defined for " ++ @tagName(target_fork)), } } @@ -153,6 +154,15 @@ pub fn TestCase(comptime target_fork: ForkSeq) type { ); cached_state.state.* = .{ .fulu = upgraded.inner }; }, + .gloas => { + const upgraded = try upgradeStateToGloas( + self.pre.allocator, + config, + epoch_cache, + try cached_state.state.tryCastToFork(.fulu), + ); + cached_state.state.* = .{ .gloas = upgraded.inner }; + }, else => unreachable, } } @@ -185,6 +195,7 @@ fn previousFork(target: ForkSeq) ForkSeq { .deneb => .capella, .electra => .deneb, .fulu => .electra, + .gloas => .fulu, else => @compileError("Unsupported fork transition for " ++ @tagName(target)), }; } diff --git a/test/spec/runner/operations.zig b/test/spec/runner/operations.zig index c7aef4ba0..2dbb9877b 100644 --- a/test/spec/runner/operations.zig +++ b/test/spec/runner/operations.zig @@ -200,7 +200,9 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { try state_transition.processDeposit(fork, allocator, config, epoch_cache, state, &self.op); }, .deposit_request => { - try state_transition.processDepositRequest(fork, state, &self.op); + const config = cached_state.config; + const epoch_cache = cached_state.epoch_cache; + try state_transition.processDepositRequest(fork, allocator, config, epoch_cache, state, &self.op); }, .execution_payload => { const config = cached_state.config; @@ -253,6 +255,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const epoch_cache = cached_state.epoch_cache; try state_transition.processVoluntaryExit( fork, + allocator, config, epoch_cache, state, diff --git a/test/spec/runner/sanity.zig b/test/spec/runner/sanity.zig index 150d96bc1..89744b5c2 100644 --- a/test/spec/runner/sanity.zig +++ b/test/spec/runner/sanity.zig @@ -190,6 +190,7 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { .deneb => AnySignedBeaconBlock{ .full_deneb = block }, .electra => AnySignedBeaconBlock{ .full_electra = block }, .fulu => AnySignedBeaconBlock{ .full_fulu = block }, + .gloas => AnySignedBeaconBlock{ .full_gloas = block }, }; const input_cached_state = if (result) |res| res else self.pre.cached_state; { diff --git a/test/spec/test_case.zig b/test/spec/test_case.zig index 89194b9bc..b1bfca77d 100644 --- a/test/spec/test_case.zig +++ b/test/spec/test_case.zig @@ -18,6 +18,7 @@ const capella = types.capella; const deneb = types.deneb; const electra = types.electra; const fulu = types.fulu; +const gloas = types.gloas; pub const BlsSetting = enum { default, @@ -192,6 +193,14 @@ pub fn loadSignedBeaconBlock(allocator: std.mem.Allocator, fork: ForkSeq, dir: s .full_fulu = out, }; }, + .gloas => blk: { + const out = try allocator.create(gloas.SignedBeaconBlock.Type); + out.* = gloas.SignedBeaconBlock.default_value; + try loadSszSnappyValue(types.gloas.SignedBeaconBlock, allocator, dir, file_name, out); + break :blk AnySignedBeaconBlock{ + .full_gloas = out, + }; + }, }; } @@ -246,6 +255,10 @@ pub fn deinitSignedBeaconBlock(signed_block: AnySignedBeaconBlock, allocator: st fulu.SignedBlindedBeaconBlock.deinit(allocator, @constCast(b)); allocator.destroy(b); }, + .full_gloas => |b| { + gloas.SignedBeaconBlock.deinit(allocator, @constCast(b)); + allocator.destroy(b); + }, } } @@ -308,6 +321,7 @@ pub fn expectEqualBeaconStates(expected: *AnyBeaconState, actual: *AnyBeaconStat .deneb => try Debug.printDiff(types.deneb.BeaconState, .deneb, expected, actual), .electra => try Debug.printDiff(types.electra.BeaconState, .electra, expected, actual), .fulu => try Debug.printDiff(types.fulu.BeaconState, .fulu, expected, actual), + .gloas => try Debug.printDiff(types.gloas.BeaconState, .gloas, expected, actual), } return error.NotEqual; } From 34457e632b42351f4e279f7cd0f43ee59c0e0c4e Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 30 Mar 2026 13:12:49 +0530 Subject: [PATCH 17/30] fix merge --- src/state_transition/block/process_voluntary_exit.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/state_transition/block/process_voluntary_exit.zig b/src/state_transition/block/process_voluntary_exit.zig index 4bde4b072..fb2424984 100644 --- a/src/state_transition/block/process_voluntary_exit.zig +++ b/src/state_transition/block/process_voluntary_exit.zig @@ -189,7 +189,6 @@ fn getValidatorVoluntaryExitValidity( return .valid; } -const std = @import("std"); const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; const Node = @import("persistent_merkle_tree").Node; const preset = @import("preset").preset; @@ -218,6 +217,7 @@ test "voluntary exit - valid" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, test_state.cached_state.state.castToFork(.electra), @@ -241,6 +241,7 @@ test "voluntary exit - inactive validator (out of bounds index)" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, test_state.cached_state.state.castToFork(.electra), @@ -271,6 +272,7 @@ test "voluntary exit - inactive validator (not active in current epoch)" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, state, @@ -301,6 +303,7 @@ test "voluntary exit - already exited validator" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, state, @@ -326,6 +329,7 @@ test "voluntary exit - early epoch" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, test_state.cached_state.state.castToFork(.electra), @@ -356,6 +360,7 @@ test "voluntary exit - short time active" { const result = try getVoluntaryExitValidity( .electra, + allocator, test_state.config, test_state.cached_state.epoch_cache, state, From 8daad8a464d5098590c8ce609a00659767ee0deb Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 30 Mar 2026 13:15:47 +0530 Subject: [PATCH 18/30] skip gloas in block benchmark --- bench/state_transition/process_block.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bench/state_transition/process_block.zig b/bench/state_transition/process_block.zig index 8d16a87c6..bbfe87d72 100644 --- a/bench/state_transition/process_block.zig +++ b/bench/state_transition/process_block.zig @@ -481,6 +481,8 @@ pub fn main() !void { defer allocator.free(block_bytes); inline for (comptime std.enums.values(ForkSeq)) |fork| { + // TODO: add gloas benchmark support + if (comptime fork == .gloas) continue; if (detected_fork == fork) return runBenchmark(fork, allocator, &pool, stdout, state_bytes, block_bytes, chain_config); } return error.NoBenchmarkRan; From c3bc1f085941d32c737ee9cf347844cc8f1472f4 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 30 Mar 2026 13:43:01 +0530 Subject: [PATCH 19/30] fix gloas comment --- src/config/ChainConfig.zig | 4 ++-- src/constants/root.zig | 2 +- src/preset/preset.zig | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/ChainConfig.zig b/src/config/ChainConfig.zig index b73e60e9c..9bd1da845 100644 --- a/src/config/ChainConfig.zig +++ b/src/config/ChainConfig.zig @@ -36,10 +36,10 @@ DENEB_FORK_EPOCH: u64, // ELECTRA ELECTRA_FORK_VERSION: [4]u8, ELECTRA_FORK_EPOCH: u64, -// FULU (assuming it's a future fork, standard pattern) +// FULU FULU_FORK_VERSION: [4]u8, FULU_FORK_EPOCH: u64, -// GLOAS (EIP-7732: Enshrined Proposer-Builder Separation) +// GLOAS GLOAS_FORK_VERSION: [4]u8, GLOAS_FORK_EPOCH: u64, diff --git a/src/constants/root.zig b/src/constants/root.zig index 4c1e42bed..8a9dc5b05 100644 --- a/src/constants/root.zig +++ b/src/constants/root.zig @@ -120,7 +120,7 @@ pub const CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA = 86; pub const G2_POINT_AT_INFINITY: [96]u8 = [_]u8{0xc0} ++ [_]u8{0} ** 95; -// Gloas (EIP-7732) constants +// Gloas pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; pub const BUILDER_INDEX_SELF_BUILD: u64 = std.math.maxInt(u64); pub const BUILDER_PAYMENT_THRESHOLD_NUMERATOR: u64 = 6; diff --git a/src/preset/preset.zig b/src/preset/preset.zig index 3367e917e..60e62203e 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -83,7 +83,7 @@ const PresetMainnet = struct { pub const DEPOSIT_CONTRACT_TREE_DEPTH = 32; pub const GENESIS_SLOT = 0; pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16; - // Gloas (EIP-7732) + // Gloas pub const PTC_SIZE = 512; pub const MAX_PAYLOAD_ATTESTATIONS = 4; pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 @@ -162,7 +162,7 @@ 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; - // Gloas (EIP-7732) + // Gloas pub const PTC_SIZE = 512; pub const MAX_PAYLOAD_ATTESTATIONS = 4; pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 From 14f7e9390bbc9ce7106e75b3d5da31f67cce55a5 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Wed, 1 Apr 2026 20:40:09 +0530 Subject: [PATCH 20/30] fix test infra for gloas --- src/state_transition/epoch/process_epoch.zig | 8 ++ test/spec/runner/operations.zig | 98 ++++++++++++++++---- test/spec/test_case.zig | 1 + test/spec/version.txt | 2 +- test/spec/write_spec_tests.zig | 1 + 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/state_transition/epoch/process_epoch.zig b/src/state_transition/epoch/process_epoch.zig index ad7dce66b..c4d90d1d4 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -26,6 +26,7 @@ const processParticipationFlagUpdates = @import("./process_participation_flag_up const processSyncCommitteeUpdates = @import("./process_sync_committee_updates.zig").processSyncCommitteeUpdates; const processBuilderPendingPayments = @import("./process_builder_pending_payments.zig").processBuilderPendingPayments; const processProposerLookahead = @import("./process_proposer_lookahead.zig").processProposerLookahead; +const processPtcWindow = @import("./process_ptc_window.zig").processPtcWindow; const Node = @import("persistent_merkle_tree").Node; pub fn processEpoch( @@ -109,6 +110,13 @@ pub fn processEpoch( try processProposerLookahead(fork, allocator, epoch_cache, state, cache); try observeEpochTransitionStep(.{ .step = .process_proposer_lookahead }, timer.read()); } + + // [New in Gloas:EIP7732] + if (comptime fork.gte(.gloas)) { + timer = try Timer.start(); + try processPtcWindow(allocator, epoch_cache, state); + try observeEpochTransitionStep(.{ .step = .process_ptc_window }, timer.read()); + } } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; diff --git a/test/spec/runner/operations.zig b/test/spec/runner/operations.zig index 2dbb9877b..e96490fb8 100644 --- a/test/spec/runner/operations.zig +++ b/test/spec/runner/operations.zig @@ -29,6 +29,8 @@ pub const Operation = enum { deposit, deposit_request, execution_payload, + execution_payload_bid, + payload_attestation, proposer_slashing, sync_aggregate, voluntary_exit, @@ -40,6 +42,7 @@ pub const Operation = enum { .block_header => "block", .bls_to_execution_change => "address_change", .execution_payload => "body", + .execution_payload_bid => "block", .withdrawals => "execution_payload", else => @tagName(self), }; @@ -55,6 +58,8 @@ pub const Operation = enum { .deposit => "Deposit", .deposit_request => "DepositRequest", .execution_payload => "BeaconBlockBody", + .execution_payload_bid => "BeaconBlock", + .payload_attestation => "PayloadAttestation", .proposer_slashing => "ProposerSlashing", .sync_aggregate => "SyncAggregate", .voluntary_exit => "SignedVoluntaryExit", @@ -70,10 +75,25 @@ pub const Operation = enum { pub const Handler = Operation; +fn loadExecutionValid(allocator: std.mem.Allocator, dir: std.fs.Dir) bool { + var file = dir.openFile("execution.yaml", .{}) catch return true; + defer file.close(); + const contents = file.readToEndAlloc(allocator, 1024) catch return true; + defer allocator.free(contents); + // Parse "{execution_valid: false}" or "{execution_valid: true}" + if (std.mem.indexOf(u8, contents, "false")) |_| return false; + return true; +} + pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const ForkTypes = @field(ssz, fork.name()); const tc_utils = TestCaseUtils(fork); - const OpType = @field(ForkTypes, operation.operationObject()); + // After EIP-7732, gloas execution_payload tests use SignedExecutionPayloadEnvelope + const is_gloas_exec_payload = comptime (operation == .execution_payload and fork.gte(.gloas)); + const OpType = if (is_gloas_exec_payload) + ForkTypes.SignedExecutionPayloadEnvelope + else + @field(ForkTypes, operation.operationObject()); return struct { pre: TestCachedBeaconState, @@ -81,10 +101,12 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { post: ?*AnyBeaconState, op: OpType.Type, bls_setting: BlsSetting, + execution_valid: bool, const Self = @This(); pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { + const pool_size = if (active_preset == .mainnet) 10_000_000 else 1_000_000; var pool = try Node.Pool.init(allocator, pool_size); defer pool.deinit(); @@ -104,6 +126,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { .post = undefined, .op = OpType.default_value, .bls_setting = loadBlsSetting(allocator, dir), + .execution_valid = loadExecutionValid(allocator, dir), }; // load pre state @@ -114,7 +137,14 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { tc.post = try tc_utils.loadPostState(allocator, pool, dir); // load the op - try loadSszValue(OpType, allocator, dir, comptime operation.inputName() ++ ".ssz_snappy", &tc.op); + // After EIP-7732, gloas withdrawals tests don't have an execution_payload input file + const input_name = comptime if (is_gloas_exec_payload) + "signed_envelope" + else + operation.inputName(); + if (comptime !(operation == .withdrawals and fork.gte(.gloas))) { + try loadSszValue(OpType, allocator, dir, input_name ++ ".ssz_snappy", &tc.op); + } errdefer { if (comptime @hasDecl(OpType, "deinit")) { OpType.deinit(allocator, &tc.op); @@ -207,21 +237,46 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { .execution_payload => { const config = cached_state.config; const epoch_cache = cached_state.epoch_cache; - const current_epoch = epoch_cache.epoch; - const fork_body = BeaconBlockBody(.full, fork){ .inner = self.op }; - try state_transition.processExecutionPayload( - fork, - allocator, - config, - state, - current_epoch, - .full, - &fork_body, - .{ - .data_availability_status = .available, - .execution_payload_status = if (self.post != null) .valid else .invalid, - }, - ); + if (comptime is_gloas_exec_payload) { + // After EIP-7732, execution_payload tests use processExecutionPayloadEnvelope + if (!self.execution_valid) { + return error.ExecutionPayloadInvalid; + } + try state_transition.processExecutionPayloadEnvelope( + allocator, + config, + epoch_cache, + state, + &self.op, + .{ .verify_signature = true, .verify_state_root = true }, + ); + } else { + const current_epoch = epoch_cache.epoch; + const fork_body = BeaconBlockBody(.full, fork){ .inner = self.op }; + try state_transition.processExecutionPayload( + fork, + allocator, + config, + state, + current_epoch, + .full, + &fork_body, + .{ + .data_availability_status = .available, + .execution_payload_status = if (self.post != null) .valid else .invalid, + }, + ); + } + }, + .execution_payload_bid => { + const config = cached_state.config; + const fork_block = BeaconBlock(.full, fork){ .inner = self.op }; + try state_transition.processExecutionPayloadBid(allocator, config, state, &fork_block); + }, + .payload_attestation => { + const config = cached_state.config; + const epoch_cache = cached_state.epoch_cache; + try state_transition.processPayloadAttestation(allocator, config, epoch_cache, state, &self.op); }, .proposer_slashing => { const config = cached_state.config; @@ -276,6 +331,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { preset.MAX_WITHDRAWALS_PER_PAYLOAD, ), }; + defer withdrawals_result.withdrawals.deinit(allocator); var withdrawal_balances = std.AutoHashMap(u64, usize).init(allocator); defer withdrawal_balances.deinit(); @@ -288,11 +344,13 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { &withdrawals_result, &withdrawal_balances, ); - defer withdrawals_result.withdrawals.deinit(allocator); + // After EIP-7732, gloas withdrawals don't use payload verification var payload_withdrawals_root: Root = undefined; - // self.op is ExecutionPayload in this case - try ssz.capella.Withdrawals.hashTreeRoot(allocator, &self.op.withdrawals, &payload_withdrawals_root); + if (comptime fork.lt(.gloas)) { + // self.op is ExecutionPayload in this case + try ssz.capella.Withdrawals.hashTreeRoot(allocator, &self.op.withdrawals, &payload_withdrawals_root); + } try state_transition.processWithdrawals( fork, diff --git a/test/spec/test_case.zig b/test/spec/test_case.zig index b1bfca77d..4b1a4b0ea 100644 --- a/test/spec/test_case.zig +++ b/test/spec/test_case.zig @@ -44,6 +44,7 @@ pub fn TestCaseUtils(comptime fork: ForkSeq) type { .deneb => .capella, .electra => .deneb, .fulu => .electra, + .gloas => .fulu, else => unreachable, }; } diff --git a/test/spec/version.txt b/test/spec/version.txt index 4519d7307..71ac5f304 100644 --- a/test/spec/version.txt +++ b/test/spec/version.txt @@ -2,4 +2,4 @@ // This file exists as a cache key for the spec tests in CI. // Do not commit changes by hand. -v1.6.0-beta.2 +v1.7.0-alpha.4 diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index b41674444..6afa4f1d6 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -11,6 +11,7 @@ const supported_forks = [_]ForkSeq{ .deneb, .electra, .fulu, + .gloas, }; const supported_test_runners = [_]RunnerKind{ From 7ba705db06d9c5f9d03c6fec4b09cc193cdb3aca Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 11:54:40 +0530 Subject: [PATCH 21/30] fix: process_withdrawals spec --- .../block/process_withdrawals.zig | 43 ++++++++++--------- src/state_transition/utils/validator.zig | 9 ++++ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index b7b7b6129..0a5aa8efb 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -13,6 +13,7 @@ const ExecutionAddress = types.primitive.ExecutionAddress.Type; const hasExecutionWithdrawalCredential = @import("../utils/electra.zig").hasExecutionWithdrawalCredential; const hasEth1WithdrawalCredential = @import("../utils/capella.zig").hasEth1WithdrawalCredential; const getMaxEffectiveBalance = @import("../utils/validator.zig").getMaxEffectiveBalance; +const isFullyWithdrawableValidator = @import("../utils/validator.zig").isFullyWithdrawableValidator; const isPartiallyWithdrawableValidator = @import("../utils/validator.zig").isPartiallyWithdrawableValidator; const decreaseBalance = @import("../utils/balance.zig").decreaseBalance; const gloas_utils = @import("../utils/gloas.zig"); @@ -138,7 +139,7 @@ pub fn getExpectedWithdrawals( var withdrawal_index = try state.nextWithdrawalIndex(); // Separate maps to track balances after applying withdrawals - var builder_balance_after_withdrawals = std.AutoHashMap(u64, u64).init(allocator); + var builder_balance_after_withdrawals = std.AutoHashMap(u64, i64).init(allocator); defer builder_balance_after_withdrawals.deinit(); // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) @@ -334,16 +335,19 @@ fn getValidatorsSweepWithdrawals( var sweep_withdrawals = std.ArrayList(Withdrawal).init(allocator); errdefer sweep_withdrawals.deinit(); - var validators = try state.validators(); + const validators = try state.validators(); var balances = try state.balances(); const next_withdrawal_validator_index = try state.nextWithdrawalValidatorIndex(); const validators_count = try validators.length(); - const bound = @min(validators_count, preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); + if (next_withdrawal_validator_index >= validators_count) { + return error.InvalidNextWithdrawalValidatorIndex; + } + const validators_limit = @min(validators_count, preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); // Just run a bounded loop max iterating over all withdrawals // however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD - var n: usize = 0; - while (n < bound) : (n += 1) { + var processed_count: usize = 0; + for (0..validators_limit) |n| { if (sweep_withdrawals.items.len + num_prior_withdrawals >= preset.MAX_WITHDRAWALS_PER_PAYLOAD) { break; } @@ -361,15 +365,8 @@ fn getValidatorsSweepWithdrawals( const withdrawable_epoch = try validator.get("withdrawable_epoch"); const withdrawal_credentials = try validator.getFieldRoot("withdrawal_credentials"); const effective_balance = try validator.get("effective_balance"); - const has_withdrawable_credentials = if (comptime fork.gte(.electra)) hasExecutionWithdrawalCredential(withdrawal_credentials) else hasEth1WithdrawalCredential(withdrawal_credentials); - // early skip for balance = 0 as its now more likely that validator has exited/slashed with - // balance zero than not have withdrawal credentials set - if (balance == 0 or !has_withdrawable_credentials) { - continue; - } - // capella full withdrawal - if (withdrawable_epoch <= epoch) { + if (isFullyWithdrawableValidator(fork, withdrawal_credentials, balance, withdrawable_epoch, epoch)) { var execution_address: ExecutionAddress = undefined; @memcpy(&execution_address, withdrawal_credentials[12..]); try sweep_withdrawals.append(.{ @@ -381,7 +378,6 @@ fn getValidatorsSweepWithdrawals( withdrawal_index.* += 1; balance_gop.value_ptr.* = 0; } else if (isPartiallyWithdrawableValidator(fork, withdrawal_credentials, effective_balance, balance)) { - // capella partial withdrawal const max_effective_balance = if (comptime fork.gte(.electra)) getMaxEffectiveBalance(withdrawal_credentials) else preset.MAX_EFFECTIVE_BALANCE; const partial_amount = balance - max_effective_balance; var execution_address: ExecutionAddress = undefined; @@ -395,9 +391,11 @@ fn getValidatorsSweepWithdrawals( withdrawal_index.* += 1; balance_gop.value_ptr.* = balance - partial_amount; } + + processed_count += 1; } - return .{ .withdrawals = sweep_withdrawals, .processed_count = n }; + return .{ .withdrawals = sweep_withdrawals, .processed_count = processed_count }; } fn getBuilderWithdrawals( @@ -405,7 +403,7 @@ fn getBuilderWithdrawals( state: *BeaconState(.gloas), withdrawal_index: *u64, prior_withdrawals_len: usize, - builder_balance_after_withdrawals: *std.AutoHashMap(u64, u64), + builder_balance_after_withdrawals: *std.AutoHashMap(u64, i64), ) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { const withdrawals_limit = preset.MAX_WITHDRAWALS_PER_PAYLOAD - 1; if (prior_withdrawals_len > withdrawals_limit) { @@ -433,7 +431,7 @@ fn getBuilderWithdrawals( var builders = try state.inner.get("builders"); var builder: types.gloas.Builder.Type = undefined; try builders.getValue(allocator, builder_index, &builder); - balance_gop.value_ptr.* = builder.balance; + balance_gop.value_ptr.* = @intCast(builder.balance); } // Use the withdrawal amount directly as specified in the spec @@ -444,7 +442,7 @@ fn getBuilderWithdrawals( .amount = bw.amount, }); withdrawal_index.* += 1; - balance_gop.value_ptr.* -= bw.amount; + balance_gop.value_ptr.* -= @as(i64, @intCast(bw.amount)); processed_count += 1; } @@ -458,7 +456,7 @@ fn getBuildersSweepWithdrawals( epoch: u64, withdrawal_index: *u64, num_prior_withdrawals: usize, - builder_balance_after_withdrawals: *std.AutoHashMap(u64, u64), + builder_balance_after_withdrawals: *std.AutoHashMap(u64, i64), ) !struct { withdrawals: std.ArrayList(Withdrawal), processed_count: usize } { const withdrawals_limit = preset.MAX_WITHDRAWALS_PER_PAYLOAD - 1; if (num_prior_withdrawals > withdrawals_limit) { @@ -477,6 +475,9 @@ fn getBuildersSweepWithdrawals( const builders_limit = @min(builders_len, preset.MAX_BUILDERS_PER_WITHDRAWALS_SWEEP); const next_withdrawal_builder_index: u64 = try state.inner.get("next_withdrawal_builder_index"); + if (next_withdrawal_builder_index >= builders_len) { + return error.InvalidNextWithdrawalBuilderIndex; + } var processed_count: usize = 0; for (0..builders_limit) |n| { @@ -490,7 +491,7 @@ fn getBuildersSweepWithdrawals( // Get builder balance (may have been decremented by builder withdrawals above) const balance_gop = try builder_balance_after_withdrawals.getOrPut(builder_index); if (!balance_gop.found_existing) { - balance_gop.value_ptr.* = builder.balance; + balance_gop.value_ptr.* = @intCast(builder.balance); } const balance = balance_gop.value_ptr.*; @@ -501,7 +502,7 @@ fn getBuildersSweepWithdrawals( .index = withdrawal_index.*, .validator_index = convertBuilderIndexToValidatorIndex(builder_index), .address = builder.execution_address, - .amount = balance, + .amount = @intCast(balance), }); withdrawal_index.* += 1; balance_gop.value_ptr.* = 0; diff --git a/src/state_transition/utils/validator.zig b/src/state_transition/utils/validator.zig index 5bf17fd0c..184e46d50 100644 --- a/src/state_transition/utils/validator.zig +++ b/src/state_transition/utils/validator.zig @@ -86,6 +86,15 @@ pub fn getMaxEffectiveBalance(withdrawal_credentials: *const WithdrawalCredentia return preset.MIN_ACTIVATION_BALANCE; } +pub fn isFullyWithdrawableValidator(comptime fork: ForkSeq, withdrawal_credentials: *const WithdrawalCredentials, balance: u64, withdrawable_epoch: u64, epoch: u64) bool { + const has_withdrawable_credentials = if (comptime fork.gte(.electra)) + hasExecutionWithdrawalCredential(withdrawal_credentials) + else + hasEth1WithdrawalCredential(withdrawal_credentials); + + return has_withdrawable_credentials and withdrawable_epoch <= epoch and balance > 0; +} + pub fn isPartiallyWithdrawableValidator(comptime fork: ForkSeq, withdrawal_credentials: *const WithdrawalCredentials, effective_balance: u64, balance: u64) bool { // Check withdrawal credentials const has_withdrawable_credentials = if (comptime fork.gte(.electra)) From 2fff48ba3212285f99436f84dbdc13ae8df4f35c Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 12:17:39 +0530 Subject: [PATCH 22/30] fix: voluntary exit builder support for gloas --- src/config/ChainConfig.zig | 1 + src/config/networks/mainnet.zig | 1 + src/config/networks/minimal.zig | 1 + .../block/process_voluntary_exit.zig | 13 ++- .../signature_sets/voluntary_exits.zig | 59 +++++++++++++- src/state_transition/utils/gloas.zig | 80 ++++++++++++++++++- 6 files changed, 142 insertions(+), 13 deletions(-) diff --git a/src/config/ChainConfig.zig b/src/config/ChainConfig.zig index 9bd1da845..b3a72f7dc 100644 --- a/src/config/ChainConfig.zig +++ b/src/config/ChainConfig.zig @@ -47,6 +47,7 @@ GLOAS_FORK_EPOCH: u64, SECONDS_PER_SLOT: u64, SECONDS_PER_ETH1_BLOCK: u64, MIN_VALIDATOR_WITHDRAWABILITY_DELAY: u64, +MIN_BUILDER_WITHDRAWABILITY_DELAY: u64, SHARD_COMMITTEE_PERIOD: u64, ETH1_FOLLOW_DISTANCE: u64, diff --git a/src/config/networks/mainnet.zig b/src/config/networks/mainnet.zig index 155b71fa7..2dea97857 100644 --- a/src/config/networks/mainnet.zig +++ b/src/config/networks/mainnet.zig @@ -42,6 +42,7 @@ pub const chain_config = ChainConfig{ .SECONDS_PER_SLOT = 12, .SECONDS_PER_ETH1_BLOCK = 14, .MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 256, + .MIN_BUILDER_WITHDRAWABILITY_DELAY = 64, .SHARD_COMMITTEE_PERIOD = 256, .ETH1_FOLLOW_DISTANCE = 2048, diff --git a/src/config/networks/minimal.zig b/src/config/networks/minimal.zig index 3f9d78056..06c84876e 100644 --- a/src/config/networks/minimal.zig +++ b/src/config/networks/minimal.zig @@ -42,6 +42,7 @@ pub const chain_config = ChainConfig{ .SECONDS_PER_SLOT = 6, .SECONDS_PER_ETH1_BLOCK = 14, .MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 256, + .MIN_BUILDER_WITHDRAWABILITY_DELAY = 2, .SHARD_COMMITTEE_PERIOD = 64, .ETH1_FOLLOW_DISTANCE = 16, diff --git a/src/state_transition/block/process_voluntary_exit.zig b/src/state_transition/block/process_voluntary_exit.zig index fb2424984..fbb7be5e4 100644 --- a/src/state_transition/block/process_voluntary_exit.zig +++ b/src/state_transition/block/process_voluntary_exit.zig @@ -37,7 +37,7 @@ pub fn processVoluntaryExit( } if (fork.gte(.gloas) and isBuilderIndex(voluntary_exit.validator_index)) { - try initiateBuilderExit(state, allocator, convertValidatorIndexToBuilderIndex(voluntary_exit.validator_index)); + try initiateBuilderExit(config, state, allocator, convertValidatorIndexToBuilderIndex(voluntary_exit.validator_index)); return; } @@ -90,7 +90,7 @@ pub fn getVoluntaryExitValidity( return getBuilderVoluntaryExitValidity(allocator, config, epoch_cache, state, signed_voluntary_exit, verify_signature); } - return getValidatorVoluntaryExitValidity(fork, config, epoch_cache, state, signed_voluntary_exit, verify_signature); + return getValidatorVoluntaryExitValidity(fork, allocator, config, epoch_cache, state, signed_voluntary_exit, verify_signature); } fn getBuilderVoluntaryExitValidity( @@ -127,10 +127,8 @@ fn getBuilderVoluntaryExitValidity( } // Verify signature - if (verify_signature) { - if (!try verifyVoluntaryExitSignature(config, epoch_cache, signed_voluntary_exit)) { - return .invalid_signature; - } + if (verify_signature and !try verifyVoluntaryExitSignature(allocator, config, epoch_cache, state, signed_voluntary_exit)) { + return .invalid_signature; } return .valid; @@ -138,6 +136,7 @@ fn getBuilderVoluntaryExitValidity( fn getValidatorVoluntaryExitValidity( comptime fork: ForkSeq, + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *const EpochCache, state: *BeaconState(fork), @@ -181,7 +180,7 @@ fn getValidatorVoluntaryExitValidity( // verify signature if (verify_signature) { - if (!try verifyVoluntaryExitSignature(config, epoch_cache, signed_voluntary_exit)) { + if (!try verifyVoluntaryExitSignature(allocator, config, epoch_cache, state, signed_voluntary_exit)) { return .invalid_signature; } } diff --git a/src/state_transition/signature_sets/voluntary_exits.zig b/src/state_transition/signature_sets/voluntary_exits.zig index 71052945a..95a423a9a 100644 --- a/src/state_transition/signature_sets/voluntary_exits.zig +++ b/src/state_transition/signature_sets/voluntary_exits.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const BeaconConfig = @import("config").BeaconConfig; const ForkSeq = @import("config").ForkSeq; const ForkTypes = @import("fork_types").ForkTypes; @@ -10,27 +11,48 @@ const SignedVoluntaryExit = types.phase0.SignedVoluntaryExit.Type; const computeStartSlotAtEpoch = @import("../utils/epoch.zig").computeStartSlotAtEpoch; const computeSigningRoot = @import("../utils/signing_root.zig").computeSigningRoot; const verifySingleSignatureSet = @import("../utils/signature_sets.zig").verifySingleSignatureSet; +const isBuilderIndex = @import("../utils/gloas.zig").isBuilderIndex; +const convertValidatorIndexToBuilderIndex = @import("../utils/gloas.zig").convertValidatorIndexToBuilderIndex; +const bls = @import("bls"); pub fn verifyVoluntaryExitSignature( + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *const EpochCache, + state: anytype, signed_voluntary_exit: *const SignedVoluntaryExit, ) !bool { const signature_set = try getVoluntaryExitSignatureSet( + allocator, config, epoch_cache, + state, signed_voluntary_exit, ); return try verifySingleSignatureSet(&signature_set); } pub fn getVoluntaryExitSignatureSet( + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *const EpochCache, + state: anytype, signed_voluntary_exit: *const SignedVoluntaryExit, ) !SingleSignatureSet { - const slot = computeStartSlotAtEpoch(signed_voluntary_exit.message.epoch); - const domain = try config.getDomainForVoluntaryExit(epoch_cache.epoch, slot); + if (isBuilderVoluntaryExit(signed_voluntary_exit)) { + const gloas_state: *BeaconState(.gloas) = @ptrCast(state); + return getBuilderVoluntaryExitSignatureSet(allocator, config, epoch_cache, gloas_state, signed_voluntary_exit); + } + return getValidatorVoluntaryExitSignatureSet(config, epoch_cache, signed_voluntary_exit); +} + +pub fn getValidatorVoluntaryExitSignatureSet( + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + signed_voluntary_exit: *const SignedVoluntaryExit, +) !SingleSignatureSet { + const message_slot = computeStartSlotAtEpoch(signed_voluntary_exit.message.epoch); + const domain = try config.getDomainForVoluntaryExit(epoch_cache.epoch, message_slot); var signing_root: [32]u8 = undefined; try computeSigningRoot(types.phase0.VoluntaryExit, &signed_voluntary_exit.message, domain, &signing_root); @@ -41,16 +63,49 @@ pub fn getVoluntaryExitSignatureSet( }; } +pub fn getBuilderVoluntaryExitSignatureSet( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_voluntary_exit: *const SignedVoluntaryExit, +) !SingleSignatureSet { + const message_slot = computeStartSlotAtEpoch(signed_voluntary_exit.message.epoch); + const domain = try config.getDomainForVoluntaryExit(epoch_cache.epoch, message_slot); + var signing_root: [32]u8 = undefined; + try computeSigningRoot(types.phase0.VoluntaryExit, &signed_voluntary_exit.message, domain, &signing_root); + + const builder_index = convertValidatorIndexToBuilderIndex(signed_voluntary_exit.message.validator_index); + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + const pubkey = bls.PublicKey.uncompress(&builder.pubkey) catch return error.InvalidBuilderPubkey; + + return .{ + .pubkey = pubkey, + .signing_root = signing_root, + .signature = signed_voluntary_exit.signature, + }; +} + +pub fn isBuilderVoluntaryExit(signed_voluntary_exit: *const SignedVoluntaryExit) bool { + return isBuilderIndex(signed_voluntary_exit.message.validator_index); +} + pub fn voluntaryExitsSignatureSets( + allocator: Allocator, config: *const BeaconConfig, epoch_cache: *const EpochCache, + state: anytype, voluntary_exits: []types.phase0.SignedVoluntaryExit.Type, out: std.ArrayList(SingleSignatureSet), ) !void { for (voluntary_exits) |*signed_voluntary_exit| { const signature_set = try getVoluntaryExitSignatureSet( + allocator, config, epoch_cache, + state, signed_voluntary_exit, ); try out.append(signature_set); diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig index ab835ab48..33811fb2b 100644 --- a/src/state_transition/utils/gloas.zig +++ b/src/state_transition/utils/gloas.zig @@ -1,15 +1,20 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const ForkSeq = @import("config").ForkSeq; const BeaconState = @import("fork_types").BeaconState; const ct = @import("consensus_types"); const preset = @import("preset").preset; const c = @import("constants"); const getBlockRootAtSlot = @import("./block_root.zig").getBlockRootAtSlot; const computeEpochAtSlot = @import("./epoch.zig").computeEpochAtSlot; +const computePayloadTimelinessCommitteesForEpoch = @import("./seed.zig").computePayloadTimelinessCommitteesForEpoch; const RootCache = @import("../cache/root_cache.zig").RootCache; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const isValidDepositSignature = @import("../block/process_deposit.zig").isValidDepositSignature; const BLSPubkey = ct.primitive.BLSPubkey.Type; +const ValidatorIndex = ct.primitive.ValidatorIndex.Type; pub fn isBuilderWithdrawalCredential(withdrawal_credentials: *const [32]u8) bool { return withdrawal_credentials[0] == c.BUILDER_WITHDRAWAL_PREFIX; @@ -79,7 +84,7 @@ pub fn canBuilderCoverBid(allocator: Allocator, state: *BeaconState(.gloas), bui return builder.balance - min_balance >= bid_amount; } -pub fn initiateBuilderExit(state: *BeaconState(.gloas), allocator: Allocator, builder_index: u64) !void { +pub fn initiateBuilderExit(config: *const BeaconConfig, state: *BeaconState(.gloas), allocator: Allocator, builder_index: u64) !void { var builders = try state.inner.get("builders"); var builder: ct.gloas.Builder.Type = undefined; try builders.getValue(allocator, builder_index, &builder); @@ -87,16 +92,16 @@ pub fn initiateBuilderExit(state: *BeaconState(.gloas), allocator: Allocator, bu if (builder.withdrawable_epoch != c.FAR_FUTURE_EPOCH) return; const current_epoch = computeEpochAtSlot(try state.slot()); - builder.withdrawable_epoch = current_epoch + c.MIN_BUILDER_WITHDRAWABILITY_DELAY; + builder.withdrawable_epoch = current_epoch + config.chain.MIN_BUILDER_WITHDRAWABILITY_DELAY; try builders.setValue(builder_index, &builder); } pub fn findBuilderIndexByPubkey(allocator: Allocator, state: *BeaconState(.gloas), pubkey: *const BLSPubkey) !?usize { var builders = try state.inner.get("builders"); const len = try builders.length(); - var it = builders.iteratorReadonly(0); for (0..len) |i| { - const b = try it.nextValue(allocator); + var b: ct.gloas.Builder.Type = undefined; + try builders.getValue(allocator, i, &b); if (std.mem.eql(u8, &b.pubkey, pubkey)) return i; } return null; @@ -140,3 +145,70 @@ pub fn isPubkeyInList(list: []const BLSPubkey, pubkey: *const BLSPubkey) bool { return false; } +pub fn isPendingValidator( + comptime fork: ForkSeq, + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(fork), + pubkey: *const BLSPubkey, +) !bool { + var pending_deposits = try state.pendingDeposits(); + const pending_deposits_len = try pending_deposits.length(); + var pending_it = pending_deposits.iteratorReadonly(0); + + for (0..pending_deposits_len) |_| { + const pending_deposit = try pending_it.nextValue(allocator); + if (!std.mem.eql(u8, &pending_deposit.pubkey, pubkey)) { + continue; + } + + if (isValidDepositSignature( + config, + &pending_deposit.pubkey, + &pending_deposit.withdrawal_credentials, + pending_deposit.amount, + pending_deposit.signature, + )) { + return true; + } + } + + return false; +} + +pub fn initializePtcWindow( + comptime fork: ForkSeq, + allocator: Allocator, + epoch_cache: *const EpochCache, + state: *BeaconState(fork), +) ![ct.gloas.PtcWindow.length][ct.gloas.PtcWindow.Element.length]ValidatorIndex { + const PtcSize = ct.gloas.PtcWindow.Element.length; + const PtcWindowLen = ct.gloas.PtcWindow.length; + + const empty_previous_epoch = [_][PtcSize]ValidatorIndex{ + [_]ValidatorIndex{0} ** PtcSize, + } ** preset.SLOTS_PER_EPOCH; + + const current_epoch = computeEpochAtSlot(try state.slot()); + var current_and_lookahead: [(1 + preset.MIN_SEED_LOOKAHEAD) * preset.SLOTS_PER_EPOCH][PtcSize]ValidatorIndex = undefined; + for (0..1 + preset.MIN_SEED_LOOKAHEAD) |epoch_offset| { + const epoch = current_epoch + epoch_offset; + const epoch_committees = try computePayloadTimelinessCommitteesForEpoch( + fork, + allocator, + state, + epoch, + epoch_cache, + ); + for (0..preset.SLOTS_PER_EPOCH) |slot_index| { + current_and_lookahead[epoch_offset * preset.SLOTS_PER_EPOCH + slot_index] = epoch_committees[slot_index]; + } + } + + // return empty_previous_epoch + current_and_lookahead + var ptc_window: [PtcWindowLen][PtcSize]ValidatorIndex = undefined; + @memcpy(ptc_window[0..preset.SLOTS_PER_EPOCH], &empty_previous_epoch); + @memcpy(ptc_window[preset.SLOTS_PER_EPOCH..], ¤t_and_lookahead); + + return ptc_window; +} From 4d910141e80459eefcec7112ee548f4c580c9de7 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 12:38:10 +0530 Subject: [PATCH 23/30] fix:processExecutionPayloadEnvelope --- .../process_execution_payload_envelope.zig | 18 +++++++++++------- src/state_transition/root.zig | 6 ++++++ .../execution_payload_envelope.zig | 5 +++-- src/state_transition/utils/signature_sets.zig | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/state_transition/block/process_execution_payload_envelope.zig b/src/state_transition/block/process_execution_payload_envelope.zig index dc33df9ce..0dadb7d59 100644 --- a/src/state_transition/block/process_execution_payload_envelope.zig +++ b/src/state_transition/block/process_execution_payload_envelope.zig @@ -7,7 +7,6 @@ const BeaconState = @import("fork_types").BeaconState; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const preset = @import("preset").preset; const c = @import("constants"); -const computeTimeAtSlot = @import("../utils/epoch.zig").computeTimeAtSlot; const processDepositRequest = @import("./process_deposit_request.zig").processDepositRequest; const processWithdrawalRequest = @import("./process_withdrawal_request.zig").processWithdrawalRequest; const processConsolidationRequest = @import("./process_consolidation_request.zig").processConsolidationRequest; @@ -37,19 +36,18 @@ pub fn processExecutionPayloadEnvelope( try validateExecutionPayloadEnvelope(allocator, config, state, envelope); - const fork = config.getForkSeqAtSlot(envelope.slot); const requests = &envelope.execution_requests; for (requests.deposits.items) |*deposit| { - try processDepositRequest(fork, allocator, config, epoch_cache, state, deposit); + try processDepositRequest(.gloas, allocator, config, epoch_cache, state, deposit); } for (requests.withdrawals.items) |*withdrawal| { - try processWithdrawalRequest(fork, config, epoch_cache, state, withdrawal); + try processWithdrawalRequest(.gloas, config, @constCast(epoch_cache), state, withdrawal); } for (requests.consolidations.items) |*consolidation| { - try processConsolidationRequest(fork, config, epoch_cache, state, consolidation); + try processConsolidationRequest(.gloas, config, epoch_cache, state, consolidation); } // Queue the builder payment @@ -62,6 +60,7 @@ pub fn processExecutionPayloadEnvelope( if (amount > 0) { var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); try builder_pending_withdrawals.pushValue(&payment.withdrawal); + try state.inner.set("builder_pending_withdrawals", builder_pending_withdrawals); } const default_payment = types.gloas.BuilderPendingPayment.default_value; @@ -72,6 +71,8 @@ pub fn processExecutionPayloadEnvelope( try execution_payload_availability.set(try state.slot() % preset.SLOTS_PER_HISTORICAL_ROOT, true); try state.inner.setValue("latest_block_hash", &payload.block_hash); + try state.commit(); + if (opts.verify_state_root) { const state_root = try state.hashTreeRoot(); if (!std.mem.eql(u8, &envelope.state_root, state_root)) { @@ -94,6 +95,7 @@ fn validateExecutionPayloadEnvelope( if (std.mem.eql(u8, latest_header_state_root, &c.ZERO_HASH)) { const previous_state_root = try state.hashTreeRoot(); try latest_block_header.setValue("state_root", previous_state_root); + try state.inner.set("latest_block_header", latest_block_header); } // Verify consistency with the beacon block @@ -108,8 +110,9 @@ fn validateExecutionPayloadEnvelope( } // Verify consistency with the committed bid - var committed_bid: types.gloas.ExecutionPayloadBid.Type = undefined; + var committed_bid: types.gloas.ExecutionPayloadBid.Type = types.gloas.ExecutionPayloadBid.default_value; try state.inner.getValue(allocator, "latest_execution_payload_bid", &committed_bid); + defer types.gloas.ExecutionPayloadBid.deinit(allocator, &committed_bid); if (envelope.builder_index != committed_bid.builder_index) { return error.EnvelopeBuilderIndexMismatch; @@ -146,7 +149,8 @@ fn validateExecutionPayloadEnvelope( } // Verify timestamp - const expected_timestamp = computeTimeAtSlot(config, try state.slot(), try state.genesisTime()); + // compute_timestamp_at_slot: genesis_time + slot * SECONDS_PER_SLOT + const expected_timestamp = (try state.genesisTime()) + (try state.slot()) * config.chain.SECONDS_PER_SLOT; if (payload.timestamp != expected_timestamp) { return error.EnvelopeTimestampMismatch; } diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index 0742d1c47..bb5e90838 100644 --- a/src/state_transition/root.zig +++ b/src/state_transition/root.zig @@ -52,6 +52,7 @@ pub const upgradeStateToCapella = @import("./slot/upgrade_state_to_capella.zig") pub const upgradeStateToDeneb = @import("./slot/upgrade_state_to_deneb.zig").upgradeStateToDeneb; pub const upgradeStateToElectra = @import("./slot/upgrade_state_to_electra.zig").upgradeStateToElectra; pub const upgradeStateToFulu = @import("./slot/upgrade_state_to_fulu.zig").upgradeStateToFulu; +pub const upgradeStateToGloas = @import("./slot/upgrade_state_to_gloas.zig").upgradeStateToGloas; // Block pub const processBlockHeader = @import("./block/process_block_header.zig").processBlockHeader; @@ -101,6 +102,11 @@ pub const preset = @import("preset").preset; const EpochShuffling = @import("./utils/epoch_shuffling.zig"); pub const calculateShufflingDecisionRoot = EpochShuffling.calculateShufflingDecisionRoot; pub const processProposerLookahead = @import("./epoch/process_proposer_lookahead.zig").processProposerLookahead; +pub const processBuilderPendingPayments = @import("./epoch/process_builder_pending_payments.zig").processBuilderPendingPayments; +pub const processExecutionPayloadBid = @import("./block/process_execution_payload_bid.zig").processExecutionPayloadBid; +pub const processExecutionPayloadEnvelope = @import("./block/process_execution_payload_envelope.zig").processExecutionPayloadEnvelope; +pub const processPayloadAttestation = @import("./block/process_payload_attestation.zig").processPayloadAttestation; +pub const processPtcWindow = @import("./epoch/process_ptc_window.zig").processPtcWindow; test { testing.refAllDecls(@This()); diff --git a/src/state_transition/signature_sets/execution_payload_envelope.zig b/src/state_transition/signature_sets/execution_payload_envelope.zig index 1bf348c09..a5aaf889b 100644 --- a/src/state_transition/signature_sets/execution_payload_envelope.zig +++ b/src/state_transition/signature_sets/execution_payload_envelope.zig @@ -33,7 +33,8 @@ pub fn getExecutionPayloadEnvelopeSignatureSet( const envelope = &signed_envelope.message; // Get the pubkey: proposer key for self-builds, builder key otherwise - const proposer_index: u64 = try state.latestBlockHeader().get("proposer_index"); + var latest_block_header = try state.latestBlockHeader(); + const proposer_index: u64 = try latest_block_header.get("proposer_index"); const pubkey = if (envelope.builder_index == c.BUILDER_INDEX_SELF_BUILD) epoch_cache.index_to_pubkey.items[proposer_index] else blk: { @@ -44,5 +45,5 @@ pub fn getExecutionPayloadEnvelopeSignatureSet( }; const signing_root = try getExecutionPayloadEnvelopeSigningRoot(allocator, config, envelope); - return createSingleSignatureSetFromComponents(&pubkey, signing_root, signed_envelope.signature); + return createSingleSignatureSetFromComponents(pubkey, signing_root, signed_envelope.signature); } diff --git a/src/state_transition/utils/signature_sets.zig b/src/state_transition/utils/signature_sets.zig index 53d6cff62..79a8cacc3 100644 --- a/src/state_transition/utils/signature_sets.zig +++ b/src/state_transition/utils/signature_sets.zig @@ -39,7 +39,7 @@ pub fn verifyAggregatedSignatureSet(set: *const AggregatedSignatureSet) !bool { return fastAggregateVerify(&set.signing_root, set.pubkeys, &signature, null, null); } -pub fn createSingleSignatureSetFromComponents(pubkey: *const PublicKey, signing_root: Root, signature: BLSSignature) SingleSignatureSet { +pub fn createSingleSignatureSetFromComponents(pubkey: PublicKey, signing_root: Root, signature: BLSSignature) SingleSignatureSet { return .{ .pubkey = pubkey, .signing_root = signing_root, From 85667afa3813456e55ad019e5ceb59df743fab6b Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 12:39:59 +0530 Subject: [PATCH 24/30] fix: process_deposit_request builder routing --- src/state_transition/block/process_deposit_request.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/state_transition/block/process_deposit_request.zig b/src/state_transition/block/process_deposit_request.zig index b49fd1edd..e3d7c59b5 100644 --- a/src/state_transition/block/process_deposit_request.zig +++ b/src/state_transition/block/process_deposit_request.zig @@ -14,6 +14,7 @@ const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; const gloas_utils = @import("../utils/gloas.zig"); const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; const isBuilderWithdrawalCredential = gloas_utils.isBuilderWithdrawalCredential; +const isPendingValidator = gloas_utils.isPendingValidator; const isValidDepositSignature = @import("./process_deposit.zig").isValidDepositSignature; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; @@ -34,13 +35,14 @@ pub fn processDepositRequest( if (fork.gte(.gloas)) { const builder_index = try findBuilderIndexByPubkey(allocator, state, pubkey); const validator_index = epoch_cache.getValidatorIndex(pubkey); + const pending_validator = try isPendingValidator(fork, allocator, config, state, pubkey); const is_builder = builder_index != null; const is_validator = validator_index != null; const is_builder_prefix = isBuilderWithdrawalCredential(withdrawal_credentials); // Route to builder if it's an existing builder OR has builder prefix and is not a validator - if (is_builder or (is_builder_prefix and !is_validator)) { + if (is_builder or (is_builder_prefix and !is_validator and !pending_validator)) { // Apply builder deposits immediately try applyDepositForBuilder(allocator, config, state, pubkey, withdrawal_credentials, amount, signature, try state.slot()); return; @@ -105,9 +107,9 @@ fn addBuilderToRegistry( const current_epoch = computeEpochAtSlot(try state.slot()); var builderIndex: ?usize = null; - var it = builders.iteratorReadonly(0); for (0..len) |i| { - const b = try it.nextValue(allocator); + var b: Builder.Type = undefined; + try builders.getValue(allocator, i, &b); if (b.withdrawable_epoch <= current_epoch and b.balance == 0) { builderIndex = i; break; @@ -127,5 +129,6 @@ fn addBuilderToRegistry( try builders.setValue(idx, &new_builder); } else { try builders.pushValue(&new_builder); + try state.inner.set("builders", builders); } } From 5fefa524b1a6485da602488fce55bbb1553cb37d Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 17:23:04 +0530 Subject: [PATCH 25/30] feat: add computePtc and balance-weighted helpers --- .../epoch/process_ptc_window.zig | 33 +++++++++ src/state_transition/utils/gloas.zig | 70 +++++++++++++++++++ test/spec/runner/epoch_processing.zig | 8 +++ 3 files changed, 111 insertions(+) create mode 100644 src/state_transition/epoch/process_ptc_window.zig diff --git a/src/state_transition/epoch/process_ptc_window.zig b/src/state_transition/epoch/process_ptc_window.zig new file mode 100644 index 000000000..52d1384d8 --- /dev/null +++ b/src/state_transition/epoch/process_ptc_window.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconState = @import("fork_types").BeaconState; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const ct = @import("consensus_types"); +const preset = @import("preset").preset; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const computeStartSlotAtEpoch = @import("../utils/epoch.zig").computeStartSlotAtEpoch; +const computePtc = @import("../utils/gloas.zig").computePtc; +const ValidatorIndex = ct.primitive.ValidatorIndex.Type; + +pub fn processPtcWindow( + allocator: Allocator, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), +) !void { + var ptc_window = try state.inner.get("ptc_window"); + const ptc_window_len = ct.gloas.PtcWindow.length; + + var ptc_entry: [ct.gloas.PtcWindow.Element.length]ValidatorIndex = undefined; + for (0..ptc_window_len - preset.SLOTS_PER_EPOCH) |i| { + try ptc_window.getValue(undefined, i + preset.SLOTS_PER_EPOCH, &ptc_entry); + try ptc_window.setValue(i, &ptc_entry); + } + + const next_epoch = computeEpochAtSlot(try state.slot()) + preset.MIN_SEED_LOOKAHEAD + 1; + const start_slot = computeStartSlotAtEpoch(next_epoch); + + for (0..preset.SLOTS_PER_EPOCH) |slot_offset| { + const ptc = try computePtc(allocator, state, start_slot + slot_offset, null, epoch_cache.effective_balance_increments.get().items); + try ptc_window.setValue(ptc_window_len - preset.SLOTS_PER_EPOCH + slot_offset, &ptc); + } +} diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig index 33811fb2b..0d2da07ea 100644 --- a/src/state_transition/utils/gloas.zig +++ b/src/state_transition/utils/gloas.zig @@ -8,7 +8,15 @@ const preset = @import("preset").preset; const c = @import("constants"); const getBlockRootAtSlot = @import("./block_root.zig").getBlockRootAtSlot; const computeEpochAtSlot = @import("./epoch.zig").computeEpochAtSlot; +const AnyBeaconState = @import("fork_types").AnyBeaconState; +const computePayloadTimelinessCommitteeForSlot = @import("./seed.zig").computePayloadTimelinessCommitteeForSlot; const computePayloadTimelinessCommitteesForEpoch = @import("./seed.zig").computePayloadTimelinessCommitteesForEpoch; +const getSeed = @import("./seed.zig").getSeed; +const Sha256 = std.crypto.hash.sha2.Sha256; +const EpochShuffling = @import("./epoch_shuffling.zig").EpochShuffling; +const computeEpochShuffling = @import("./epoch_shuffling.zig").computeEpochShuffling; +const isActiveValidator = @import("./validator.zig").isActiveValidator; +const computeStartSlotAtEpoch = @import("./epoch.zig").computeStartSlotAtEpoch; const RootCache = @import("../cache/root_cache.zig").RootCache; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const isValidDepositSignature = @import("../block/process_deposit.zig").isValidDepositSignature; @@ -131,6 +139,29 @@ pub fn isAttestationSameSlotRootCache(root_cache: *RootCache(.gloas), data: *con return is_matching and is_current; } +// TODO: Need to check if these are used at all +/// computePayloadTimelinessCommitteeIndices uses the optimized increments-based check instead. +pub fn isBalanceWeightedAcceptance(effective_balance: u64, random_value: u64) bool { + const MAX_RANDOM_VALUE: u64 = 0xFFFF; + return effective_balance * MAX_RANDOM_VALUE >= preset.MAX_EFFECTIVE_BALANCE_ELECTRA * random_value; +} + +// TODO: Need to check if these are used at all +/// computePayloadTimelinessCommitteeIndices inlines this logic with increments instead of full Gwei balances. +pub fn computeBalanceWeightedAcceptance(effective_balance: u64, seed: *const [32]u8, i: u64) bool { + var hash_input: [40]u8 = undefined; + @memcpy(hash_input[0..32], seed); + std.mem.writeInt(u64, hash_input[32..][0..8], i / 16, .little); + + var random_bytes: [32]u8 = undefined; + Sha256.hash(&hash_input, &random_bytes, .{}); + + const offset: usize = @intCast((i % 16) * 2); + const random_value: u64 = std.mem.readInt(u16, random_bytes[offset..][0..2], .little); + + return isBalanceWeightedAcceptance(effective_balance, random_value); +} + pub fn isParentBlockFull(state: *BeaconState(.gloas)) !bool { var bid = try state.inner.get("latest_execution_payload_bid"); const bid_block_hash = try bid.getFieldRoot("block_hash"); @@ -176,6 +207,45 @@ pub fn isPendingValidator( return false; } +pub fn computePtc( + allocator: Allocator, + state: *BeaconState(.gloas), + slot: u64, + shuffling: ?*EpochShuffling, + effective_balance_increments: []const u16, +) ![ct.gloas.PtcWindow.Element.length]ValidatorIndex { + const epoch = computeEpochAtSlot(slot); + + const epoch_shuffling = if (shuffling) |s| s else blk: { + var validators = try state.validators(); + const validator_count = try validators.length(); + var active_indices = std.ArrayList(ValidatorIndex).init(allocator); + for (0..validator_count) |i| { + var validator: ct.phase0.Validator.Type = undefined; + try validators.getValue(undefined, i, &validator); + if (isActiveValidator(&validator, epoch)) { + try active_indices.append(@intCast(i)); + } + } + var any_state = AnyBeaconState{ .gloas = state.inner }; + break :blk try computeEpochShuffling(allocator, &any_state, try active_indices.toOwnedSlice(), epoch); + }; + defer if (shuffling == null) epoch_shuffling.deinit(); + + var epoch_seed: [32]u8 = undefined; + try getSeed(.gloas, state, epoch, c.DOMAIN_PTC_ATTESTER, &epoch_seed); + + var slot_seed_input: [40]u8 = undefined; + @memcpy(slot_seed_input[0..32], &epoch_seed); + std.mem.writeInt(u64, slot_seed_input[32..][0..8], slot, .little); + + var slot_seed: [32]u8 = undefined; + Sha256.hash(&slot_seed_input, &slot_seed, .{}); + + const slot_committees = epoch_shuffling.committees[slot % preset.SLOTS_PER_EPOCH]; + return computePayloadTimelinessCommitteeForSlot(allocator, &slot_seed, slot_committees, effective_balance_increments); +} + pub fn initializePtcWindow( comptime fork: ForkSeq, allocator: Allocator, diff --git a/test/spec/runner/epoch_processing.zig b/test/spec/runner/epoch_processing.zig index e4bec7573..7383f0efb 100644 --- a/test/spec/runner/epoch_processing.zig +++ b/test/spec/runner/epoch_processing.zig @@ -28,6 +28,8 @@ pub const EpochProcessingFn = enum { pending_deposits, pending_consolidations, proposer_lookahead, + builder_pending_payments, + ptc_window, pub fn suiteName(self: EpochProcessingFn) []const u8 { return @tagName(self) ++ "/pyspec_tests"; @@ -132,6 +134,12 @@ pub fn TestCase(comptime fork: ForkSeq, comptime epoch_process_fn: EpochProcessi .proposer_lookahead => { try state_transition.processProposerLookahead(fork, allocator, epoch_cache, fork_state, &epoch_transition_cache); }, + .builder_pending_payments => { + try state_transition.processBuilderPendingPayments(allocator, fork_state, epoch_cache); + }, + .ptc_window => { + try state_transition.processPtcWindow(allocator, epoch_cache, fork_state); + }, } } }; From 33a1852b0ccb7d883d0a6d1ee06bcc444ab27a6b Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 17:37:04 +0530 Subject: [PATCH 26/30] feat: add PtcWindow to gloas state and read PTC from state.ptc_window --- src/consensus_types/gloas.zig | 5 ++ src/preset/preset.zig | 10 ++- .../block/process_payload_attestation.zig | 3 +- src/state_transition/cache/epoch_cache.zig | 62 ++++++++++++++----- src/state_transition/metrics.zig | 1 + .../slot/upgrade_state_to_gloas.zig | 4 ++ 6 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/consensus_types/gloas.zig b/src/consensus_types/gloas.zig index 6f35d9049..e0089b9be 100644 --- a/src/consensus_types/gloas.zig +++ b/src/consensus_types/gloas.zig @@ -92,6 +92,10 @@ pub const LightClientFinalityUpdate = electra.LightClientFinalityUpdate; pub const LightClientOptimisticUpdate = electra.LightClientOptimisticUpdate; pub const ProposerLookahead = fulu.ProposerLookahead; +pub const PtcWindow = ssz.FixedVectorType( + ssz.FixedVectorType(p.ValidatorIndex, preset.PTC_SIZE), + (2 + preset.MIN_SEED_LOOKAHEAD) * preset.SLOTS_PER_EPOCH, +); pub const BlobSidecar = electra.BlobSidecar; @@ -247,4 +251,5 @@ pub const BeaconState = ssz.VariableContainerType(struct { builder_pending_withdrawals: ssz.FixedListType(BuilderPendingWithdrawal, preset.BUILDER_PENDING_WITHDRAWALS_LIMIT), latest_block_hash: p.Bytes32, payload_expected_withdrawals: ssz.FixedListType(Withdrawal, preset.MAX_WITHDRAWALS_PER_PAYLOAD), + ptc_window: PtcWindow, }); diff --git a/src/preset/preset.zig b/src/preset/preset.zig index 60e62203e..74fecfd68 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -83,7 +83,6 @@ const PresetMainnet = struct { pub const DEPOSIT_CONTRACT_TREE_DEPTH = 32; pub const GENESIS_SLOT = 0; pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16; - // Gloas pub const PTC_SIZE = 512; pub const MAX_PAYLOAD_ATTESTATIONS = 4; pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 @@ -162,12 +161,11 @@ 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; - // Gloas - pub const PTC_SIZE = 512; + pub const PTC_SIZE = 2; pub const MAX_PAYLOAD_ATTESTATIONS = 4; - pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; // 2^40 - pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576; // 2^20 - pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16_384; // 2^14 + 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; }; const preset_str = @import("build_options").preset; diff --git a/src/state_transition/block/process_payload_attestation.zig b/src/state_transition/block/process_payload_attestation.zig index 31081063d..ac7aa48a5 100644 --- a/src/state_transition/block/process_payload_attestation.zig +++ b/src/state_transition/block/process_payload_attestation.zig @@ -25,7 +25,8 @@ pub fn processPayloadAttestation( return error.PayloadAttestationNotFromPreviousSlot; } - const indexed_payload_attestation = try epoch_cache.getIndexedPayloadAttestation(allocator, data.slot, payload_attestation); + var indexed_payload_attestation = try epoch_cache.getIndexedPayloadAttestation(allocator, state, data.slot, payload_attestation); + defer indexed_payload_attestation.attesting_indices.deinit(allocator); if (!(try isValidIndexedPayloadAttestation(allocator, config, epoch_cache, &indexed_payload_attestation, true))) { return error.InvalidPayloadAttestation; diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index 9a014b845..bf4ebed31 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -30,6 +30,7 @@ const getTotalSlashingsByIncrement = @import("../epoch/process_slashings.zig").g const computeEpochShuffling = @import("../utils/epoch_shuffling.zig").computeEpochShuffling; const getSeed = @import("../utils/seed.zig").getSeed; const computeProposers = @import("../utils/seed.zig").computeProposers; +const computePayloadTimelinessCommitteesForEpoch = @import("../utils/seed.zig").computePayloadTimelinessCommitteesForEpoch; const SyncCommitteeCacheRc = @import("./sync_committee_cache.zig").SyncCommitteeCacheRc; const SyncCommitteeCacheAllForks = @import("./sync_committee_cache.zig").SyncCommitteeCache; const computeSyncParticipantReward = @import("../utils/sync_committee.zig").computeSyncParticipantReward; @@ -449,6 +450,19 @@ pub const EpochCache = struct { .previous_payload_timeliness_committees = null, }; + // Compute PTC for all slots in the prev/current epoch + if (current_epoch >= config.chain.GLOAS_FORK_EPOCH) { + epoch_cache_ptr.payload_timeliness_committees = switch (state.forkSeq()) { + inline else => |f| try computePayloadTimelinessCommitteesForEpoch(f, allocator, state.castToFork(f), current_epoch, epoch_cache_ptr), + }; + + if (!is_genesis and previous_epoch >= config.chain.GLOAS_FORK_EPOCH) { + epoch_cache_ptr.previous_payload_timeliness_committees = switch (state.forkSeq()) { + inline else => |f| try computePayloadTimelinessCommitteesForEpoch(f, allocator, state.castToFork(f), previous_epoch, epoch_cache_ptr), + }; + } + } + return epoch_cache_ptr; } @@ -587,6 +601,21 @@ pub const EpochCache = struct { /// At fork boundary, this runs post-fork logic and after `upgradeState*`. pub fn finalProcessEpoch(self: *EpochCache, state: *AnyBeaconState) !void { self.proposers_prev_epoch = self.proposers; + + // Shift and compute current epoch PTC eagerly for all slots + if (self.epoch >= self.config.chain.GLOAS_FORK_EPOCH) { + self.previous_payload_timeliness_committees = self.payload_timeliness_committees; + self.payload_timeliness_committees = switch (state.forkSeq()) { + inline else => |f| try computePayloadTimelinessCommitteesForEpoch( + f, + self.allocator, + state.castToFork(f), + self.epoch, + self, + ), + }; + } + switch (state.forkSeq()) { inline else => |fork| { const fork_state = state.castToFork(fork); @@ -892,35 +921,34 @@ pub const EpochCache = struct { /// Convert a PayloadAttestation into an IndexedPayloadAttestation by resolving /// aggregation bits against the Payload Timeliness Committee for the attestation's slot. - pub fn getPayloadTimelinessCommittee(self: *const EpochCache, slot: Slot) ![]const ValidatorIndex { + /// Spec: get_ptc — Read PTC from state.ptc_window + pub fn getPayloadTimelinessCommittee(self: *const EpochCache, state: *BeaconState(.gloas), slot: Slot) ![preset.PTC_SIZE]ValidatorIndex { + _ = self; const epoch = computeEpochAtSlot(slot); + const state_epoch = computeEpochAtSlot(try state.slot()); - if (epoch < self.config.chain.GLOAS_FORK_EPOCH) { - return error.PtcNotAvailableBeforeGloas; - } - - if (epoch == self.epoch) { - if (self.payload_timeliness_committees) |*committees| { - return &committees[slot % preset.SLOTS_PER_EPOCH]; - } - } + var ptc_window = try state.inner.get("ptc_window"); - if (epoch == self.epoch -| 1) { - if (self.previous_payload_timeliness_committees) |*committees| { - return &committees[slot % preset.SLOTS_PER_EPOCH]; - } - } + const index = if (epoch < state_epoch) blk: { + break :blk slot % preset.SLOTS_PER_EPOCH; + } else blk: { + const offset = (epoch - state_epoch + 1) * preset.SLOTS_PER_EPOCH; + break :blk offset + slot % preset.SLOTS_PER_EPOCH; + }; - return error.PtcNotAvailableForSlot; + var ptc_entry: [preset.PTC_SIZE]ValidatorIndex = undefined; + try ptc_window.getValue(undefined, index, &ptc_entry); + return ptc_entry; } pub fn getIndexedPayloadAttestation( self: *const EpochCache, allocator: Allocator, + state: *BeaconState(.gloas), slot: Slot, payload_attestation: *const types.gloas.PayloadAttestation.Type, ) !types.gloas.IndexedPayloadAttestation.Type { - const payload_timeliness_committee = try self.getPayloadTimelinessCommittee(slot); + const payload_timeliness_committee = try self.getPayloadTimelinessCommittee(state, slot); var attesting_indices = std.ArrayList(ValidatorIndex).init(allocator); errdefer attesting_indices.deinit(); diff --git a/src/state_transition/metrics.zig b/src/state_transition/metrics.zig index 81751865e..9414e1660 100644 --- a/src/state_transition/metrics.zig +++ b/src/state_transition/metrics.zig @@ -37,6 +37,7 @@ pub const EpochTransitionStepKind = enum { process_pending_consolidations, process_builder_pending_payments, process_proposer_lookahead, + process_ptc_window, }; pub const ProposerRewardKind = enum { diff --git a/src/state_transition/slot/upgrade_state_to_gloas.zig b/src/state_transition/slot/upgrade_state_to_gloas.zig index 7546320e5..0708158bc 100644 --- a/src/state_transition/slot/upgrade_state_to_gloas.zig +++ b/src/state_transition/slot/upgrade_state_to_gloas.zig @@ -18,6 +18,7 @@ const gloas_utils = @import("../utils/gloas.zig"); const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; const isBuilderWithdrawalCredential = gloas_utils.isBuilderWithdrawalCredential; const isPubkeyInList = gloas_utils.isPubkeyInList; +const initializePtcWindow = gloas_utils.initializePtcWindow; pub fn upgradeStateToGloas( allocator: Allocator, @@ -48,6 +49,9 @@ pub fn upgradeStateToGloas( const availability = ExecPayloadAvailability{ .data = [_]u8{0xFF} ** @divExact(ExecPayloadAvailability.length, 8) }; try state.inner.setValue("execution_payload_availability", &availability); + const ptc_window = try initializePtcWindow(.gloas, allocator, epoch_cache, &state); + try state.inner.setValue("ptc_window", &ptc_window); + try onboardBuildersFromPendingDeposits(allocator, config, epoch_cache, &state); fulu_state.deinit(); From 144da0bacb1ce1e07ac2b88e9d93d66d8d4be856 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 17:38:24 +0530 Subject: [PATCH 27/30] fix: gloas attestation index validation and proposer slashing epoch calc --- .../block/process_attestation_phase0.zig | 10 ++++++++-- .../block/process_proposer_slashing.zig | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index c5e40c865..d6cc740f8 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -92,8 +92,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..]); diff --git a/src/state_transition/block/process_proposer_slashing.zig b/src/state_transition/block/process_proposer_slashing.zig index fa0f8d663..1650c6c76 100644 --- a/src/state_transition/block/process_proposer_slashing.zig +++ b/src/state_transition/block/process_proposer_slashing.zig @@ -12,6 +12,7 @@ const getProposerSlashingSignatureSets = @import("../signature_sets/proposer_sla const verifySignature = @import("../utils/signature_sets.zig").verifySingleSignatureSet; const slashValidator = @import("./slash_validator.zig").slashValidator; const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const computePreviousEpoch = @import("../utils/epoch.zig").computePreviousEpoch; const preset = @import("preset").preset; pub fn processProposerSlashing( @@ -31,7 +32,7 @@ pub fn processProposerSlashing( const slot = proposer_slashing.signed_header_1.message.slot; const proposal_epoch = computeEpochAtSlot(slot); const current_epoch = epoch_cache.epoch; - const previous_epoch = current_epoch - 1; + const previous_epoch = computePreviousEpoch(current_epoch); const payment_index: ?u64 = if (proposal_epoch == current_epoch) preset.SLOTS_PER_EPOCH + (slot % preset.SLOTS_PER_EPOCH) From 1f717e8547840c90e10d64c9959cc50c096fc037 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 17:49:38 +0530 Subject: [PATCH 28/30] zig fmt and test:config --- src/config/networks/gnosis.zig | 1 + .../signature_sets/indexed_payload_attestation.zig | 2 +- test/spec/runner/operations.zig | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/networks/gnosis.zig b/src/config/networks/gnosis.zig index d9224631f..7052095f8 100644 --- a/src/config/networks/gnosis.zig +++ b/src/config/networks/gnosis.zig @@ -42,6 +42,7 @@ pub const chain_config = ChainConfig{ .SECONDS_PER_SLOT = 5, .SECONDS_PER_ETH1_BLOCK = 6, .MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 256, + .MIN_BUILDER_WITHDRAWABILITY_DELAY = 64, .SHARD_COMMITTEE_PERIOD = 256, .ETH1_FOLLOW_DISTANCE = 1024, diff --git a/src/state_transition/signature_sets/indexed_payload_attestation.zig b/src/state_transition/signature_sets/indexed_payload_attestation.zig index aa0f17ec8..901798f6e 100644 --- a/src/state_transition/signature_sets/indexed_payload_attestation.zig +++ b/src/state_transition/signature_sets/indexed_payload_attestation.zig @@ -39,4 +39,4 @@ pub fn getIndexedPayloadAttestationSignatureSet( try getPayloadAttestationDataSigningRoot(config, &indexed_payload_attestation.data, &signing_root); return createAggregateSignatureSetFromComponents(pubkeys, signing_root, indexed_payload_attestation.signature); -} \ No newline at end of file +} diff --git a/test/spec/runner/operations.zig b/test/spec/runner/operations.zig index e96490fb8..e9bd52d0b 100644 --- a/test/spec/runner/operations.zig +++ b/test/spec/runner/operations.zig @@ -106,7 +106,6 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const Self = @This(); pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { - const pool_size = if (active_preset == .mainnet) 10_000_000 else 1_000_000; var pool = try Node.Pool.init(allocator, pool_size); defer pool.deinit(); From 9da8da0902e0cc795d21e7b8a3ff479ce1f7fe69 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 17:58:29 +0530 Subject: [PATCH 29/30] zbuild sync --- build.zig | 3 ++- zbuild.zon | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 62a3d93be..1c00fcad3 100644 --- a/build.zig +++ b/build.zig @@ -146,7 +146,6 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - module_bls.linkLibrary(dep_blst.artifact("blst")); b.modules.put(b.dupe("bls"), module_bls) catch @panic("OOM"); const module_state_transition = b.createModule(.{ @@ -1199,4 +1198,6 @@ pub fn build(b: *std.Build) void { module_bls_spec_tests.addImport("hex", module_hex); module_bls_spec_tests.addImport("yaml", dep_yaml.module("yaml")); module_bls_spec_tests.addImport("spec_test_options", options_module_spec_test_options); + + _ = dep_blst; } diff --git a/zbuild.zon b/zbuild.zon index 86da67b60..e71d76618 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -67,7 +67,7 @@ .type = "string", }, .spec_test_version = .{ - .default = "v1.6.0-beta.2", + .default = "v1.7.0-alpha.4", .type = "string", }, .spec_test_out_dir = .{ From 9e686893c004fdf63388729676bbbad18327334f Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 2 Apr 2026 18:09:31 +0530 Subject: [PATCH 30/30] update zbuild and zbuild sync --- build.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 1c00fcad3..62a3d93be 100644 --- a/build.zig +++ b/build.zig @@ -146,6 +146,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + module_bls.linkLibrary(dep_blst.artifact("blst")); b.modules.put(b.dupe("bls"), module_bls) catch @panic("OOM"); const module_state_transition = b.createModule(.{ @@ -1198,6 +1199,4 @@ pub fn build(b: *std.Build) void { module_bls_spec_tests.addImport("hex", module_hex); module_bls_spec_tests.addImport("yaml", dep_yaml.module("yaml")); module_bls_spec_tests.addImport("spec_test_options", options_module_spec_test_options); - - _ = dep_blst; }