diff --git a/bench/state_transition/process_block.zig b/bench/state_transition/process_block.zig index 81dfe8578..0fe6e7cd3 100644 --- a/bench/state_transition/process_block.zig +++ b/bench/state_transition/process_block.zig @@ -66,6 +66,7 @@ fn ProcessWithdrawalsBench(comptime fork: ForkSeq) type { const state = BenchState.cloned_cached_state.state.castToFork(fork); state_transition.getExpectedWithdrawals( fork, + allocator, BenchState.cloned_cached_state.epoch_cache, state, &withdrawals_result, @@ -325,6 +326,7 @@ fn ProcessBlockSegmentedBench(comptime fork: ForkSeq) type { defer withdrawal_balances.deinit(); state_transition.getExpectedWithdrawals( fork, + allocator, epoch_cache, state, &withdrawals_result, diff --git a/bindings/napi/BeaconStateView.zig b/bindings/napi/BeaconStateView.zig index 4fb7d7623..95b23d2e6 100644 --- a/bindings/napi/BeaconStateView.zig +++ b/bindings/napi/BeaconStateView.zig @@ -1537,6 +1537,7 @@ pub fn getExpectedWithdrawals(self: *const BeaconStateView) !js.Value { inline .capella, .deneb, .electra, .fulu => |f| { try st.getExpectedWithdrawals( f, + allocator, cached_state.epoch_cache, cached_state.state.castToFork(f), &withdrawals_result, diff --git a/build.zig.zon b/build.zig.zon index a9f0b425c..9ca510b80 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -205,6 +205,7 @@ .metrics, .hashing, .time, + .testing_allocators, }, }, .era = .{ diff --git a/src/consensus_types/gloas.zig b/src/consensus_types/gloas.zig index 40c3af009..69081e059 100644 --- a/src/consensus_types/gloas.zig +++ b/src/consensus_types/gloas.zig @@ -58,6 +58,9 @@ pub const HistoricalSummary = capella.HistoricalSummary; pub const BlobIdentifier = deneb.BlobIdentifier; pub const BlobKzgCommitments = deneb.BlobKzgCommitments; +pub const LogsBloom = bellatrix.LogsBloom; +pub const ExtraData = bellatrix.ExtraData; +pub const Transactions = bellatrix.Transactions; // Reuse Electra types pub const PendingDeposit = electra.PendingDeposit; @@ -66,7 +69,6 @@ pub const PendingConsolidation = electra.PendingConsolidation; pub const DepositRequest = electra.DepositRequest; pub const WithdrawalRequest = electra.WithdrawalRequest; pub const ConsolidationRequest = electra.ConsolidationRequest; -pub const ExecutionRequests = electra.ExecutionRequests; pub const SingleAttestation = electra.SingleAttestation; pub const Attestation = electra.Attestation; pub const Attestations = electra.Attestations; @@ -151,6 +153,39 @@ pub const BuilderPendingWithdrawal = ssz.FixedContainerType(struct { pub const BuilderPendingPayment = ssz.FixedContainerType(struct { weight: p.Uint64, withdrawal: BuilderPendingWithdrawal, + proposer_index: p.ValidatorIndex, +}); + +pub const BuilderDepositRequest = ssz.FixedContainerType(struct { + pubkey: p.BLSPubkey, + withdrawal_credentials: p.Bytes32, + amount: p.Gwei, + signature: p.BLSSignature, +}); + +pub const BuilderDepositRequests = ssz.FixedListType( + BuilderDepositRequest, + preset.MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + .{}, +); + +pub const BuilderExitRequest = ssz.FixedContainerType(struct { + source_address: p.ExecutionAddress, + pubkey: p.BLSPubkey, +}); + +pub const BuilderExitRequests = ssz.FixedListType( + BuilderExitRequest, + preset.MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, + .{}, +); + +pub const ExecutionRequests = ssz.VariableContainerType(struct { + deposits: ssz.FixedListType(DepositRequest, preset.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, .{}), + withdrawals: ssz.FixedListType(WithdrawalRequest, preset.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, .{}), + consolidations: ssz.FixedListType(ConsolidationRequest, preset.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, .{}), + builder_deposits: BuilderDepositRequests, + builder_exits: BuilderExitRequests, }); pub const PayloadAttestationData = ssz.FixedContainerType(struct { @@ -179,10 +214,11 @@ pub const IndexedPayloadAttestation = ssz.VariableContainerType(struct { }); pub const ProposerPreferences = ssz.FixedContainerType(struct { + dependent_root: p.Root, proposal_slot: p.Slot, validator_index: p.ValidatorIndex, fee_recipient: p.ExecutionAddress, - gas_limit: p.Uint64, + target_gas_limit: p.Uint64, }); pub const SignedProposerPreferences = ssz.FixedContainerType(struct { diff --git a/src/constants/root.zig b/src/constants/root.zig index 5aed64870..8fd708314 100644 --- a/src/constants/root.zig +++ b/src/constants/root.zig @@ -32,6 +32,7 @@ pub const DOMAIN_BLS_TO_EXECUTION_CHANGE = [_]u8{ 10, 0, 0, 0 }; pub const DOMAIN_BEACON_BUILDER = [_]u8{ 11, 0, 0, 0 }; // 0x0B000000 pub const DOMAIN_PTC_ATTESTER = [_]u8{ 12, 0, 0, 0 }; // 0x0C000000 pub const DOMAIN_PROPOSER_PREFERENCES = [_]u8{ 13, 0, 0, 0 }; // 0x0D000000 +pub const DOMAIN_BUILDER_DEPOSIT = [_]u8{ 14, 0, 0, 0 }; // 0x0E000000 // Application specific domains @@ -39,7 +40,25 @@ pub const DOMAIN_APPLICATION_MASK = [_]u8{ 0, 0, 0, 1 }; pub const DOMAIN_APPLICATION_BUILDER = [_]u8{ 0, 0, 0, 1 }; // need to be updated when new domain is added -pub const ALL_DOMAINS = [_][4]u8{ DOMAIN_BEACON_PROPOSER, DOMAIN_BEACON_ATTESTER, DOMAIN_RANDAO, DOMAIN_DEPOSIT, DOMAIN_VOLUNTARY_EXIT, DOMAIN_SELECTION_PROOF, DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_CONTRIBUTION_AND_PROOF, DOMAIN_BLS_TO_EXECUTION_CHANGE, DOMAIN_BEACON_BUILDER, DOMAIN_PTC_ATTESTER, DOMAIN_PROPOSER_PREFERENCES, DOMAIN_APPLICATION_MASK, DOMAIN_APPLICATION_BUILDER }; +pub const ALL_DOMAINS = [_][4]u8{ + DOMAIN_BEACON_PROPOSER, + DOMAIN_BEACON_ATTESTER, + DOMAIN_RANDAO, + DOMAIN_DEPOSIT, + DOMAIN_VOLUNTARY_EXIT, + DOMAIN_SELECTION_PROOF, + DOMAIN_AGGREGATE_AND_PROOF, + DOMAIN_SYNC_COMMITTEE, + DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, + DOMAIN_CONTRIBUTION_AND_PROOF, + DOMAIN_BLS_TO_EXECUTION_CHANGE, + DOMAIN_BEACON_BUILDER, + DOMAIN_PTC_ATTESTER, + DOMAIN_PROPOSER_PREFERENCES, + DOMAIN_BUILDER_DEPOSIT, + DOMAIN_APPLICATION_MASK, + DOMAIN_APPLICATION_BUILDER, +}; // Gloas (EIP-7732) misc @@ -47,6 +66,7 @@ pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; // 2**40 pub const BUILDER_INDEX_SELF_BUILD: u64 = std.math.maxInt(u64); pub const BUILDER_PAYMENT_THRESHOLD_NUMERATOR = 6; pub const BUILDER_PAYMENT_THRESHOLD_DENOMINATOR = 10; +pub const PAYLOAD_BUILDER_VERSION = 0; // Participation flag indices @@ -122,6 +142,8 @@ pub const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA = 23; pub const DEPOSIT_REQUEST_TYPE = 0x00; pub const WITHDRAWAL_REQUEST_TYPE = 0x01; pub const CONSOLIDATION_REQUEST_TYPE = 0x02; +pub const BUILDER_DEPOSIT_REQUEST_TYPE = 0x03; +pub const BUILDER_EXIT_REQUEST_TYPE = 0x04; pub const CURRENT_SYNC_COMMITTEE_GINDEX = 54; pub const EXECUTION_PAYLOAD_GINDEX = 25; diff --git a/src/preset/preset.zig b/src/preset/preset.zig index dd85b491a..f3ddfc27f 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -85,6 +85,8 @@ const PresetMainnet = struct { pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16; pub const PTC_SIZE = 512; pub const MAX_PAYLOAD_ATTESTATIONS = 4; + pub const MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD = 256; + pub const MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD = 16; pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576; pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16384; @@ -161,8 +163,10 @@ const PresetMinimal = struct { pub const KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH = 4; pub const MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 2; pub const MAX_PENDING_DEPOSITS_PER_EPOCH = PresetMainnet.MAX_PENDING_DEPOSITS_PER_EPOCH; - pub const PTC_SIZE = 2; + pub const PTC_SIZE = 16; pub const MAX_PAYLOAD_ATTESTATIONS = 4; + pub const MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD = 256; + pub const MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD = 16; pub const BUILDER_REGISTRY_LIMIT = 1_099_511_627_776; pub const BUILDER_PENDING_WITHDRAWALS_LIMIT = 1_048_576; pub const MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16; 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..2fe43b650 --- /dev/null +++ b/src/state_transition/block/is_valid_indexed_payload_attestation.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const types = @import("consensus_types"); +const ValidatorIndex = types.primitive.ValidatorIndex.Type; +const BeaconConfig = @import("config").BeaconConfig; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const getIndexedPayloadAttestationSignatureSet = @import("../signature_sets/indexed_payload_attestation.zig").getIndexedPayloadAttestationSignatureSet; +const verifyAggregatedSignatureSet = @import("../utils/signature_sets.zig").verifyAggregatedSignatureSet; + +/// Validate an IndexedPayloadAttestation: check that attesting indices are non-empty, +/// sorted, and (optionally) that the aggregate BLS signature is valid. +pub fn isValidIndexedPayloadAttestation( + allocator: std.mem.Allocator, + io: std.Io, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + indexed_payload_attestation: *const types.gloas.IndexedPayloadAttestation.Type, + verify_signature: bool, +) !bool { + const attesting_indices = indexed_payload_attestation.attesting_indices.items; + + if (attesting_indices.len == 0) return false; + + var prev: ValidatorIndex = 0; + for (attesting_indices, 0..) |index, i| { + if (i >= 1 and index < prev) { + return false; + } + prev = index; + } + + if (!verify_signature) return true; + + const sig_set = try getIndexedPayloadAttestationSignatureSet(allocator, io, config, epoch_cache, indexed_payload_attestation); + defer allocator.free(sig_set.pubkeys); + return verifyAggregatedSignatureSet(&sig_set); +} diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index 8cab8458a..5f80dbee2 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; @@ -50,10 +53,18 @@ pub fn processAttestationsAltair( defer root_cache.deinit(); // Process all attestations first and then increase the balance of the proposer once - // let newSeenAttesters = 0; - // let newSeenAttestersEffectiveBalance = 0; - + // TODO: metrics — newSeenAttesters, newSeenAttestersEffectiveBalance, attestationsPerBlock var proposer_reward: u64 = 0; + + var builder_weight_map = std.AutoHashMap(u64, u64).init(allocator); + defer builder_weight_map.deinit(); + + // Get executionPayloadAvailability for gloas + const execution_payload_availability = if (fork.gte(.gloas)) + try state.inner.get("execution_payload_availability") + else + @as(?void, null); + for (attestations) |*attestation| { const data = &attestation.data; try validateAttestation(fork, epoch_cache, state, attestation); @@ -83,7 +94,17 @@ pub fn processAttestationsAltair( const in_current_epoch = data.target.epoch == current_epoch; var epoch_participation = if (in_current_epoch) try state.currentEpochParticipation() else try state.previousEpochParticipation(); - const flags_attestation = try getAttestationParticipationStatus(fork, data, state_slot - data.slot, current_epoch, root_cache); + // Count how much additional weight added to current or previous epoch's builder pending payment (in ETH increment) + var payment_weight_to_add: u64 = 0; + + const flags_attestation = try getAttestationParticipationStatus( + fork, + data, + state_slot - data.slot, + current_epoch, + root_cache, + execution_payload_availability, + ); // For each participant, update their participation // In epoch processing, this participation info is used to calculate balance updates @@ -124,13 +145,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( @@ -138,7 +196,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 @@ -150,9 +209,29 @@ pub fn getAttestationParticipationStatus( const is_matching_target = std.mem.eql(u8, &data.target.root, try root_cache.getBlockRoot(data.target.epoch)); // a timely head is only be set if the target is _also_ matching - const is_matching_head = + var is_matching_head = is_matching_target and std.mem.eql(u8, &data.beacon_block_root, try root_cache.getBlockRootAtSlot(data.slot)); + if (fork.gte(.gloas)) { + var is_matching_payload = false; + + if (try isAttestationSameSlotRootCache(root_cache, data)) { + if (data.index != 0) { + return error.AttestingSameSlotMustIndicateEmptyPayload; + } + is_matching_payload = true; + } else { + if (data.index != 0 and data.index != 1) { + return error.DataIndexMustBe0Or1; + } + + is_matching_payload = + (data.index != 0) == try execution_payload_availability.get(data.slot % preset.SLOTS_PER_HISTORICAL_ROOT); + } + + is_matching_head = is_matching_head and is_matching_payload; + } + var flags: u8 = 0; if (is_matching_source and inclusion_delay <= SLOTS_PER_EPOCH_SQRT) flags |= TIMELY_SOURCE; if (is_matching_target and isTimelyTarget(fork, inclusion_delay)) flags |= TIMELY_TARGET; diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index 8ed9261c3..d9de2115c 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -94,8 +94,14 @@ pub fn validateAttestation( } if (fork.gte(.electra)) { - if (data.index != 0) { - return error.InvalidAttestationNonZeroDataIndex; + if (fork.gte(.gloas)) { + if (data.index >= 2) { + return error.InvalidAttestationDataIndexOutOfRange; + } + } else { + if (data.index != 0) { + return error.InvalidAttestationNonZeroDataIndex; + } } var committee_indices_buffer: [preset.MAX_COMMITTEES_PER_SLOT]usize = undefined; const committee_indices_len = try attestation.committee_bits.getTrueBitIndexes(committee_indices_buffer[0..]); diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index ae9f40ac0..06f2f71aa 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -24,7 +24,10 @@ 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 processParentExecutionPayload = @import("./process_parent_execution_payload.zig").processParentExecutionPayload; const isExecutionEnabled = @import("../utils/execution.zig").isExecutionEnabled; +const isParentBlockFull = @import("../utils/gloas.zig").isParentBlockFull; // TODO: proposer reward api // const ProposerRewardType = @import("../types/proposer_reward.zig").ProposerRewardType; @@ -49,12 +52,39 @@ pub fn processBlock( ) !void { // Build slashings cache against the *current* latest_block_header slot (pre-header update). try buildSlashingsCacheIfNeeded(allocator, state, slashings_cache); + if (comptime fork.gte(.gloas) and block_type == .full) { + try processParentExecutionPayload(allocator, io, config, epoch_cache, state, block); + } try processBlockHeader(fork, allocator, epoch_cache, state, block_type, block); // Keep cache slot in sync with latest_block_header without forcing a rebuild. slashings_cache.updateLatestBlockSlot(block.slot()); const body = block.body(); const current_epoch = epoch_cache.epoch; + if (comptime fork.gte(.gloas)) { + if (try isParentBlockFull(state)) { + var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( + allocator, + preset.MAX_WITHDRAWALS_PER_PAYLOAD, + ) }; + defer withdrawals_result.withdrawals.deinit(allocator); + var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); + defer withdrawal_balances.deinit(); + + try getExpectedWithdrawals( + fork, + allocator, + epoch_cache, + state, + &withdrawals_result, + &withdrawal_balances, + ); + + const empty_root: Root = .{0} ** 32; + try processWithdrawals(fork, allocator, state, withdrawals_result, empty_root); + } + } + // 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. // Gloas (ePBS): execution payload is decoupled from the block, processed via ExecutionPayloadEnvelope @@ -63,13 +93,17 @@ pub fn processBlock( // 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)) { - var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined; - var withdrawals_result = WithdrawalsResult{ .withdrawals = Withdrawals.initBuffer(&withdrawals_buf) }; + var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( + allocator, + preset.MAX_WITHDRAWALS_PER_PAYLOAD, + ) }; + defer withdrawals_result.withdrawals.deinit(allocator); var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); defer withdrawal_balances.deinit(); try getExpectedWithdrawals( fork, + allocator, epoch_cache, state, &withdrawals_result, @@ -102,6 +136,17 @@ pub fn processBlock( } } + if (comptime fork.gte(.gloas)) { + if (comptime block_type != .full) return error.InvalidBlockTypeForFork; + try processExecutionPayloadBid( + allocator, + config, + epoch_cache, + state, + &body.inner.signed_execution_payload_bid, + ); + } + try processRandao(fork, io, config, epoch_cache, state, block_type, body, block.proposerIndex(), opts.verify_signature); try processEth1Data(fork, state, body.eth1Data()); try processOperations(fork, allocator, io, config, epoch_cache, state, slashings_cache, block_type, body, opts); @@ -118,3 +163,78 @@ pub fn processBlock( } } } + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const generateGloasBlock = @import("../test_utils/root.zig").generateGloasBlock; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +fn tryGloasBlock( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *EpochCache, + baseline: *BeaconState(.gloas), + block: *const types.gloas.BeaconBlock.Type, +) !void { + var state = try baseline.clone(.{ .transfer_cache = false }); + defer state.deinit(); + var slashings_cache = try SlashingsCache.initEmpty(std.testing.allocator); + defer slashings_cache.deinit(); + const fork_block = BeaconBlock(.full, .gloas){ .inner = block.* }; + + try processBlock( + .gloas, + allocator, + std.testing.io, + config, + epoch_cache, + &state, + &slashings_cache, + .full, + &fork_block, + .{}, + .{ .verify_signature = false }, + ); +} + +test "Gloas process block - OOM does not leak or double-free transient allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + var block: types.gloas.BeaconBlock.Type = undefined; + try generateGloasBlock(test_state.cached_state, &block); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 512) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + tryGloasBlock( + oom.allocator(), + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + test_state.cached_state.state.castToFork(.gloas), + &block, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} diff --git a/src/state_transition/block/process_builder_deposit_request.zig b/src/state_transition/block/process_builder_deposit_request.zig new file mode 100644 index 000000000..340b253ce --- /dev/null +++ b/src/state_transition/block/process_builder_deposit_request.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const BeaconState = @import("fork_types").BeaconState; +const types = @import("consensus_types"); +const c = @import("constants"); +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const gloas_utils = @import("../utils/gloas.zig"); +const addBuilderToRegistry = gloas_utils.addBuilderToRegistry; +const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; +const isValidBuilderDepositSignature = gloas_utils.isValidBuilderDepositSignature; + +/// Process a builder deposit request from the execution layer: register a new builder +/// (proof-of-possession gated) or top up an existing builder's balance. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/beacon-chain.md#new-process_builder_deposit_request +pub fn processBuilderDepositRequest( + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(.gloas), + request: *const types.gloas.BuilderDepositRequest.Type, +) !void { + const builder_index = try findBuilderIndexByPubkey(allocator, state, &request.pubkey); + + if (builder_index) |idx| { + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, idx, &builder); + + if (builder.withdrawable_epoch != c.FAR_FUTURE_EPOCH) { + builder.withdrawable_epoch = computeEpochAtSlot(try state.slot()) + config.chain.MIN_BUILDER_WITHDRAWABILITY_DELAY; + } + + builder.balance += request.amount; + + try builders.setValue(idx, &builder); + return; + } + + if (!isValidBuilderDepositSignature( + config, + &request.pubkey, + &request.withdrawal_credentials, + request.amount, + request.signature, + )) { + return; + } + + var execution_address: types.primitive.ExecutionAddress.Type = undefined; + @memcpy(&execution_address, request.withdrawal_credentials[12..32]); + + try addBuilderToRegistry( + allocator, + state, + &request.pubkey, + request.withdrawal_credentials[0], + &execution_address, + request.amount, + try state.slot(), + ); +} diff --git a/src/state_transition/block/process_builder_exit_request.zig b/src/state_transition/block/process_builder_exit_request.zig new file mode 100644 index 000000000..164226fb7 --- /dev/null +++ b/src/state_transition/block/process_builder_exit_request.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const BeaconState = @import("fork_types").BeaconState; +const types = @import("consensus_types"); +const gloas_utils = @import("../utils/gloas.zig"); +const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; +const getPendingBalanceToWithdrawForBuilder = gloas_utils.getPendingBalanceToWithdrawForBuilder; +const initiateBuilderExit = gloas_utils.initiateBuilderExit; +const isActiveBuilder = gloas_utils.isActiveBuilder; + +/// Apply a builder exit request. Authorizes the exit via `source_address` (the builder's +/// execution_address), not the BLS key — mirroring EIP-7002's 0x01 credential exit. +/// +/// Drops the request silently if any precondition fails; the EL has already dequeued it +/// deterministically, so the fee is forfeited but `requests_hash` agreement is unaffected. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/beacon-chain.md#new-process_builder_exit_request +pub fn processBuilderExitRequest( + allocator: Allocator, + config: *const BeaconConfig, + state: *BeaconState(.gloas), + request: *const types.gloas.BuilderExitRequest.Type, +) !void { + const maybe_builder_index = try findBuilderIndexByPubkey(allocator, state, &request.pubkey); + const builder_index = maybe_builder_index orelse return; + + var builders = try state.inner.get("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders.getValue(allocator, builder_index, &builder); + + if (!isActiveBuilder(&builder, try state.finalizedEpoch())) { + return; + } + if (!std.mem.eql(u8, &builder.execution_address, &request.source_address)) { + return; + } + if (try getPendingBalanceToWithdrawForBuilder(allocator, state, builder_index) != 0) { + return; + } + + try initiateBuilderExit(config, state, allocator, builder_index); +} diff --git a/src/state_transition/block/process_consolidation_request.zig b/src/state_transition/block/process_consolidation_request.zig index cb05f56ef..b0c25096b 100644 --- a/src/state_transition/block/process_consolidation_request.zig +++ b/src/state_transition/block/process_consolidation_request.zig @@ -55,7 +55,7 @@ pub fn processConsolidationRequest( } // If there is too little available consolidation churn limit, consolidation requests are ignored - if (getConsolidationChurnLimit(epoch_cache) <= preset.MIN_ACTIVATION_BALANCE) { + if (getConsolidationChurnLimit(fork, epoch_cache) <= preset.MIN_ACTIVATION_BALANCE) { return; } diff --git a/src/state_transition/block/process_execution_payload.zig b/src/state_transition/block/process_execution_payload.zig index 4900511e1..c65dd8a26 100644 --- a/src/state_transition/block/process_execution_payload.zig +++ b/src/state_transition/block/process_execution_payload.zig @@ -38,7 +38,12 @@ pub fn processExecutionPayload( // Verify consistency of the parent hash, block number, base fee per gas and gas limit // with respect to the previous execution payload header - if (isMergeTransitionComplete(fork, state)) { + if (comptime fork.gte(.capella)) { + const latest_block_hash = try state.latestExecutionPayloadHeaderBlockHash(); + if (!std.mem.eql(u8, parent_hash, latest_block_hash)) { + return error.InvalidExecutionPayloadParentHash; + } + } else if (isMergeTransitionComplete(fork, state)) { const latest_block_hash = try state.latestExecutionPayloadHeaderBlockHash(); if (!std.mem.eql(u8, parent_hash, latest_block_hash)) { return error.InvalidExecutionPayloadParentHash; 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..929c10407 --- /dev/null +++ b/src/state_transition/block/process_execution_payload_bid.zig @@ -0,0 +1,213 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconConfig = @import("config").BeaconConfig; +const BeaconState = @import("fork_types").BeaconState; + +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +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; +const getBlockRootAtSlot = @import("../utils/block_root.zig").getBlockRootAtSlot; + +pub fn processExecutionPayloadBid( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + signed_bid: *const types.gloas.SignedExecutionPayloadBid.Type, +) !void { + const bid = &signed_bid.message; + const builder_index = bid.builder_index; + const amount = bid.value; + const state_slot = try state.slot(); + + 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 (builder.version != c.PAYLOAD_BUILDER_VERSION) return error.InvalidBuilderVersion; + + if (!(try canBuilderCoverBid(allocator, state, builder_index, amount))) return error.BuilderInsufficientBalance; + + if (!(try verifyExecutionPayloadBidSignature(allocator, config, state_slot, &builder.pubkey, signed_bid))) return error.InvalidBidSignature; + } + + if (bid.slot != state_slot) return error.BidSlotMismatch; + if (state_slot <= c.GENESIS_SLOT) return error.ExecutionPayloadBidAtGenesis; + + 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; + + const parent_block_root = try getBlockRootAtSlot(.gloas, state, state_slot - 1); + if (!std.mem.eql(u8, &bid.parent_block_root, parent_block_root)) return error.BidParentBlockRootMismatch; + + const current_epoch = computeEpochAtSlot(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, + }, + .proposer_index = try epoch_cache.getBeaconProposer(state_slot), + }; + 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, .{}) catch return false; + return true; +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const interopSign = @import("../test_utils/root.zig").interopSign; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +fn addTestBuilder(state: *BeaconState(.gloas), amount: u64) !void { + var validators = try state.validators(); + var validator: types.phase0.Validator.Type = undefined; + try validators.getValue(undefined, 0, &validator); + + const builder = types.gloas.Builder.Type{ + .pubkey = validator.pubkey, + .version = c.PAYLOAD_BUILDER_VERSION, + .execution_address = [_]u8{0x42} ** 20, + .balance = preset.MIN_DEPOSIT_AMOUNT + amount, + .deposit_epoch = 0, + .withdrawable_epoch = c.FAR_FUTURE_EPOCH, + }; + var builders = try state.inner.get("builders"); + try builders.pushValue(&builder); + try state.inner.set("builders", builders); + try state.commit(); +} + +fn makeSignedTestBid( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + amount: u64, +) !types.gloas.SignedExecutionPayloadBid.Type { + const slot = try state.slot(); + const parent_block_hash = try state.inner.getFieldRoot("latest_block_hash"); + const parent_block_root = try getBlockRootAtSlot(.gloas, state, slot - 1); + const prev_randao = try getRandaoMix(.gloas, state, epoch_cache.epoch); + + var signed_bid = types.gloas.SignedExecutionPayloadBid.default_value; + signed_bid.message.parent_block_hash = parent_block_hash.*; + signed_bid.message.parent_block_root = parent_block_root.*; + signed_bid.message.prev_randao = prev_randao.*; + signed_bid.message.builder_index = 0; + signed_bid.message.slot = slot; + signed_bid.message.value = amount; + try signed_bid.message.blob_kzg_commitments.append( + allocator, + types.primitive.KZGCommitment.default_value, + ); + + const signing_root = try getExecutionPayloadBidSigningRoot(allocator, config, slot, &signed_bid.message); + signed_bid.signature = (try interopSign(0, &signing_root)).compress(); + return signed_bid; +} + +fn tryExternalBuilderBid( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + baseline: *BeaconState(.gloas), + signed_bid: *const types.gloas.SignedExecutionPayloadBid.Type, +) !void { + var state = try baseline.clone(.{ .transfer_cache = false }); + defer state.deinit(); + try processExecutionPayloadBid(allocator, config, epoch_cache, &state, signed_bid); +} + +test "Gloas external builder bid - OOM does not leak or double-free transient allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + const state = test_state.cached_state.state.castToFork(.gloas); + const bid_amount = 1; + try addTestBuilder(state, bid_amount); + + var signed_bid = try makeSignedTestBid( + allocator, + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + state, + bid_amount, + ); + defer types.gloas.SignedExecutionPayloadBid.deinit(allocator, &signed_bid); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 128) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + tryExternalBuilderBid( + oom.allocator(), + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + state, + &signed_bid, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} diff --git a/src/state_transition/block/process_operations.zig b/src/state_transition/block/process_operations.zig index 178f692dd..481108c1d 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, @@ -93,6 +94,12 @@ pub fn processOperations( try processConsolidationRequest(fork, io, config, epoch_cache, state, consolidation_request); } } + + if (comptime fork.gte(.gloas)) { + for (body.inner.payload_attestations.items) |*payload_attestation| { + try processPayloadAttestation(allocator, io, config, epoch_cache, state, payload_attestation); + } + } } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; diff --git a/src/state_transition/block/process_parent_execution_payload.zig b/src/state_transition/block/process_parent_execution_payload.zig new file mode 100644 index 000000000..d7ffaa2d7 --- /dev/null +++ b/src/state_transition/block/process_parent_execution_payload.zig @@ -0,0 +1,138 @@ +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 EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const types = @import("consensus_types"); +const preset = @import("preset").preset; +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 processBuilderDepositRequest = @import("./process_builder_deposit_request.zig").processBuilderDepositRequest; +const processBuilderExitRequest = @import("./process_builder_exit_request.zig").processBuilderExitRequest; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; + +/// Process parent execution payload effects as the first step of processBlock. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-process_parent_execution_payload +pub fn processParentExecutionPayload( + allocator: Allocator, + io: std.Io, + config: *const BeaconConfig, + epoch_cache: *EpochCache, + state: *BeaconState(.gloas), + block: *const BeaconBlock(.full, .gloas), +) !void { + const bid = &block.body().inner.signed_execution_payload_bid.message; + var parent_bid = types.gloas.ExecutionPayloadBid.default_value; + try state.inner.getValue(allocator, "latest_execution_payload_bid", &parent_bid); + defer types.gloas.ExecutionPayloadBid.deinit(allocator, &parent_bid); + + const requests = &block.body().inner.parent_execution_requests; + const is_parent_block_full = std.mem.eql(u8, &bid.parent_block_hash, &parent_bid.block_hash); + if (!is_parent_block_full) { + try assertEmptyExecutionRequests(requests); + return; + } + + var requests_root: [32]u8 = undefined; + try types.gloas.ExecutionRequests.hashTreeRoot(allocator, requests, &requests_root); + if (!std.mem.eql(u8, &requests_root, &parent_bid.execution_requests_root)) { + return error.ParentExecutionRequestsRootMismatch; + } + + try applyParentExecutionPayload(allocator, io, config, epoch_cache, state, requests, &parent_bid); +} + +/// Process the parent's execution requests, queue the builder payment, update payload availability, +/// and update the latest block hash. +/// +/// Called from processParentExecutionPayload during block processing, and from the validator during +/// block production before computing withdrawals. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload +pub fn applyParentExecutionPayload( + allocator: Allocator, + io: std.Io, + config: *const BeaconConfig, + epoch_cache: *EpochCache, + state: *BeaconState(.gloas), + requests: *const types.gloas.ExecutionRequests.Type, + parent_bid: *const types.gloas.ExecutionPayloadBid.Type, +) !void { + const parent_slot = parent_bid.slot; + const parent_epoch = computeEpochAtSlot(parent_slot); + const current_epoch = computeEpochAtSlot(try state.slot()); + + for (requests.deposits.items) |*deposit| { + try processDepositRequest(.gloas, state, deposit); + } + + for (requests.withdrawals.items) |*withdrawal| { + try processWithdrawalRequest(.gloas, io, config, epoch_cache, state, withdrawal); + } + + for (requests.consolidations.items) |*consolidation| { + try processConsolidationRequest(.gloas, io, config, epoch_cache, state, consolidation); + } + + for (requests.builder_deposits.items) |*builder_deposit| { + try processBuilderDepositRequest(allocator, config, state, builder_deposit); + } + + for (requests.builder_exits.items) |*builder_exit| { + try processBuilderExitRequest(allocator, config, state, builder_exit); + } + + if (parent_epoch == current_epoch) { + try settleBuilderPayment(allocator, state, preset.SLOTS_PER_EPOCH + (parent_slot % preset.SLOTS_PER_EPOCH)); + } else if (parent_epoch + 1 == current_epoch) { + try settleBuilderPayment(allocator, state, parent_slot % preset.SLOTS_PER_EPOCH); + } else if (parent_bid.value > 0) { + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + const withdrawal = types.gloas.BuilderPendingWithdrawal.Type{ + .fee_recipient = parent_bid.fee_recipient, + .amount = parent_bid.value, + .builder_index = parent_bid.builder_index, + }; + try builder_pending_withdrawals.pushValue(&withdrawal); + try state.inner.set("builder_pending_withdrawals", builder_pending_withdrawals); + } + + var execution_payload_availability = try state.inner.get("execution_payload_availability"); + try execution_payload_availability.set(parent_slot % preset.SLOTS_PER_HISTORICAL_ROOT, true); + try state.inner.set("execution_payload_availability", execution_payload_availability); + try state.inner.setValue("latest_block_hash", &parent_bid.block_hash); +} + +/// Settle a builder payment at the given index: move its withdrawal (if any) to the +/// pending withdrawals list and clear the payment slot. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-settle_builder_payment +fn settleBuilderPayment(allocator: Allocator, state: *BeaconState(.gloas), payment_index: u64) !void { + var builder_pending_payments = try state.inner.get("builder_pending_payments"); + if (payment_index >= 2 * preset.SLOTS_PER_EPOCH) return error.InvalidBuilderPendingPaymentIndex; + + var payment: types.gloas.BuilderPendingPayment.Type = undefined; + try builder_pending_payments.getValue(allocator, payment_index, &payment); + if (payment.withdrawal.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; + try builder_pending_payments.setValue(payment_index, &default_payment); +} + +fn assertEmptyExecutionRequests(requests: *const types.gloas.ExecutionRequests.Type) !void { + if (requests.deposits.items.len != 0 or + requests.withdrawals.items.len != 0 or + requests.consolidations.items.len != 0 or + requests.builder_deposits.items.len != 0 or + requests.builder_exits.items.len != 0) + { + return error.ParentExecutionRequestsNotEmpty; + } +} 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..4607cf2e5 --- /dev/null +++ b/src/state_transition/block/process_payload_attestation.zig @@ -0,0 +1,90 @@ +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, + io: std.Io, + 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; + } + + 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, io, config, epoch_cache, &indexed_payload_attestation, true))) { + return error.InvalidPayloadAttestation; + } +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +test "Gloas payload attestation - OOM does not double-free indexed data" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + const state = test_state.cached_state.state.castToFork(.gloas); + + var latest_block_header = try state.latestBlockHeader(); + var payload_attestation = types.gloas.PayloadAttestation.default_value; + payload_attestation.data.slot = (try state.slot()) - 1; + payload_attestation.data.beacon_block_root = (try latest_block_header.getFieldRoot("parent_root")).*; + try payload_attestation.aggregation_bits.set(0, true); + + var saw_oom = false; + var saw_terminal_result = false; + var fail_at: usize = 0; + while (fail_at < 64) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + processPayloadAttestation( + oom.allocator(), + std.testing.io, + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + state, + &payload_attestation, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => { + try std.testing.expect(!oom.double_free); + saw_terminal_result = true; + break; + }, + }; + + try std.testing.expect(!oom.double_free); + saw_terminal_result = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_terminal_result); +} diff --git a/src/state_transition/block/process_proposer_slashing.zig b/src/state_transition/block/process_proposer_slashing.zig index 903877e4e..834b1ded7 100644 --- a/src/state_transition/block/process_proposer_slashing.zig +++ b/src/state_transition/block/process_proposer_slashing.zig @@ -11,6 +11,9 @@ 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 computePreviousEpoch = @import("../utils/epoch.zig").computePreviousEpoch; +const preset = @import("preset").preset; pub fn processProposerSlashing( comptime fork: ForkSeq, @@ -25,6 +28,31 @@ pub fn processProposerSlashing( ) !void { try buildSlashingsCacheIfNeeded(allocator, state, slashings_cache); try assertValidProposerSlashing(fork, io, 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 = computePreviousEpoch(current_epoch); + + 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"); + var payment: types.gloas.BuilderPendingPayment.Type = undefined; + try builder_pending_payments.getValue(allocator, idx, &payment); + if (payment.proposer_index == proposer_slashing.signed_header_1.message.proposer_index) { + 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); } diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 501dafe9d..d7b388442 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -13,13 +13,26 @@ 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"); +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_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 +46,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 checked through parent execution payload processing. + 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 parent execution payload verification. + 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 @@ -82,143 +122,425 @@ pub fn processWithdrawals( } } -/// Called by the block proposer to find a list of withdrawals to include in the block. -/// -/// This list is assumed to be bounded by `preset.MAX_WITHDRAWALS_PER_PAYLOAD`. -/// -/// Caller should deinit `withdrawal_balances` with .deinit() after use. +// Consumer should deinit WithdrawalsResult with .deinit() after use pub fn getExpectedWithdrawals( comptime fork: ForkSeq, + allocator: Allocator, epoch_cache: *const EpochCache, state: *BeaconState(fork), withdrawals_result: *WithdrawalsResult, withdrawal_balances: *std.AutoHashMap(ValidatorIndex, usize), ) !void { - std.debug.assert(withdrawals_result.withdrawals.capacity == preset.MAX_WITHDRAWALS_PER_PAYLOAD); - if (comptime fork.lt(.capella)) return error.InvalidForkSequence; + if (comptime fork.lt(.capella)) { + return error.InvalidForkSequence; + } 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, i64).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)) { + var result = try getBuilderWithdrawals( + allocator, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + &builder_balance_after_withdrawals, + ); + defer result.withdrawals.deinit(allocator); + 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(); - 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..]); - withdrawals_result.withdrawals.appendAssumeCapacity(.{ - .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; + var result = try getPendingPartialWithdrawals( + fork, + allocator, + epoch, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + withdrawal_balances, + ); + defer result.withdrawals.deinit(allocator); + 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)) { + var result = try getBuildersSweepWithdrawals( + allocator, + state, + epoch, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + &builder_balance_after_withdrawals, + ); + defer result.withdrawals.deinit(allocator); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); + } + processed_builders_sweep_count = result.processed_count; + } + + // get_validators_sweep_withdrawals + { + var result = try getValidatorsSweepWithdrawals( + fork, + allocator, + epoch, + state, + &withdrawal_index, + withdrawals_result.withdrawals.items.len, + withdrawal_balances, + ); + defer result.withdrawals.deinit(allocator); + for (result.withdrawals.items) |w| { + try withdrawals_result.withdrawals.append(allocator, w); + } + withdrawals_result.sampled_validators = 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 (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) = .empty; + errdefer pending_partial_withdrawals_result.deinit(allocator); + + 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(); + 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(allocator, .{ + .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) = .empty; + errdefer sweep_withdrawals.deinit(allocator); + + 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; + } + // 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"); 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..]); - withdrawals_result.withdrawals.appendAssumeCapacity(.{ - .index = withdrawal_index, + try sweep_withdrawals.append(allocator, .{ + .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) - { - // capella partial withdrawal - const partial_amount = balance - effective_balance; + withdrawal_index.* += 1; + balance_gop.value_ptr.* = 0; + } else if (isPartiallyWithdrawableValidator(fork, withdrawal_credentials, effective_balance, 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..]); - withdrawals_result.withdrawals.appendAssumeCapacity(.{ - .index = withdrawal_index, + try sweep_withdrawals.append(allocator, .{ + .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; + processed_count += 1; + } + + return .{ .withdrawals = sweep_withdrawals, .processed_count = processed_count }; +} + +fn getBuilderWithdrawals( + allocator: Allocator, + state: *BeaconState(.gloas), + withdrawal_index: *u64, + prior_withdrawals_len: usize, + 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) { + return error.PriorWithdrawalsExceedLimit; + } + var builder_withdrawals: std.ArrayList(Withdrawal) = .empty; + errdefer builder_withdrawals.deinit(allocator); + + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + try builder_pending_withdrawals.commit(); + 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(); + 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.* = @intCast(builder.balance); } + + // Use the withdrawal amount directly as specified in the spec + try builder_withdrawals.append(allocator, .{ + .index = withdrawal_index.*, + .validator_index = convertBuilderIndexToValidatorIndex(builder_index), + .address = bw.fee_recipient, + .amount = bw.amount, + }); + withdrawal_index.* += 1; + balance_gop.value_ptr.* -= @as(i64, @intCast(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, i64), +) !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) = .empty; + errdefer builders_sweep_withdrawals.deinit(allocator); + + 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"); + if (next_withdrawal_builder_index >= builders_len) { + return error.InvalidNextWithdrawalBuilderIndex; + } + 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.* = @intCast(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(allocator, .{ + .index = withdrawal_index.*, + .validator_index = convertBuilderIndexToValidatorIndex(builder_index), + .address = builder.execution_address, + .amount = @intCast(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; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +fn addGloasWithdrawalInputs(state: *BeaconState(.gloas)) !void { + var builders = try state.inner.get("builders"); + const builder = types.gloas.Builder.Type{ + .pubkey = [_]u8{0x11} ** 48, + .version = c.PAYLOAD_BUILDER_VERSION, + .execution_address = [_]u8{0x22} ** 20, + .balance = 64, + .deposit_epoch = 0, + .withdrawable_epoch = 0, + }; + try builders.pushValue(&builder); + try state.inner.set("builders", builders); + + var builder_pending_withdrawals = try state.inner.get("builder_pending_withdrawals"); + const pending_withdrawal = types.gloas.BuilderPendingWithdrawal.Type{ + .fee_recipient = [_]u8{0x33} ** 20, + .amount = 8, + .builder_index = 0, + }; + try builder_pending_withdrawals.pushValue(&pending_withdrawal); + try state.inner.set("builder_pending_withdrawals", builder_pending_withdrawals); + try state.inner.set("next_withdrawal_builder_index", @as(u64, 0)); + try state.commit(); +} test "process withdrawals - sanity" { const allocator = std.testing.allocator; @@ -229,10 +551,13 @@ test "process withdrawals - sanity" { var test_state = try TestCachedBeaconState.init(allocator, &pool, 256); defer test_state.deinit(); - var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]types.capella.Withdrawal.Type = undefined; var withdrawals_result = WithdrawalsResult{ - .withdrawals = Withdrawals.initBuffer(&withdrawals_buf), + .withdrawals = try Withdrawals.initCapacity( + allocator, + preset.MAX_WITHDRAWALS_PER_PAYLOAD, + ), }; + defer withdrawals_result.withdrawals.deinit(allocator); var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); defer withdrawal_balances.deinit(); @@ -241,6 +566,7 @@ test "process withdrawals - sanity" { try getExpectedWithdrawals( .electra, + allocator, test_state.cached_state.epoch_cache, test_state.cached_state.state.castToFork(.electra), &withdrawals_result, @@ -254,3 +580,129 @@ test "process withdrawals - sanity" { root, ); } + +test "gloas expected withdrawals - OOM does not double-free transient allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + const state = test_state.cached_state.state.castToFork(.gloas); + try addGloasWithdrawalInputs(state); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 512) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + const fault_allocator = oom.allocator(); + + var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]Withdrawal = undefined; + var withdrawals_result = WithdrawalsResult{ + .withdrawals = Withdrawals.initBuffer(&withdrawals_buf), + }; + var call_error: ?anyerror = null; + { + var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(fault_allocator); + defer withdrawal_balances.deinit(); + + getExpectedWithdrawals( + .gloas, + fault_allocator, + test_state.cached_state.epoch_cache, + state, + &withdrawals_result, + &withdrawal_balances, + ) catch |err| { + call_error = err; + }; + } + + try std.testing.expect(!oom.double_free); + if (call_error) |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + continue; + }, + else => return err, + }; + + saw_success = true; + try std.testing.expectEqual(@as(usize, 2), withdrawals_result.withdrawals.items.len); + try std.testing.expectEqual(@as(usize, 1), withdrawals_result.processed_builder_withdrawals_count); + try std.testing.expectEqual(@as(usize, 1), withdrawals_result.processed_builders_sweep_count); + for (withdrawals_result.withdrawals.items) |withdrawal| { + try std.testing.expect(isBuilderIndex(withdrawal.validator_index)); + } + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} + +test "gloas process withdrawals - mutation path applies builder withdrawals and truncates queues" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + const state = test_state.cached_state.state.castToFork(.gloas); + try addGloasWithdrawalInputs(state); + + // processWithdrawals is a no-op unless the parent block is full; the test is only + // meaningful when the precondition holds. + try std.testing.expect(try isParentBlockFull(state)); + + const initial_next_withdrawal_index = try state.nextWithdrawalIndex(); + + var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]Withdrawal = undefined; + var withdrawals_result = WithdrawalsResult{ + .withdrawals = Withdrawals.initBuffer(&withdrawals_buf), + }; + var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); + defer withdrawal_balances.deinit(); + + try getExpectedWithdrawals( + .gloas, + allocator, + test_state.cached_state.epoch_cache, + state, + &withdrawals_result, + &withdrawal_balances, + ); + + // 8 from the builder payment queue + 56 sweeping the remaining builder balance. + try std.testing.expectEqual(@as(usize, 2), withdrawals_result.withdrawals.items.len); + + // payload_withdrawals_root is unused for gloas (parent payload verifies later), pass zeroes. + try processWithdrawals(.gloas, allocator, state, withdrawals_result, [_]u8{0} ** 32); + + // Commit so the assertions read back through the committed tree, not just cached child views. + try state.commit(); + + // applyWithdrawals drained the single builder: 64 - 8 (payment) - 56 (sweep) == 0. + var builders_view = try state.inner.getReadonly("builders"); + var builder: types.gloas.Builder.Type = undefined; + try builders_view.getValue(allocator, 0, &builder); + try std.testing.expectEqual(@as(u64, 0), builder.balance); + + // The processed builder payment was truncated off the queue. + var builder_pending_withdrawals = try state.inner.getReadonly("builder_pending_withdrawals"); + try std.testing.expectEqual(@as(usize, 0), try builder_pending_withdrawals.length()); + + // Both expected withdrawals were recorded for parent-payload verification. + var payload_expected_withdrawals = try state.inner.getReadonly("payload_expected_withdrawals"); + try std.testing.expectEqual(@as(usize, 2), try payload_expected_withdrawals.length()); + + // Sweep advanced the builder cursor by processed_builders_sweep_count (1) modulo builders_len (1). + try std.testing.expectEqual(@as(u64, 0), try state.inner.getReadonly("next_withdrawal_builder_index")); + + // Withdrawal index advanced by the two withdrawals produced this block. + try std.testing.expectEqual(initial_next_withdrawal_index + 2, try state.nextWithdrawalIndex()); +} diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index bbd749e0b..45ccc59ef 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -45,6 +45,31 @@ const getActivationChurnLimit = @import("../utils/validator.zig").getActivationC const ForkSeq = @import("config").ForkSeq; const ForkTypes = @import("fork_types").ForkTypes; +const PtcCommittees = [preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex; + +const PtcWindowEpochCacheData = struct { + previous: PtcCommittees, + current: PtcCommittees, + next: PtcCommittees, +}; + +fn getPtcWindowEpochCacheData(state: *AnyBeaconState) !PtcWindowEpochCacheData { + return getPtcWindowEpochCacheDataFromState(try state.tryCastToFork(.gloas)); +} + +fn getPtcWindowEpochCacheDataFromState(state: *BeaconState(.gloas)) !PtcWindowEpochCacheData { + var ptc_window = try state.inner.getReadonly("ptc_window"); + var data: PtcWindowEpochCacheData = undefined; + + for (0..preset.SLOTS_PER_EPOCH) |i| { + try ptc_window.getValue(undefined, i, &data.previous[i]); + try ptc_window.getValue(undefined, preset.SLOTS_PER_EPOCH + i, &data.current[i]); + try ptc_window.getValue(undefined, 2 * preset.SLOTS_PER_EPOCH + i, &data.next[i]); + } + + return data; +} + pub const EpochCacheImmutableData = struct { config: *const BeaconConfig, pubkey_cache: *PubkeyCache, @@ -125,6 +150,15 @@ pub const EpochCache = struct { epoch: Epoch, + /// 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, + + /// PTC for current epoch, computed eagerly at epoch transition. + payload_timeliness_committees: ?[preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex, + + /// PTC for next epoch, sourced from state.ptc_window or the epoch transition stash. + next_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(allocator); @@ -196,8 +230,7 @@ pub const EpochCache = struct { const pubkey_cache = immutable_data.pubkey_cache; const current_epoch = computeEpochAtSlot(try state.slot()); - const is_genesis = current_epoch == GENESIS_EPOCH; - const previous_epoch = if (is_genesis) GENESIS_EPOCH else current_epoch - 1; + const previous_epoch = if (current_epoch == GENESIS_EPOCH) GENESIS_EPOCH else current_epoch - 1; const next_epoch = current_epoch + 1; var total_active_balance_increments: u64 = 0; @@ -465,8 +498,19 @@ 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, + .next_payload_timeliness_committees = null, }; + // Copy PTC slices from state.ptc_window ad it comes from cached committees. + if (current_epoch >= config.chain.GLOAS_FORK_EPOCH and state.forkSeq().gte(.gloas)) { + const ptc_data = try getPtcWindowEpochCacheData(state); + epoch_cache_ptr.previous_payload_timeliness_committees = ptc_data.previous; + epoch_cache_ptr.payload_timeliness_committees = ptc_data.current; + epoch_cache_ptr.next_payload_timeliness_committees = ptc_data.next; + } + return epoch_cache_ptr; } @@ -523,6 +567,9 @@ pub const EpochCache = struct { .next_sync_committee_indexed = self.next_sync_committee_indexed.ref(), .sync_period = self.sync_period, .epoch = self.epoch, + .payload_timeliness_committees = self.payload_timeliness_committees, + .previous_payload_timeliness_committees = self.previous_payload_timeliness_committees, + .next_payload_timeliness_committees = self.next_payload_timeliness_committees, }; const epoch_cache_ptr = try allocator.create(EpochCache); @@ -556,23 +603,20 @@ pub const EpochCache = struct { return self.effective_balance_increments.get(); } - pub fn afterProcessEpoch(self: *EpochCache, state: *AnyBeaconState, epoch_transition_cache: *const EpochTransitionCache) !void { + fn takeNextShufflingRc(self: *EpochCache, state: *AnyBeaconState, epoch_transition_cache: *EpochTransitionCache) !*EpochShufflingRc { + const next_shuffling = try epoch_transition_cache.takeNextShuffling(self.allocator, state); + errdefer next_shuffling.deinit(); + return try EpochShufflingRc.init(self.allocator, next_shuffling); + } + + pub fn afterProcessEpoch(self: *EpochCache, state: *AnyBeaconState, epoch_transition_cache: *EpochTransitionCache) !void { const upcoming_epoch = self.epoch + 1; const epoch_after_upcoming = upcoming_epoch + 1; const slot = try state.slot(); - const next_shuffling_active_indices = try self.allocator.alloc(ValidatorIndex, epoch_transition_cache.next_shuffling_active_indices.len); - std.mem.copyForwards(ValidatorIndex, next_shuffling_active_indices, epoch_transition_cache.next_shuffling_active_indices); - - const next_shuffling = try computeEpochShuffling( - self.allocator, - state, - next_shuffling_active_indices, - epoch_after_upcoming, - ); - errdefer next_shuffling.deinit(); - - const next_shuffling_rc = try EpochShufflingRc.init(self.allocator, next_shuffling); + const next_shuffling_rc = try self.takeNextShufflingRc(state, epoch_transition_cache); + errdefer next_shuffling_rc.unref(); + const next_decision_root = try calculateShufflingDecisionRoot(state, epoch_after_upcoming); self.previous_shuffling.unref(); self.previous_shuffling = self.current_shuffling; @@ -582,7 +626,7 @@ pub const EpochCache = struct { self.current_decision_root = self.next_decision_root; self.next_shuffling = next_shuffling_rc; - self.next_decision_root = try calculateShufflingDecisionRoot(state, epoch_after_upcoming); + self.next_decision_root = next_decision_root; self.churn_limit = getChurnLimit(self.config, self.current_shuffling.get().active_indices.len); self.activation_churn_limit = getActivationChurnLimit(self.config, self.config.forkSeq(slot), self.current_shuffling.get().active_indices.len); @@ -608,8 +652,33 @@ pub const EpochCache = struct { } /// At fork boundary, this runs post-fork logic and after `upgradeState*`. - pub fn finalProcessEpoch(self: *EpochCache, state: *AnyBeaconState) !void { + pub fn finalProcessEpoch(self: *EpochCache, state: *AnyBeaconState, epoch_transition_cache: ?*const EpochTransitionCache) !void { self.proposers_prev_epoch = self.proposers; + + // Shift cached PTC slices when processPtcWindow stashed the next epoch committees. + // At the fork boundary or in direct tests, rebuild from state.ptc_window instead. + if (self.epoch >= self.config.chain.GLOAS_FORK_EPOCH and state.forkSeq().gte(.gloas)) { + var read_from_state = self.epoch == self.config.chain.GLOAS_FORK_EPOCH or + self.payload_timeliness_committees == null or + self.next_payload_timeliness_committees == null or + epoch_transition_cache == null; + + if (!read_from_state and epoch_transition_cache.?.next_epoch_payload_timeliness_committees == null) { + read_from_state = true; + } + + if (read_from_state) { + const ptc_data = try getPtcWindowEpochCacheData(state); + self.previous_payload_timeliness_committees = ptc_data.previous; + self.payload_timeliness_committees = ptc_data.current; + self.next_payload_timeliness_committees = ptc_data.next; + } else { + self.previous_payload_timeliness_committees = self.payload_timeliness_committees; + self.payload_timeliness_committees = self.next_payload_timeliness_committees; + self.next_payload_timeliness_committees = epoch_transition_cache.?.next_epoch_payload_timeliness_committees; + } + } + switch (state.forkSeq()) { inline else => |fork| { const fork_state = state.castToFork(fork); @@ -920,4 +989,56 @@ 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. + /// Spec: get_ptc — Read PTC from state.ptc_window + pub fn getPayloadTimelinessCommittee(self: *const EpochCache, state: *BeaconState(.gloas), slot: Slot) ![preset.PTC_SIZE]ValidatorIndex { + const epoch = computeEpochAtSlot(slot); + const state_epoch = computeEpochAtSlot(try state.slot()); + + if (epoch < self.config.chain.GLOAS_FORK_EPOCH) return error.PayloadTimelinessCommitteeBeforeGloasFork; + if (epoch < state_epoch -| 1 or epoch > state_epoch + 1) return error.PayloadTimelinessCommitteeEpochOutOfRange; + + var ptc_window = try state.inner.get("ptc_window"); + + const epoch_window_index: u64 = if (epoch < state_epoch) 0 else epoch - state_epoch + 1; + const index = epoch_window_index * preset.SLOTS_PER_EPOCH + slot % preset.SLOTS_PER_EPOCH; + + 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(state, slot); + + var attesting_indices: std.ArrayList(ValidatorIndex) = .empty; + errdefer attesting_indices.deinit(allocator); + + for (0..payload_timeliness_committee.len) |i| { + if (try payload_attestation.aggregation_bits.get(i)) { + try attesting_indices.append(allocator, 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, + .data = payload_attestation.data, + .signature = payload_attestation.signature, + }; + } }; diff --git a/src/state_transition/cache/epoch_transition_cache.zig b/src/state_transition/cache/epoch_transition_cache.zig index e86364e53..7f31ca119 100644 --- a/src/state_transition/cache/epoch_transition_cache.zig +++ b/src/state_transition/cache/epoch_transition_cache.zig @@ -10,6 +10,8 @@ const BeaconConfig = @import("config").BeaconConfig; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const AnyBeaconState = @import("fork_types").AnyBeaconState; const BeaconState = @import("fork_types").BeaconState; +const EpochShuffling = @import("../utils/epoch_shuffling.zig").EpochShuffling; +const computeEpochShuffling = @import("../utils/epoch_shuffling.zig").computeEpochShuffling; const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; const upgradeStateToFulu = @import("../slot/upgrade_state_to_fulu.zig").upgradeStateToFulu; @@ -38,6 +40,7 @@ const BoolArray = std.ArrayList(bool); const UsizeArray = std.ArrayList(usize); const U8Array = std.ArrayList(u8); const U64Array = std.ArrayList(u64); +const PtcCommittees = [preset.SLOTS_PER_EPOCH][preset.PTC_SIZE]ValidatorIndex; const ValidatorActivation = struct { validator_index: ValidatorIndex, @@ -202,7 +205,9 @@ pub const EpochTransitionCache = struct { slashing_penalties: []u64, balances: ?U64Array, next_shuffling_active_indices: []const ValidatorIndex, + next_shuffling: ?*EpochShuffling, next_epoch_total_active_balance_by_increment: u64, + next_epoch_payload_timeliness_committees: ?PtcCommittees, // these are borrowed from ReusedEpochTransitionCache is_active_prev_epoch: []const bool, is_active_curr_epoch: []const bool, @@ -528,8 +533,10 @@ pub const EpochTransitionCache = struct { .indices_eligible_for_activation = indices_eligible_for_activation, .indices_to_eject = indices_to_eject, .next_shuffling_active_indices = next_shuffling_active_indices, + .next_shuffling = null, // to be updated in processEffectiveBalanceUpdates .next_epoch_total_active_balance_by_increment = 0, + .next_epoch_payload_timeliness_committees = null, .is_active_prev_epoch = reused_cache.is_active_prev_epoch.items, .is_active_curr_epoch = reused_cache.is_active_current_epoch.items, .is_active_next_epoch = reused_cache.is_active_next_epoch.items, @@ -561,6 +568,49 @@ pub const EpochTransitionCache = struct { if (self.balances) |*balances| { balances.deinit(allocator); } + if (self.next_shuffling) |next_shuffling| { + next_shuffling.deinit(); + } + } + + pub fn getNextShuffling( + self: *EpochTransitionCache, + allocator: Allocator, + comptime fork: ForkSeq, + state: *BeaconState(fork), + ) !*EpochShuffling { + var any_state = @unionInit(AnyBeaconState, @tagName(fork), state.inner); + return self.getNextShufflingFromAnyState(allocator, &any_state); + } + + pub fn getNextShufflingFromAnyState( + self: *EpochTransitionCache, + allocator: Allocator, + state: *AnyBeaconState, + ) !*EpochShuffling { + if (self.next_shuffling) |next_shuffling| return next_shuffling; + + const active_indices = try allocator.alloc(ValidatorIndex, self.next_shuffling_active_indices.len); + std.mem.copyForwards(ValidatorIndex, active_indices, self.next_shuffling_active_indices); + + // computeEpochShuffling takes ownership of active_indices on both success and failure. + self.next_shuffling = try computeEpochShuffling( + allocator, + state, + active_indices, + self.current_epoch + preset.MIN_SEED_LOOKAHEAD + 1, + ); + return self.next_shuffling.?; + } + + pub fn takeNextShuffling( + self: *EpochTransitionCache, + allocator: Allocator, + state: *AnyBeaconState, + ) !*EpochShuffling { + const next_shuffling = try self.getNextShufflingFromAnyState(allocator, state); + self.next_shuffling = null; + return next_shuffling; } /// Ensure rewards/penalties arrays match the current validator count. @@ -595,7 +645,7 @@ test "EpochTransitionCache - finalProcessEpoch" { test_state.cached_state.state.* = .{ .fulu = fulu_state.inner }; const epoch_cache = test_state.cached_state.epoch_cache; - try epoch_cache.finalProcessEpoch(test_state.cached_state.state); + try epoch_cache.finalProcessEpoch(test_state.cached_state.state, null); } test "EpochTransitionCache.beforeProcessEpoch" { 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 756e8311f..8624519c4 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -25,6 +25,8 @@ const processParticipationRecordUpdates = @import("./process_participation_recor const processParticipationFlagUpdates = @import("./process_participation_flag_updates.zig").processParticipationFlagUpdates; const processSyncCommitteeUpdates = @import("./process_sync_committee_updates.zig").processSyncCommitteeUpdates; const processProposerLookahead = @import("./process_proposer_lookahead.zig").processProposerLookahead; +const processBuilderPendingPayments = @import("./process_builder_pending_payments.zig").processBuilderPendingPayments; +const processPtcWindow = @import("./process_ptc_window.zig").processPtcWindow; const Node = @import("persistent_merkle_tree").Node; pub fn processEpoch( @@ -70,6 +72,12 @@ pub fn processEpoch( try observeEpochTransitionStep(.{ .step = .process_pending_consolidations }, @as(u64, @intCast(time.since(io, timer).nanoseconds))); } + if (comptime fork.gte(.gloas)) { + timer = time.start(io); + try processBuilderPendingPayments(allocator, state, epoch_cache); + try observeEpochTransitionStep(.{ .step = .process_builder_pending_payments }, @as(u64, @intCast(time.since(io, timer).nanoseconds))); + } + // const numUpdate = processEffectiveBalanceUpdates(fork, state, cache); timer = time.start(io); _ = try processEffectiveBalanceUpdates(fork, allocator, epoch_cache, state, cache); @@ -103,6 +111,12 @@ pub fn processEpoch( try processProposerLookahead(fork, allocator, epoch_cache, state, cache); try observeEpochTransitionStep(.{ .step = .process_proposer_lookahead }, @as(u64, @intCast(time.since(io, timer).nanoseconds))); } + + if (comptime fork.gte(.gloas)) { + timer = time.start(io); + try processPtcWindow(allocator, epoch_cache, state, cache); + try observeEpochTransitionStep(.{ .step = .process_ptc_window }, @as(u64, @intCast(time.since(io, timer).nanoseconds))); + } } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; diff --git a/src/state_transition/epoch/process_pending_deposits.zig b/src/state_transition/epoch/process_pending_deposits.zig index 93f8288a5..f93415313 100644 --- a/src/state_transition/epoch/process_pending_deposits.zig +++ b/src/state_transition/epoch/process_pending_deposits.zig @@ -6,7 +6,9 @@ const BeaconConfig = @import("config").BeaconConfig; const BeaconState = @import("fork_types").BeaconState; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const EpochTransitionCache = @import("../cache/epoch_transition_cache.zig").EpochTransitionCache; -const getActivationExitChurnLimit = @import("../utils/validator.zig").getActivationExitChurnLimit; +const validator_utils = @import("../utils/validator.zig"); +const getActivationExitChurnLimit = validator_utils.getActivationExitChurnLimit; +const getGloasActivationChurnLimit = validator_utils.getGloasActivationChurnLimit; const preset = @import("preset").preset; const isValidatorKnown = @import("../utils/electra.zig").isValidatorKnown; const validateDepositSignature = @import("../block/process_deposit.zig").validateDepositSignature; @@ -31,7 +33,11 @@ pub fn processPendingDeposits( ) !void { const next_epoch = epoch_cache.epoch + 1; const deposit_balance_to_consume = try state.depositBalanceToConsume(); - const available_for_processing = deposit_balance_to_consume + getActivationExitChurnLimit(epoch_cache); + const churn_limit = if (comptime fork.gte(.gloas)) + getGloasActivationChurnLimit(epoch_cache) + else + getActivationExitChurnLimit(epoch_cache); + const available_for_processing = deposit_balance_to_consume + churn_limit; const finalized_slot = computeStartSlotAtEpoch(try state.finalizedEpoch()); var processed_amount: u64 = 0; diff --git a/src/state_transition/epoch/process_proposer_lookahead.zig b/src/state_transition/epoch/process_proposer_lookahead.zig index 7c4960754..be149dc2f 100644 --- a/src/state_transition/epoch/process_proposer_lookahead.zig +++ b/src/state_transition/epoch/process_proposer_lookahead.zig @@ -8,6 +8,7 @@ const BeaconState = @import("fork_types").BeaconState; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const EpochTransitionCache = @import("../cache/epoch_transition_cache.zig").EpochTransitionCache; const upgradeStateToFulu = @import("../slot/upgrade_state_to_fulu.zig").upgradeStateToFulu; +const FLAG_UNSLASHED = @import("../utils/attester_status.zig").FLAG_UNSLASHED; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; const seed_utils = @import("../utils/seed.zig"); @@ -23,7 +24,7 @@ pub fn processProposerLookahead( allocator: Allocator, epoch_cache: *EpochCache, state: *BeaconState(fork), - epoch_transition_cache: *const EpochTransitionCache, + epoch_transition_cache: *EpochTransitionCache, ) !void { const proposer_lookahead: *[ssz.fulu.ProposerLookahead.length]u64 = try state.proposerLookaheadSlice(allocator); defer allocator.free(@as([]u64, proposer_lookahead)); @@ -43,9 +44,21 @@ pub fn processProposerLookahead( const current_epoch = computeEpochAtSlot(try state.slot()); const new_epoch = current_epoch + preset.MIN_SEED_LOOKAHEAD + 1; - // Active indices for the new epoch come from the epoch transition cache - // (computed during beforeProcessEpoch for current_epoch + 2) - const active_indices = epoch_transition_cache.next_shuffling_active_indices; + // Share the N+2 shuffling with processPtcWindow and afterProcessEpoch. + const next_shuffling = try epoch_transition_cache.getNextShuffling(allocator, fork, state); + const shuffling_active_indices = next_shuffling.active_indices; + var unslashed_active_indices: std.ArrayList(ValidatorIndex) = .empty; + defer unslashed_active_indices.deinit(allocator); + + const active_indices = if (comptime fork.gte(.gloas)) blk: { + try unslashed_active_indices.ensureTotalCapacity(allocator, shuffling_active_indices.len); + for (shuffling_active_indices) |index| { + if ((epoch_transition_cache.flags[@intCast(index)] & FLAG_UNSLASHED) != 0) { + unslashed_active_indices.appendAssumeCapacity(index); + } + } + break :blk unslashed_active_indices.items; + } else shuffling_active_indices; const effective_balance_increments = epoch_cache.getEffectiveBalanceIncrements(); var seed: [32]u8 = undefined; @@ -65,6 +78,7 @@ pub fn processProposerLookahead( } const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; test "processProposerLookahead sanity" { const allocator = std.testing.allocator; @@ -91,3 +105,57 @@ test "processProposerLookahead sanity" { test_state.epoch_transition_cache, ); } + +fn tryGloasProposerLookahead( + allocator: Allocator, + epoch_cache: *EpochCache, + baseline_state: *BeaconState(.gloas), + baseline_cache: *const EpochTransitionCache, +) !void { + var state = try baseline_state.clone(.{ .transfer_cache = false }); + defer state.deinit(); + + var cache = baseline_cache.*; + cache.next_shuffling = null; + defer if (cache.next_shuffling) |shuffling| shuffling.deinit(); + + try processProposerLookahead(.gloas, allocator, epoch_cache, &state, &cache); +} + +test "Gloas proposer lookahead - OOM does not leak or double-free shuffling allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 2048) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + tryGloasProposerLookahead( + oom.allocator(), + test_state.cached_state.epoch_cache, + test_state.cached_state.state.castToFork(.gloas), + test_state.epoch_transition_cache, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} 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..dd94b8d91 --- /dev/null +++ b/src/state_transition/epoch/process_ptc_window.zig @@ -0,0 +1,112 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BeaconState = @import("fork_types").BeaconState; +const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; +const EpochTransitionCache = @import("../cache/epoch_transition_cache.zig").EpochTransitionCache; +const ct = @import("consensus_types"); +const preset = @import("preset").preset; +const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const computePayloadTimelinessCommitteesForEpoch = @import("../utils/seed.zig").computePayloadTimelinessCommitteesForEpoch; +const ValidatorIndex = ct.primitive.ValidatorIndex.Type; + +/// Update the `ptc_window` field in the beacon state by shifting out the oldest epoch's +/// PTC entries and appending newly computed entries for the next lookahead epoch. +/// Stashes the computed PTCs in the transition cache for finalProcessEpoch to shift +/// into the epoch cache without reading from state. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/beacon-chain.md#new-process_ptc_window +pub fn processPtcWindow( + allocator: Allocator, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), + epoch_transition_cache: *EpochTransitionCache, +) !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 next_shuffling = try epoch_transition_cache.getNextShuffling(allocator, .gloas, state); + + const next_epoch_payload_timeliness_committees = try computePayloadTimelinessCommitteesForEpoch( + .gloas, + allocator, + state, + next_epoch, + &next_shuffling.committees, + epoch_cache.effective_balance_increments.get().items, + ); + for (0..preset.SLOTS_PER_EPOCH) |slot_offset| { + try ptc_window.setValue( + ptc_window_len - preset.SLOTS_PER_EPOCH + slot_offset, + &next_epoch_payload_timeliness_committees[slot_offset], + ); + } + + try state.inner.set("ptc_window", ptc_window); + epoch_transition_cache.next_epoch_payload_timeliness_committees = next_epoch_payload_timeliness_committees; +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +fn tryGloasPtcWindow( + allocator: Allocator, + epoch_cache: *const EpochCache, + baseline_state: *BeaconState(.gloas), + baseline_cache: *const EpochTransitionCache, +) !void { + var state = try baseline_state.clone(.{ .transfer_cache = false }); + defer state.deinit(); + + var cache = baseline_cache.*; + cache.next_shuffling = null; + cache.next_epoch_payload_timeliness_committees = null; + defer if (cache.next_shuffling) |shuffling| shuffling.deinit(); + + try processPtcWindow(allocator, epoch_cache, &state, &cache); +} + +test "Gloas PTC window - OOM does not leak or double-free shuffling allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 512) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + tryGloasPtcWindow( + oom.allocator(), + test_state.cached_state.epoch_cache, + test_state.cached_state.state.castToFork(.gloas), + test_state.epoch_transition_cache, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} diff --git a/src/state_transition/metrics.zig b/src/state_transition/metrics.zig index 58a3c2c80..58143c12c 100644 --- a/src/state_transition/metrics.zig +++ b/src/state_transition/metrics.zig @@ -35,7 +35,9 @@ pub const EpochTransitionStepKind = enum { process_sync_committee_updates, process_pending_deposits, process_pending_consolidations, + process_builder_pending_payments, process_proposer_lookahead, + process_ptc_window, }; pub const ProposerRewardKind = enum { diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index e3f904b98..1c8ee6475 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; @@ -76,6 +77,8 @@ pub const processBlsToExecutionChange = @import("./block/process_bls_to_executio pub const processDepositRequest = @import("./block/process_deposit_request.zig").processDepositRequest; pub const processWithdrawalRequest = @import("./block/process_withdrawal_request.zig").processWithdrawalRequest; pub const processConsolidationRequest = @import("./block/process_consolidation_request.zig").processConsolidationRequest; +pub const processBuilderDepositRequest = @import("./block/process_builder_deposit_request.zig").processBuilderDepositRequest; +pub const processBuilderExitRequest = @import("./block/process_builder_exit_request.zig").processBuilderExitRequest; // utils pub const validator_status = @import("./utils/validator_status.zig"); @@ -90,6 +93,7 @@ pub const isExecutionEnabled = @import("./utils/execution.zig").isExecutionEnabl pub const isMergeTransitionComplete = @import("./utils/execution.zig").isMergeTransitionComplete; pub const getRandaoMix = @import("./utils/seed.zig").getRandaoMix; pub const getEffectiveBalanceIncrementsZeroInactive = @import("./utils/balance.zig").getEffectiveBalanceIncrementsZeroInactive; +pub const PendingDepositsLookup = @import("./utils/pending_deposits_lookup.zig").PendingDepositsLookup; pub const getStateSlotFromBytes = @import("ssz_bytes.zig").getStateSlotFromBytes; pub const getLastProcessedSlotFromStateBytes = @import("ssz_bytes.zig").getLastProcessedSlotFromStateBytes; pub const STATE_SLOTS_PREFIX_LEN = @import("ssz_bytes.zig").STATE_SLOTS_PREFIX_LEN; @@ -108,6 +112,12 @@ 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 processPayloadAttestation = @import("./block/process_payload_attestation.zig").processPayloadAttestation; +pub const processPtcWindow = @import("./epoch/process_ptc_window.zig").processPtcWindow; +pub const processParentExecutionPayload = @import("./block/process_parent_execution_payload.zig").processParentExecutionPayload; +pub const applyParentExecutionPayload = @import("./block/process_parent_execution_payload.zig").applyParentExecutionPayload; const load_state = @import("load_state.zig"); pub const loadState = load_state.loadState; 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/signature_sets/execution_payload_envelope.zig b/src/state_transition/signature_sets/execution_payload_envelope.zig new file mode 100644 index 000000000..f8586581d --- /dev/null +++ b/src/state_transition/signature_sets/execution_payload_envelope.zig @@ -0,0 +1,49 @@ +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.payload.slot_number), 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 + 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: { + 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); +} 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..ba94edf19 --- /dev/null +++ b/src/state_transition/signature_sets/indexed_payload_attestation.zig @@ -0,0 +1,73 @@ +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, + io: std.Io, + 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); + epoch_cache.pubkey_cache.getPubkeys(io, attesting_indices, pubkeys) catch |err| switch (err) { + error.InvalidIndex => return error.PubkeyNotFound, + else => return err, + }; + + var signing_root: Root = undefined; + try getPayloadAttestationDataSigningRoot(config, &indexed_payload_attestation.data, &signing_root); + + return createAggregateSignatureSetFromComponents(pubkeys, signing_root, indexed_payload_attestation.signature); +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; + +// The pubkey lookup is fallible, so it can fail after `pubkeys` is allocated. The OOM suite only +// injects allocator failures, so it never reaches this branch; std.testing.allocator fails the test +// if the errdefer does not release `pubkeys`. +test "Gloas indexed payload attestation - unknown validator index does not leak pubkeys" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + + var indexed = types.gloas.IndexedPayloadAttestation.default_value; + defer indexed.attesting_indices.deinit(allocator); + // First index is valid, second is past the end of the pubkey cache. + try indexed.attesting_indices.append(allocator, 0); + try indexed.attesting_indices.append(allocator, 1_000_000); + + try std.testing.expectError(error.PubkeyNotFound, getIndexedPayloadAttestationSignatureSet( + allocator, + std.testing.io, + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + &indexed, + )); +} diff --git a/src/state_transition/slot/process_slot.zig b/src/state_transition/slot/process_slot.zig index 17652306c..f1c52deb5 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,16 @@ 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); + try s.set("execution_payload_availability", executionPayloadAvailability); + }, + else => {}, + } + } } diff --git a/src/state_transition/slot/upgrade_state_to_altair.zig b/src/state_transition/slot/upgrade_state_to_altair.zig index 89bea9d9d..d87e1ef5c 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); var attesting_indices = try attestation.aggregation_bits.intersectValues(ValidatorIndex, allocator, committee_indices); defer attesting_indices.deinit(allocator); diff --git a/src/state_transition/slot/upgrade_state_to_electra.zig b/src/state_transition/slot/upgrade_state_to_electra.zig index e62cd4e3e..283d490ab 100644 --- a/src/state_transition/slot/upgrade_state_to_electra.zig +++ b/src/state_transition/slot/upgrade_state_to_electra.zig @@ -54,7 +54,7 @@ pub fn upgradeStateToElectra( try state.setEarliestExitEpoch(earliest_exit_epoch + 1); try state.setEarliestConsolidationEpoch(computeActivationExitEpoch(current_epoch_pre)); try state.setExitBalanceToConsume(getActivationExitChurnLimit(epoch_cache)); - try state.setConsolidationBalanceToConsume(getConsolidationChurnLimit(epoch_cache)); + try state.setConsolidationBalanceToConsume(getConsolidationChurnLimit(.electra, epoch_cache)); const sort_fn = struct { pub fn sort(validator_arr: []const ct.phase0.Validator.Type, a: ValidatorIndex, b: ValidatorIndex) bool { 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..a9a2dcfe3 --- /dev/null +++ b/src/state_transition/slot/upgrade_state_to_gloas.zig @@ -0,0 +1,199 @@ +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 c = @import("constants"); + +const ExecutionPayloadBid = ct.gloas.ExecutionPayloadBid; +const PendingDeposit = ct.electra.PendingDeposit.Type; +const BitVector = @import("ssz").BitVector; +const ExecPayloadAvailability = BitVector(preset.SLOTS_PER_HISTORICAL_ROOT); +const isValidatorKnown = @import("../utils/electra.zig").isValidatorKnown; +const validateDepositSignature = @import("../block/process_deposit.zig").validateDepositSignature; +const gloas_utils = @import("../utils/gloas.zig"); +const addBuilderToRegistry = gloas_utils.addBuilderToRegistry; +const findBuilderIndexByPubkey = gloas_utils.findBuilderIndexByPubkey; +const isBuilderWithdrawalCredential = gloas_utils.isBuilderWithdrawalCredential; +const initializePtcWindow = gloas_utils.initializePtcWindow; +const PendingDepositsLookup = @import("../utils/pending_deposits_lookup.zig").PendingDepositsLookup; + +/// Upgrade a state from Fulu to Gloas. +pub fn upgradeStateToGloas( + allocator: Allocator, + io: std.Io, + 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 latest_execution_payload_header = ct.fulu.ExecutionPayloadHeader.default_value; + try fulu_state.latestExecutionPayloadHeader(allocator, &latest_execution_payload_header); + defer ct.fulu.ExecutionPayloadHeader.deinit(allocator, &latest_execution_payload_header); + + 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; + bid.gas_limit = latest_execution_payload_header.gas_limit; + try ct.gloas.ExecutionRequests.hashTreeRoot(allocator, &ct.gloas.ExecutionRequests.default_value, &bid.execution_requests_root); + 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); + + const ptc_window = try initializePtcWindow(.gloas, allocator, epoch_cache, &state); + try state.inner.setValue("ptc_window", &ptc_window); + + try onboardBuildersFromPendingDeposits(allocator, io, config, epoch_cache, &state); + + fulu_state.deinit(); + return state; +} + +/// Applies any pending deposits for builders to onboard builders during the fork transition +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits +fn onboardBuildersFromPendingDeposits( + allocator: Allocator, + io: std.Io, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + state: *BeaconState(.gloas), +) !void { + var remaining_pending_deposits: std.ArrayList(PendingDeposit) = .empty; + defer remaining_pending_deposits.deinit(allocator); + + var pending_deposits_lookup = PendingDepositsLookup.init(allocator); + defer pending_deposits_lookup.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(); + + const validator_index = epoch_cache.pubkey_cache.get(io, deposit.pubkey); + if (try isValidatorKnown(.gloas, state, validator_index)) { + try remaining_pending_deposits.append(allocator, deposit); + try pending_deposits_lookup.add(&deposit); + continue; + } + + const builder_index = try findBuilderIndexByPubkey(allocator, state, &deposit.pubkey); + if (builder_index) |idx| { + var builders = try state.inner.get("builders"); + var builder: ct.gloas.Builder.Type = undefined; + try builders.getValue(allocator, idx, &builder); + builder.balance += deposit.amount; + try builders.setValue(idx, &builder); + continue; + } else { + if (!isBuilderWithdrawalCredential(&deposit.withdrawal_credentials)) { + try remaining_pending_deposits.append(allocator, deposit); + try pending_deposits_lookup.add(&deposit); + continue; + } + + if (try pending_deposits_lookup.hasPendingValidator(config, &deposit.pubkey)) { + try remaining_pending_deposits.append(allocator, deposit); + try pending_deposits_lookup.add(&deposit); + continue; + } + } + + validateDepositSignature(config, &deposit.pubkey, &deposit.withdrawal_credentials, deposit.amount, deposit.signature) catch continue; + + var execution_address: ct.primitive.ExecutionAddress.Type = undefined; + @memcpy(&execution_address, deposit.withdrawal_credentials[12..32]); + try addBuilderToRegistry( + allocator, + state, + &deposit.pubkey, + c.PAYLOAD_BUILDER_VERSION, + &execution_address, + deposit.amount, + deposit.slot, + ); + } + + 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); +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +fn tryGloasUpgrade( + allocator: Allocator, + config: *const BeaconConfig, + epoch_cache: *const EpochCache, + baseline: *BeaconState(.fulu), +) !void { + var fulu_state = try baseline.clone(.{ .transfer_cache = false }); + var owns_fulu_state = true; + defer if (owns_fulu_state) fulu_state.deinit(); + + var gloas_state = try upgradeStateToGloas(allocator, std.testing.io, config, epoch_cache, &fulu_state); + owns_fulu_state = false; + defer gloas_state.deinit(); +} + +test "Gloas fork upgrade - OOM does not double-free transient allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initElectraForGloas(allocator, &pool, 64); + defer test_state.deinit(); + try test_state.upgradeToFuluForGloas(allocator); + const baseline = test_state.cached_state.state.castToFork(.fulu); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 256) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + tryGloasUpgrade( + oom.allocator(), + test_state.cached_state.config, + test_state.cached_state.epoch_cache, + baseline, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index 7cc03b341..90367da34 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -28,6 +28,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 deinitReusedEpochTransitionCache = @import("cache/epoch_transition_cache.zig").deinitReusedEpochTransitionCache; @@ -132,8 +133,13 @@ pub fn processSlots( const upgraded = try upgradeStateToFulu(allocator, config, epoch_cache, electra_state); state.* = .{ .fulu = upgraded.inner }; } + if (state_epoch == config.chain.GLOAS_FORK_EPOCH) { + const fulu_state = try state.tryCastToFork(.fulu); + const upgraded = try upgradeStateToGloas(allocator, io, config, epoch_cache, fulu_state); + state.* = .{ .gloas = upgraded.inner }; + } - try epoch_cache.finalProcessEpoch(state); + try epoch_cache.finalProcessEpoch(state, &epoch_transition_cache); metrics.state_transition.epoch_transition.observe(time.durationSeconds(time.since(io, epoch_transition_timer))); } else { try state.setSlot(next_slot); diff --git a/src/state_transition/test_utils/generate_block.zig b/src/state_transition/test_utils/generate_block.zig index 404a3c641..f286b4d69 100644 --- a/src/state_transition/test_utils/generate_block.zig +++ b/src/state_transition/test_utils/generate_block.zig @@ -9,7 +9,9 @@ const state_transition = @import("../root.zig"); const CachedBeaconState = state_transition.CachedBeaconState; const computeStartSlotAtEpoch = state_transition.computeStartSlotAtEpoch; const getBlockRootAtSlot = state_transition.getBlockRootAtSlot; +const getRandaoMix = state_transition.getRandaoMix; const BeaconState = @import("fork_types").BeaconState; +const c = @import("constants"); /// Generate a valid electra block for the given pre-state. pub fn generateElectraBlock(allocator: Allocator, cached_state: *CachedBeaconState, out: *types.electra.SignedBeaconBlock.Type) !void { @@ -97,3 +99,27 @@ pub fn generateElectraBlock(allocator: Allocator, cached_state: *CachedBeaconSta .signature = types.primitive.BLSSignature.default_value, }; } + +/// Generate a valid empty Gloas block with a self-build execution payload bid. +pub fn generateGloasBlock(cached_state: *CachedBeaconState, out: *types.gloas.BeaconBlock.Type) !void { + const state = try cached_state.state.tryCastToFork(.gloas); + const slot = try state.slot(); + + var latest_header = try state.latestBlockHeader(); + const parent_root = try latest_header.hashTreeRoot(); + const parent_block_root = try getBlockRootAtSlot(.gloas, state, slot - 1); + const parent_block_hash = try state.inner.getFieldRoot("latest_block_hash"); + const prev_randao = try getRandaoMix(.gloas, state, cached_state.epoch_cache.epoch); + + out.* = types.gloas.BeaconBlock.default_value; + out.slot = slot; + out.proposer_index = try cached_state.getBeaconProposer(slot); + out.parent_root = parent_root.*; + out.body.signed_execution_payload_bid.message.parent_block_hash = parent_block_hash.*; + out.body.signed_execution_payload_bid.message.parent_block_root = parent_block_root.*; + out.body.signed_execution_payload_bid.message.prev_randao = prev_randao.*; + out.body.signed_execution_payload_bid.message.builder_index = c.BUILDER_INDEX_SELF_BUILD; + out.body.signed_execution_payload_bid.message.slot = slot; + out.body.signed_execution_payload_bid.message.value = 0; + out.body.signed_execution_payload_bid.signature = c.G2_POINT_AT_INFINITY; +} diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index 5b29085c9..3d658ab8b 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -1,5 +1,6 @@ const std = @import("std"); const bls = @import("bls"); +const c = @import("constants"); const Allocator = std.mem.Allocator; const ForkSeq = @import("config").ForkSeq; const mainnet_chain_config = @import("config").mainnet.chain_config; @@ -182,6 +183,60 @@ pub const TestCachedBeaconState = struct { return initFromState(allocator, pool, state, ForkSeq.electra, fork_epoch); } + pub fn initElectraForGloas(allocator: Allocator, pool: *Node.Pool, validator_count: usize) !TestCachedBeaconState { + const fork_epoch = if (active_chain_config.ELECTRA_FORK_EPOCH == c.FAR_FUTURE_EPOCH) + 0 + else + active_chain_config.ELECTRA_FORK_EPOCH; + const test_chain_config = active_chain_config.merge(.{ .ELECTRA_FORK_EPOCH = fork_epoch }); + var state = try generateElectraState(allocator, pool, test_chain_config, validator_count); + errdefer { + state.deinit(); + allocator.destroy(state); + } + + return initFromState(allocator, pool, state, ForkSeq.electra, fork_epoch); + } + + pub fn initGloas(allocator: Allocator, pool: *Node.Pool, validator_count: usize) !TestCachedBeaconState { + var test_state = try initElectraForGloas(allocator, pool, validator_count); + errdefer test_state.deinit(); + try test_state.upgradeToGloas(allocator); + return test_state; + } + + pub fn upgradeToFuluForGloas(self: *TestCachedBeaconState, allocator: Allocator) !void { + const fork_epoch = self.cached_state.epoch_cache.epoch; + self.config.* = BeaconConfig.init( + getConfig(active_chain_config, .gloas, fork_epoch), + (try self.cached_state.state.genesisValidatorsRoot()).*, + ); + + const fulu_state = try state_transition.upgradeStateToFulu( + allocator, + self.cached_state.config, + self.cached_state.epoch_cache, + try self.cached_state.state.tryCastToFork(.electra), + ); + self.cached_state.state.* = .{ .fulu = fulu_state.inner }; + } + + pub fn upgradeToGloas(self: *TestCachedBeaconState, allocator: Allocator) !void { + if (self.cached_state.state.forkSeq() == .electra) { + try self.upgradeToFuluForGloas(allocator); + } + + const gloas_state = try state_transition.upgradeStateToGloas( + allocator, + std.testing.io, + self.cached_state.config, + self.cached_state.epoch_cache, + try self.cached_state.state.tryCastToFork(.fulu), + ); + self.cached_state.state.* = .{ .gloas = gloas_state.inner }; + try self.cached_state.state.commit(); + } + pub fn initFromState(allocator: Allocator, pool: *Node.Pool, state: *AnyBeaconState, fork: ForkSeq, fork_epoch: Epoch) !TestCachedBeaconState { const pubkey_cache = try allocator.create(PubkeyCache); errdefer allocator.destroy(pubkey_cache); diff --git a/src/state_transition/test_utils/root.zig b/src/state_transition/test_utils/root.zig index a4ff2d5f5..ee7a213f5 100644 --- a/src/state_transition/test_utils/root.zig +++ b/src/state_transition/test_utils/root.zig @@ -4,6 +4,7 @@ const testing = std.testing; /// Utils that could be used for different kinds of tests like int, perf pub const TestCachedBeaconState = @import("./generate_state.zig").TestCachedBeaconState; pub const generateElectraBlock = @import("./generate_block.zig").generateElectraBlock; +pub const generateGloasBlock = @import("./generate_block.zig").generateGloasBlock; pub const interopSign = @import("./interop_pubkeys.zig").interopSign; test { diff --git a/src/state_transition/utils/committee_indices.zig b/src/state_transition/utils/committee_indices.zig index 98c99ef2d..d51a869be 100644 --- a/src/state_transition/utils/committee_indices.zig +++ b/src/state_transition/utils/committee_indices.zig @@ -29,6 +29,7 @@ const ComputeShuffledIndex = struct { } var arena = std.heap.ArenaAllocator.init(parent_allocator); + errdefer arena.deinit(); const allocator = arena.allocator(); const pivot_by_index = try allocator.alloc(?u64, @intCast(rounds)); diff --git a/src/state_transition/utils/epoch.zig b/src/state_transition/utils/epoch.zig index 0c02d93ef..0b5429e80 100644 --- a/src/state_transition/utils/epoch.zig +++ b/src/state_transition/utils/epoch.zig @@ -9,6 +9,7 @@ const ForkSeq = @import("config").ForkSeq; const BeaconState = @import("fork_types").BeaconState; const EpochCache = @import("../cache/epoch_cache.zig").EpochCache; const getActivationExitChurnLimit = @import("../utils/validator.zig").getActivationExitChurnLimit; +const getGloasExitChurnLimit = @import("../utils/validator.zig").getGloasExitChurnLimit; const getConsolidationChurnLimit = @import("../utils/validator.zig").getConsolidationChurnLimit; pub fn computeEpochAtSlot(slot: Slot) Epoch { @@ -47,7 +48,10 @@ pub fn computeExitEpochAndUpdateChurn( ) !u64 { const state_earliest_exit_epoch = try state.earliestExitEpoch(); var earliest_exit_epoch = @max(state_earliest_exit_epoch, computeActivationExitEpoch(epoch_cache.epoch)); - const per_epoch_churn = getActivationExitChurnLimit(epoch_cache); + const per_epoch_churn = if (comptime fork.gte(.gloas)) + getGloasExitChurnLimit(epoch_cache) + else + getActivationExitChurnLimit(epoch_cache); const state_exit_balance_to_consume = try state.exitBalanceToConsume(); // New epoch for exits. @@ -79,7 +83,7 @@ pub fn computeConsolidationEpochAndUpdateChurn( ) !u64 { const state_earliest_consolidation_epoch = try state.earliestConsolidationEpoch(); var earliest_consolidation_epoch = @max(state_earliest_consolidation_epoch, computeActivationExitEpoch(epoch_cache.epoch)); - const per_epoch_consolidation_churn = getConsolidationChurnLimit(epoch_cache); + const per_epoch_consolidation_churn = getConsolidationChurnLimit(fork, epoch_cache); const state_consolidation_balance_to_consume = try state.consolidationBalanceToConsume(); diff --git a/src/state_transition/utils/epoch_shuffling.zig b/src/state_transition/utils/epoch_shuffling.zig index c22a4bd50..9e07c5392 100644 --- a/src/state_transition/utils/epoch_shuffling.zig +++ b/src/state_transition/utils/epoch_shuffling.zig @@ -42,6 +42,7 @@ pub const EpochShuffling = struct { std.mem.copyForwards(ValidatorIndex, shuffling, active_indices); try unshuffleList(shuffling, seed[0..], preset.SHUFFLE_ROUND_COUNT); const committees = try buildCommitteesFromShuffling(allocator, shuffling); + errdefer freeCommittees(allocator, committees); const epoch_shuffling_ptr = try allocator.create(EpochShuffling); errdefer allocator.destroy(epoch_shuffling_ptr); @@ -74,6 +75,11 @@ pub const EpochShuffling = struct { const committee_count = committees_per_slot * preset.SLOTS_PER_EPOCH; var epoch_committees: [preset.SLOTS_PER_EPOCH]SlotCommittees = undefined; + var initialized_slots: usize = 0; + errdefer for (epoch_committees[0..initialized_slots]) |slot_committees| { + allocator.free(slot_committees); + }; + for (0..preset.SLOTS_PER_EPOCH) |slot| { const slot_committees = try allocator.alloc(Committee, committees_per_slot); for (0..committees_per_slot) |committee_index| { @@ -83,10 +89,17 @@ pub const EpochShuffling = struct { slot_committees[committee_index] = shuffling[start_offset..end_offset]; } epoch_committees[slot] = slot_committees; + initialized_slots += 1; } return epoch_committees; } + + fn freeCommittees(allocator: Allocator, committees: EpochCommittees) void { + for (committees) |slot_committees| { + allocator.free(slot_committees); + } + } }; test EpochShuffling { diff --git a/src/state_transition/utils/gloas.zig b/src/state_transition/utils/gloas.zig new file mode 100644 index 000000000..975eb2395 --- /dev/null +++ b/src/state_transition/utils/gloas.zig @@ -0,0 +1,417 @@ +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 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 computeDomain = @import("./domain.zig").computeDomain; +const computeSigningRoot = @import("./signing_root.zig").computeSigningRoot; +const bls = @import("bls"); +const verify = @import("./bls.zig").verify; + +const BLSPubkey = ct.primitive.BLSPubkey.Type; +const ValidatorIndex = ct.primitive.ValidatorIndex.Type; +const ExecutionAddress = ct.primitive.ExecutionAddress.Type; +const BLSSignature = ct.primitive.BLSSignature.Type; + +pub fn isBuilderWithdrawalCredential(withdrawal_credentials: *const [32]u8) bool { + return withdrawal_credentials[0] == c.BUILDER_WITHDRAWAL_PREFIX; +} + +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; +} + +fn hasBuilderIndexFlag(index: u64) bool { + return (index & c.BUILDER_INDEX_FLAG) != 0; +} + +/// Check if a validator index represents a builder (has the builder flag set). +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-is_builder_index +pub fn isBuilderIndex(validator_index: u64) bool { + return hasBuilderIndexFlag(validator_index); +} + +/// Convert a builder index to a flagged validator index for use in Withdrawal containers. +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_builder_index_to_validator_index +pub fn convertBuilderIndexToValidatorIndex(builder_index: u64) u64 { + return if (hasBuilderIndexFlag(builder_index)) builder_index else builder_index | c.BUILDER_INDEX_FLAG; +} + +/// Convert a flagged validator index back to a builder index. +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_validator_index_to_builder_index +pub fn convertValidatorIndexToBuilderIndex(validator_index: u64) u64 { + return if (hasBuilderIndexFlag(validator_index)) validator_index & ~c.BUILDER_INDEX_FLAG else validator_index; +} + +/// Check if a builder is active (deposited and not yet withdrawable). +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#isactivebuilder +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; +} + +/// Compute the gas limit that satisfies the EIP-1559 adjustment rule from `parent_gas_limit`, +/// clamping `target_gas_limit` into the allowed window of `±max(parent_gas_limit / 1024, 1) - 1`. +/// +/// From https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md +pub fn getExpectedGasLimit(parent_gas_limit: u64, target_gas_limit: u64) u64 { + const max_gas_limit_difference = @max(parent_gas_limit / 1024, 1) - 1; + + if (target_gas_limit > parent_gas_limit) { + return parent_gas_limit + @min(target_gas_limit - parent_gas_limit, max_gas_limit_difference); + } + + return parent_gas_limit - @min(parent_gas_limit - target_gas_limit, max_gas_limit_difference); +} + +/// Check if `gas_limit` is compatible with `target_gas_limit` under the EIP-1559 transition rule +/// from `parent_gas_limit`. The bid must hit `target_gas_limit` when the target is within one +/// adjustment step of the parent, otherwise it must hit the clamped boundary. +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/builder.md#new-is_gas_limit_target_compatible +pub fn isGasLimitTargetCompatible(parent_gas_limit: u64, gas_limit: u64, target_gas_limit: u64) bool { + return gas_limit == getExpectedGasLimit(parent_gas_limit, target_gas_limit); +} + +/// Get the total pending balance to withdraw for a builder (from withdrawals + payments). +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-get_pending_balance_to_withdraw_for_builder +pub fn getPendingBalanceToWithdrawForBuilder(allocator: Allocator, state: *BeaconState(.gloas), builder_index: u64) !u64 { + var pending_balance: u64 = 0; + + var withdrawals = try state.inner.getReadonly("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(); + if (w.builder_index == builder_index) { + pending_balance += w.amount; + } + } + + var payments = try state.inner.getReadonly("builder_pending_payments"); + 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; + } + } + + return pending_balance; +} + +/// Check if a builder has sufficient balance to cover a bid amount. +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-can_builder_cover_bid +pub fn canBuilderCoverBid(allocator: Allocator, state: *BeaconState(.gloas), builder_index: u64, bid_amount: u64) !bool { + var builders = try state.inner.getReadonly("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; +} + +/// Initiate a builder exit by setting their withdrawable epoch. +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-initiate_builder_exit +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); + + if (builder.withdrawable_epoch != c.FAR_FUTURE_EPOCH) return; + + const current_epoch = computeEpochAtSlot(try state.slot()); + builder.withdrawable_epoch = current_epoch + config.chain.MIN_BUILDER_WITHDRAWABILITY_DELAY; + try builders.setValue(builder_index, &builder); +} + +/// Find the index of a builder by their public key. +/// Returns null if not found. +/// +/// May consider builder pubkey cache if performance becomes an issue. +pub fn findBuilderIndexByPubkey(allocator: Allocator, state: *BeaconState(.gloas), pubkey: *const BLSPubkey) !?usize { + var builders = try state.inner.get("builders"); + const len = try builders.length(); + for (0..len) |i| { + 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; +} + +/// Add a new builder to the builders registry. Reuses slots from exited and fully withdrawn +/// builders when available, otherwise appends. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/beacon-chain.md#new-add_builder_to_registry +pub fn addBuilderToRegistry( + allocator: Allocator, + state: *BeaconState(.gloas), + pubkey: *const BLSPubkey, + version: u8, + execution_address: *const ExecutionAddress, + 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 builder_index: ?usize = null; + for (0..len) |i| { + var builder: ct.gloas.Builder.Type = undefined; + try builders.getValue(allocator, i, &builder); + if (builder.withdrawable_epoch <= current_epoch and builder.balance == 0) { + builder_index = i; + break; + } + } + + const new_builder = ct.gloas.Builder.Type{ + .pubkey = pubkey.*, + .version = version, + .execution_address = execution_address.*, + .balance = amount, + .deposit_epoch = computeEpochAtSlot(slot), + .withdrawable_epoch = c.FAR_FUTURE_EPOCH, + }; + + if (builder_index) |idx| { + try builders.setValue(idx, &new_builder); + } else { + try builders.pushValue(&new_builder); + try state.inner.set("builders", builders); + } +} + +/// Verify the proof of possession on a builder deposit request. +/// +/// The dedicated `DOMAIN_BUILDER_DEPOSIT` (vs the validator `DOMAIN_DEPOSIT`) prevents replay +/// of validator deposit signatures against the builder deposit contract and vice versa. +/// +/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.11/specs/gloas/beacon-chain.md#new-is_valid_builder_deposit_signature +pub fn isValidBuilderDepositSignature( + config: *const BeaconConfig, + pubkey: *const BLSPubkey, + withdrawal_credentials: *const [32]u8, + amount: u64, + deposit_signature: BLSSignature, +) bool { + const deposit_message = ct.phase0.DepositMessage.Type{ + .pubkey = pubkey.*, + .withdrawal_credentials = withdrawal_credentials.*, + .amount = amount, + }; + + var domain: ct.primitive.Domain.Type = undefined; + computeDomain(c.DOMAIN_BUILDER_DEPOSIT, config.chain.GENESIS_FORK_VERSION, c.ZERO_HASH, &domain) catch return false; + + var signing_root: [32]u8 = undefined; + computeSigningRoot(ct.phase0.DepositMessage, &deposit_message, &domain, &signing_root) catch return false; + + const public_key = bls.PublicKey.uncompress(pubkey) catch return false; + public_key.validate() catch return false; + const signature = bls.Signature.uncompress(&deposit_signature) catch return false; + signature.validate(true) catch return false; + verify(&signing_root, &public_key, &signature, .{}) catch return false; + return true; +} + +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.getReadonly("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 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) = .empty; + defer active_indices.deinit(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(allocator, @intCast(i)); + } + } + var any_state = AnyBeaconState{ .gloas = state.inner }; + break :blk try computeEpochShuffling(allocator, &any_state, try active_indices.toOwnedSlice(allocator), 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, + epoch_cache: *const EpochCache, + state: *BeaconState(fork), +) ![ct.gloas.PtcWindow.length][ct.gloas.PtcWindow.Element.length]ValidatorIndex { + const ptc_size = ct.gloas.PtcWindow.Element.length; + const ptc_window_len = ct.gloas.PtcWindow.length; + + const empty_previous_epoch = [_][ptc_size]ValidatorIndex{ + [_]ValidatorIndex{0} ** ptc_size, + } ** preset.SLOTS_PER_EPOCH; + + const current_epoch = computeEpochAtSlot(try state.slot()); + var ptcs: [(1 + preset.MIN_SEED_LOOKAHEAD) * preset.SLOTS_PER_EPOCH][ptc_size]ValidatorIndex = undefined; + for (0..1 + preset.MIN_SEED_LOOKAHEAD) |epoch_offset| { + const epoch = current_epoch + epoch_offset; + var owned_shuffling: ?*EpochShuffling = null; + defer if (owned_shuffling) |shuffling| shuffling.deinit(); + + const shuffling = epoch_cache.getShufflingAtEpochOrNull(epoch) orelse blk: { + var validators = try state.validators(); + const validator_count = try validators.length(); + var active_indices: std.ArrayList(ValidatorIndex) = .empty; + defer active_indices.deinit(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(allocator, @intCast(i)); + } + } + + var any_state = @unionInit(AnyBeaconState, @tagName(fork), state.inner); + owned_shuffling = try computeEpochShuffling( + allocator, + &any_state, + try active_indices.toOwnedSlice(allocator), + epoch, + ); + break :blk owned_shuffling.?; + }; + const epoch_committees = try computePayloadTimelinessCommitteesForEpoch( + fork, + allocator, + state, + epoch, + &shuffling.committees, + epoch_cache.effective_balance_increments.get().items, + ); + for (0..preset.SLOTS_PER_EPOCH) |slot_index| { + ptcs[epoch_offset * preset.SLOTS_PER_EPOCH + slot_index] = epoch_committees[slot_index]; + } + } + + // return empty_previous_epoch + ptcs + var ptc_window: [ptc_window_len][ptc_size]ValidatorIndex = undefined; + @memcpy(ptc_window[0..preset.SLOTS_PER_EPOCH], &empty_previous_epoch); + @memcpy(ptc_window[preset.SLOTS_PER_EPOCH..], &ptcs); + + return ptc_window; +} + +const Node = @import("persistent_merkle_tree").Node; +const TestCachedBeaconState = @import("../test_utils/root.zig").TestCachedBeaconState; +const DoubleFreeDetectAllocator = @import("testing_allocators").DoubleFreeDetectAllocator; + +test "Gloas PTC window fallback - OOM does not leak or double-free shuffling allocations" { + const allocator = std.testing.allocator; + var pool = try Node.Pool.init(.{ .page_allocator = allocator, .allocator = allocator, .pool_size = 500_000 }); + defer pool.deinit(); + + var test_state = try TestCachedBeaconState.initGloas(allocator, &pool, 256); + defer test_state.deinit(); + const state = test_state.cached_state.state.castToFork(.gloas); + try state.setSlot(try state.slot() + 2 * preset.SLOTS_PER_EPOCH); + + var saw_oom = false; + var saw_success = false; + var fail_at: usize = 0; + while (fail_at < 512) : (fail_at += 1) { + var oom = DoubleFreeDetectAllocator.init(std.testing.allocator, fail_at); + defer oom.deinit(); + + _ = initializePtcWindow( + .gloas, + oom.allocator(), + test_state.cached_state.epoch_cache, + state, + ) catch |err| switch (err) { + error.OutOfMemory => { + saw_oom = true; + try std.testing.expect(!oom.double_free); + continue; + }, + else => return err, + }; + + try std.testing.expect(!oom.double_free); + saw_success = true; + break; + } + + try std.testing.expect(saw_oom); + try std.testing.expect(saw_success); +} diff --git a/src/state_transition/utils/pending_deposits_lookup.zig b/src/state_transition/utils/pending_deposits_lookup.zig new file mode 100644 index 000000000..721924ef4 --- /dev/null +++ b/src/state_transition/utils/pending_deposits_lookup.zig @@ -0,0 +1,128 @@ +//! Mutable lookup for the pending-deposit sequence used by builder-routing logic. +//! +//! This is to implement the spec's `is_pending_validator(pending_deposits, pubkey)` lazily: +//! deposits are grouped by pubkey without verifying signatures, and BLS verification is +//! deferred until a builder deposit needs to know whether the same pubkey already has a +//! valid pending validator deposit. +//! +//! Call `add()` whenever a deposit is appended to the represented sequence. A cached `true` +//! result short-circuits all subsequent checks for that pubkey; a cached `false` records +//! how many deposits were already verified, so appending a new deposit only verifies the +//! newly-appended tail rather than re-running BLS on previously-invalid entries. + +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 validateDepositSignature = @import("../block/process_deposit.zig").validateDepositSignature; + +const BLSPubkey = ct.primitive.BLSPubkey.Type; +const PendingDeposit = ct.electra.PendingDeposit.Type; + +pub const PendingDepositsLookup = struct { + allocator: Allocator, + deposits_by_pubkey: std.AutoHashMapUnmanaged(BLSPubkey, PendingDeposits), + validation_cache: std.AutoHashMapUnmanaged(BLSPubkey, PendingDepositsValidation), + + const PendingDeposits = struct { + deposits: std.ArrayList(PendingDeposit) = .empty, + + fn deinit(self: *PendingDeposits, allocator: Allocator) void { + self.deposits.deinit(allocator); + } + }; + + const PendingDepositsValidation = struct { + has_valid_signature: bool, + validated_count: usize, + }; + + /// Build an empty lookup for a sequence that will be populated incrementally. + pub fn init(allocator: Allocator) PendingDepositsLookup { + return .{ + .allocator = allocator, + .deposits_by_pubkey = .empty, + .validation_cache = .empty, + }; + } + + /// Build a pubkey -> pending-deposits lookup from `state.pendingDeposits`. + /// No BLS work is done here; signature verification happens lazily in `hasPendingValidator`. + pub fn initFromState(comptime fork: ForkSeq, allocator: Allocator, state: *BeaconState(fork)) !PendingDepositsLookup { + var lookup = PendingDepositsLookup.init(allocator); + errdefer lookup.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 pending_deposit = try pending_it.nextValue(); + try lookup.add(&pending_deposit); + } + + return lookup; + } + + pub fn deinit(self: *PendingDepositsLookup) void { + var value_iterator = self.deposits_by_pubkey.valueIterator(); + while (value_iterator.next()) |entry| { + entry.deinit(self.allocator); + } + self.deposits_by_pubkey.deinit(self.allocator); + self.validation_cache.deinit(self.allocator); + } + + /// Append a pending deposit to the represented sequence. + pub fn add(self: *PendingDepositsLookup, pending_deposit: *const PendingDeposit) !void { + const result = try self.deposits_by_pubkey.getOrPut(self.allocator, pending_deposit.pubkey); + if (!result.found_existing) { + result.value_ptr.* = .{}; + } + try result.value_ptr.deposits.append(self.allocator, pending_deposit.*); + } + + /// Returns true if any pending deposit for `pubkey` has a valid BLS deposit signature. + /// Memoizes the result in `validation_cache` so repeated checks for the same pubkey + /// within a block only verify deposits that have not already been checked. + pub fn hasPendingValidator( + self: *PendingDepositsLookup, + config: *const BeaconConfig, + pubkey: *const BLSPubkey, + ) !bool { + const validation = self.validation_cache.get(pubkey.*); + if (validation) |v| { + if (v.has_valid_signature) return true; + } + + const entry = self.deposits_by_pubkey.getPtr(pubkey.*) orelse return false; + + const start_index = if (validation) |v| v.validated_count else 0; + if (start_index == entry.deposits.items.len) return false; + + var i = start_index; + while (i < entry.deposits.items.len) : (i += 1) { + const deposit = &entry.deposits.items[i]; + validateDepositSignature( + config, + &deposit.pubkey, + &deposit.withdrawal_credentials, + deposit.amount, + deposit.signature, + ) catch continue; + try self.validation_cache.put(self.allocator, pubkey.*, .{ + .has_valid_signature = true, + .validated_count = i + 1, + }); + return true; + } + + try self.validation_cache.put(self.allocator, pubkey.*, .{ + .has_valid_signature = false, + .validated_count = entry.deposits.items.len, + }); + return false; + } +}; diff --git a/src/state_transition/utils/seed.zig b/src/state_transition/utils/seed.zig index 3ab7f9b31..9ad8aad8f 100644 --- a/src/state_transition/utils/seed.zig +++ b/src/state_transition/utils/seed.zig @@ -77,6 +77,110 @@ 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, + committees: []const []const []const ValidatorIndex, + effective_balance_increments: []const u16, +) ![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| { + std.mem.writeInt(u64, slot_seed_input[32..][0..8], start_slot + i, .little); + + var slot_seed: [32]u8 = undefined; + Sha256.hash(&slot_seed_input, &slot_seed, .{}); + result[i] = try computePayloadTimelinessCommitteeForSlot( + allocator, + &slot_seed, + committees[i], + effective_balance_increments, + ); + } + 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); 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); diff --git a/src/state_transition/utils/validator.zig b/src/state_transition/utils/validator.zig index 0e409af95..27b2a2049 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; @@ -72,7 +74,36 @@ pub fn getActivationExitChurnLimit(epoch_cache: *const EpochCache) u64 { return @min(epoch_cache.config.chain.MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, getBalanceChurnLimitFromCache(epoch_cache)); } -pub fn getConsolidationChurnLimit(epoch_cache: *const EpochCache) u64 { +/// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-get_activation_churn_limit +pub fn getGloasActivationChurnLimit(epoch_cache: *const EpochCache) u64 { + const churn = getBalanceChurnLimit( + epoch_cache.total_active_balance_increments, + epoch_cache.config.chain.CHURN_LIMIT_QUOTIENT_GLOAS, + epoch_cache.config.chain.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, + ); + return @min(epoch_cache.config.chain.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, churn); +} + +/// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-get_exit_churn_limit +pub fn getGloasExitChurnLimit(epoch_cache: *const EpochCache) u64 { + return getBalanceChurnLimit( + epoch_cache.total_active_balance_increments, + epoch_cache.config.chain.CHURN_LIMIT_QUOTIENT_GLOAS, + epoch_cache.config.chain.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, + ); +} + +/// Spec (electra): get_consolidation_churn_limit (uses combined balance churn minus activation+exit churn) +/// Spec (gloas): get_consolidation_churn_limit (independent quotient, no MIN floor) +pub fn getConsolidationChurnLimit(comptime fork: ForkSeq, epoch_cache: *const EpochCache) u64 { + if (comptime fork.gte(.gloas)) { + return getBalanceChurnLimit( + epoch_cache.total_active_balance_increments, + epoch_cache.config.chain.CONSOLIDATION_CHURN_LIMIT_QUOTIENT, + 0, + ); + } + return getBalanceChurnLimitFromCache(epoch_cache) - getActivationExitChurnLimit(epoch_cache); } @@ -84,6 +115,39 @@ 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)) + 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; diff --git a/test/spec/runner/epoch_processing.zig b/test/spec/runner/epoch_processing.zig index 713e51170..dc5ff04c3 100644 --- a/test/spec/runner/epoch_processing.zig +++ b/test/spec/runner/epoch_processing.zig @@ -26,8 +26,11 @@ pub const EpochProcessingFn = enum { sync_committee_updates, historical_summaries_update, pending_deposits, + pending_deposits_churn, pending_consolidations, proposer_lookahead, + builder_pending_payments, + ptc_window, pub fn suiteName(self: EpochProcessingFn) []const u8 { return @tagName(self) ++ "/pyspec_tests"; @@ -128,11 +131,17 @@ pub fn TestCase(comptime fork: ForkSeq, comptime epoch_process_fn: EpochProcessi .slashings_reset => try state_transition.processSlashingsReset(fork, epoch_cache, fork_state, &epoch_transition_cache), .sync_committee_updates => try state_transition.processSyncCommitteeUpdates(fork, allocator, epoch_cache, fork_state), .historical_summaries_update => try state_transition.processHistoricalSummariesUpdate(fork, fork_state, &epoch_transition_cache), - .pending_deposits => try state_transition.processPendingDeposits(fork, allocator, std.testing.io, config, epoch_cache, fork_state, &epoch_transition_cache), + .pending_deposits, .pending_deposits_churn => try state_transition.processPendingDeposits(fork, allocator, std.testing.io, config, epoch_cache, fork_state, &epoch_transition_cache), .pending_consolidations => try state_transition.processPendingConsolidations(fork, epoch_cache, fork_state, &epoch_transition_cache), .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, &epoch_transition_cache); + }, } } }; diff --git a/test/spec/runner/fork.zig b/test/spec/runner/fork.zig index 37ef252b2..69d4d0199 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,16 @@ pub fn TestCase(comptime target_fork: ForkSeq) type { ); cached_state.state.* = .{ .fulu = upgraded.inner }; }, + .gloas => { + const upgraded = try upgradeStateToGloas( + self.pre.allocator, + std.testing.io, + config, + epoch_cache, + try cached_state.state.tryCastToFork(.fulu), + ); + cached_state.state.* = .{ .gloas = upgraded.inner }; + }, else => unreachable, } } @@ -183,6 +194,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 4754ccef7..2d30cfb6d 100644 --- a/test/spec/runner/operations.zig +++ b/test/spec/runner/operations.zig @@ -25,13 +25,19 @@ pub const Operation = enum { attester_slashing, block_header, bls_to_execution_change, + builder_deposit_request, + builder_exit_request, consolidation_request, deposit, deposit_request, execution_payload, + execution_payload_bid, + parent_execution_payload, + payload_attestation, proposer_slashing, sync_aggregate, voluntary_exit, + voluntary_exit_churn, withdrawal_request, withdrawals, @@ -40,6 +46,8 @@ pub const Operation = enum { .block_header => "block", .bls_to_execution_change => "address_change", .execution_payload => "body", + .parent_execution_payload => "block", + .voluntary_exit_churn => "voluntary_exit", .withdrawals => "execution_payload", else => @tagName(self), }; @@ -51,13 +59,19 @@ pub const Operation = enum { .attester_slashing => "AttesterSlashing", .block_header => "BeaconBlock", .bls_to_execution_change => "SignedBLSToExecutionChange", + .builder_deposit_request => "BuilderDepositRequest", + .builder_exit_request => "BuilderExitRequest", .consolidation_request => "ConsolidationRequest", .deposit => "Deposit", .deposit_request => "DepositRequest", .execution_payload => "BeaconBlockBody", + .execution_payload_bid => "SignedExecutionPayloadBid", + .parent_execution_payload => "BeaconBlock", + .payload_attestation => "PayloadAttestation", .proposer_slashing => "ProposerSlashing", .sync_aggregate => "SyncAggregate", .voluntary_exit => "SignedVoluntaryExit", + .voluntary_exit_churn => "SignedVoluntaryExit", .withdrawal_request => "WithdrawalRequest", .withdrawals => "ExecutionPayload", }; @@ -70,6 +84,14 @@ pub const Operation = enum { pub const Handler = Operation; +fn loadExecutionValid(allocator: std.mem.Allocator, dir: std.Io.Dir) bool { + const contents = dir.readFileAlloc(std.testing.io, "execution.yaml", allocator, .limited(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); @@ -81,6 +103,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { post: ?*AnyBeaconState, op: OpType.Type, bls_setting: BlsSetting, + execution_valid: bool, const Self = @This(); @@ -104,6 +127,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 +138,11 @@ 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 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); @@ -192,6 +220,14 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const config = cached_state.config; try state_transition.processBlsToExecutionChange(fork, config, state, &self.op); }, + .builder_deposit_request => { + const config = cached_state.config; + try state_transition.processBuilderDepositRequest(allocator, config, state, &self.op); + }, + .builder_exit_request => { + const config = cached_state.config; + try state_transition.processBuilderExitRequest(allocator, config, state, &self.op); + }, .consolidation_request => { const config = cached_state.config; const epoch_cache = cached_state.epoch_cache; @@ -220,10 +256,26 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { &fork_body, .{ .data_availability_status = .available, - .execution_payload_status = if (self.post != null) .valid else .invalid, + .execution_payload_status = if (self.execution_valid) .valid else .invalid, }, ); }, + .execution_payload_bid => { + const config = cached_state.config; + const epoch_cache = cached_state.epoch_cache; + try state_transition.processExecutionPayloadBid(allocator, config, epoch_cache, state, &self.op); + }, + .parent_execution_payload => { + const config = cached_state.config; + const epoch_cache = cached_state.epoch_cache; + const fork_block = BeaconBlock(.full, fork){ .inner = self.op }; + try state_transition.processParentExecutionPayload(allocator, std.testing.io, config, epoch_cache, state, &fork_block); + }, + .payload_attestation => { + const config = cached_state.config; + const epoch_cache = cached_state.epoch_cache; + try state_transition.processPayloadAttestation(allocator, std.testing.io, config, epoch_cache, state, &self.op); + }, .proposer_slashing => { const config = cached_state.config; const epoch_cache = cached_state.epoch_cache; @@ -253,7 +305,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { verify, ); }, - .voluntary_exit => { + .voluntary_exit, .voluntary_exit_churn => { const config = cached_state.config; const epoch_cache = cached_state.epoch_cache; try state_transition.processVoluntaryExit( @@ -274,25 +326,29 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { .withdrawals => { const epoch_cache = cached_state.epoch_cache; - var withdrawals_buf: [preset.MAX_WITHDRAWALS_PER_PAYLOAD]ssz.capella.Withdrawal.Type = undefined; var withdrawals_result = WithdrawalsResult{ - .withdrawals = Withdrawals.initBuffer(&withdrawals_buf), + .withdrawals = try Withdrawals.initCapacity(allocator, preset.MAX_WITHDRAWALS_PER_PAYLOAD), }; + defer withdrawals_result.withdrawals.deinit(allocator); var withdrawal_balances = std.AutoHashMap(u64, usize).init(allocator); defer withdrawal_balances.deinit(); try state_transition.getExpectedWithdrawals( fork, + allocator, epoch_cache, state, &withdrawals_result, &withdrawal_balances, ); + // 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/write_spec_tests.zig b/test/spec/write_spec_tests.zig index b31a83e85..485dc2296 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{