From 58d5129e5a28738621e763ccf9cc9a5197b3a2ea Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 09:38:23 +0700 Subject: [PATCH 01/72] fix: implement computeIndexedAttestation() functions --- .../block/is_valid_indexed_attestation.zig | 1 + .../block/process_attestation_phase0.zig | 10 ++-- src/state_transition/cache/epoch_cache.zig | 52 +++++++++++-------- .../signature_sets/indexed_attestation.zig | 18 +++++-- src/state_transition/types/attestation.zig | 8 +-- 5 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/state_transition/block/is_valid_indexed_attestation.zig b/src/state_transition/block/is_valid_indexed_attestation.zig index 591ee701b..17d3e57bc 100644 --- a/src/state_transition/block/is_valid_indexed_attestation.zig +++ b/src/state_transition/block/is_valid_indexed_attestation.zig @@ -15,6 +15,7 @@ pub fn isValidIndexedAttestation(comptime IA: type, cached_state: *const CachedB if (verify_signature) { const signature_set = try getIndexedAttestationSignatureSet(IA, cached_state.allocator, cached_state, indexed_attestation); + defer cached_state.allocator.free(signature_set.pubkeys); return try verifyAggregatedSignatureSet(&signature_set); } else { return true; diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index 1f58f47d5..6c1c61e01 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -39,11 +39,13 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco } try state.previousEpochPendingAttestations().append(allocator, pending_attestation); } - const indexed_attestation = try epoch_cache.getIndexedAttestation(.{ - .phase0 = attestation.*, - }); + var indexed_attestation: ssz.phase0.IndexedAttestation.Type = undefined; + try epoch_cache.computeIndexedAttestationPhase0(attestation, &indexed_attestation); + defer indexed_attestation.attesting_indices.deinit(allocator); - _ = try isValidIndexedAttestation(ssz.phase0.IndexedAttestation.Type, cached_state, indexed_attestation.phase0, verify_signature); + if (!try isValidIndexedAttestation(ssz.phase0.IndexedAttestation.Type, cached_state, &indexed_attestation, verify_signature)) { + return error.InvalidAttestationInvalidIndexedAttestation; + } } /// AT could be either Phase0Attestation or ElectraAttestation diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index 1f198451f..e09143383 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -451,6 +451,7 @@ pub const EpochCache = struct { self.effective_balance_increment = try EffectiveBalanceIncrementsRc.init(self.allocator, effective_balance_increment); } + /// Consumer borrows the returned slice pub fn getBeaconCommittee(self: *const EpochCache, slot: Slot, index: CommitteeIndex) ![]const ValidatorIndex { const shuffling = self.getShufflingAtSlotOrNull(slot) orelse return error.EpochShufflingNotFound; const slot_committees = shuffling.committees[slot % preset.SLOTS_PER_EPOCH]; @@ -488,36 +489,45 @@ pub const EpochCache = struct { /// consumer takes ownership of the returned indexed attestation /// hence it needs to deinit attesting_indices inside - /// TODO: unit test - pub fn getIndexedAttestation(self: *const EpochCache, attestation: Attestation) !IndexedAttestation { - var attesting_indices_ = switch (attestation) { - .phase0 => |phase0_attestation| try self.getAttestingIndicesPhase0(&phase0_attestation), - .electra => |electra_attestation| try self.getAttestingIndicesElectra(&electra_attestation), - }; + pub fn computeIndexedAttestationPhase0(self: *const EpochCache, attestation: *const ssz.phase0.Attestation.Type, out: *ssz.phase0.IndexedAttestation.Type) !void { + var attesting_indices_ = try self.getAttestingIndicesPhase0(attestation); + const sort_fn = struct { + pub fn sort(_: void, a: ValidatorIndex, b: ValidatorIndex) bool { + return a < b; + } + }.sort; const attesting_indices = attesting_indices_.moveToUnmanaged(); + std.mem.sort(ValidatorIndex, attesting_indices.items, {}, sort_fn); + out.attesting_indices = attesting_indices; + out.data = attestation.data; + out.signature = attestation.signature; + out.* = .{ + .attesting_indices = attesting_indices, + .data = attestation.data, + .signature = attestation.signature, + }; + } + + /// consumer takes ownership of the returned indexed attestation + /// hence it needs to deinit attesting_indices inside + pub fn computeIndexedAttestationElectra(self: *const EpochCache, attestation: *const ssz.electra.Attestation.Type, out: *ssz.electra.IndexedAttestation.Type) !void { + var attesting_indices_ = try self.getAttestingIndicesElectra(attestation); const sort_fn = struct { pub fn sort(_: void, a: ValidatorIndex, b: ValidatorIndex) bool { return a < b; } }.sort; + const attesting_indices = attesting_indices_.moveToUnmanaged(); std.mem.sort(ValidatorIndex, attesting_indices.items, {}, sort_fn); - return switch (attestation) { - .phase0 => |phase0_attestation| IndexedAttestation{ - .phase0 = &ssz.phase0.IndexedAttestation.Type{ - .attesting_indices = attesting_indices, - .data = phase0_attestation.data, - .signature = phase0_attestation.signature, - }, - }, - .electra => |electra_attestation| IndexedAttestation{ - .electra = &ssz.electra.IndexedAttestation.Type{ - .attesting_indices = attesting_indices, - .data = electra_attestation.data, - .signature = electra_attestation.signature, - }, - }, + out.attesting_indices = attesting_indices; + out.data = attestation.data; + out.signature = attestation.signature; + out.* = .{ + .attesting_indices = attesting_indices, + .data = attestation.data, + .signature = attestation.signature, }; } diff --git a/src/state_transition/signature_sets/indexed_attestation.zig b/src/state_transition/signature_sets/indexed_attestation.zig index 01056d29b..6312541a1 100644 --- a/src/state_transition/signature_sets/indexed_attestation.zig +++ b/src/state_transition/signature_sets/indexed_attestation.zig @@ -36,8 +36,8 @@ pub fn getAttestationWithIndicesSignatureSet( attesting_indices: []u64, ) !AggregatedSignatureSet { const epoch_cache = cached_state.getEpochCache(); - const pubkeys = try allocator.alloc(PublicKey, attesting_indices.len); + errdefer allocator.free(pubkeys); for (0..attesting_indices.len) |i| { pubkeys[i] = epoch_cache.index_to_pubkey.items[@intCast(attesting_indices[i])]; } @@ -48,25 +48,33 @@ pub fn getAttestationWithIndicesSignatureSet( return createAggregateSignatureSetFromComponents(pubkeys, signing_root, signature); } +/// Consumer need to free the returned pubkeys array pub fn getIndexedAttestationSignatureSet(comptime IA: type, allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, indexed_attestation: *const IA) !AggregatedSignatureSet { return try getAttestationWithIndicesSignatureSet(allocator, cached_state, &indexed_attestation.data, indexed_attestation.signature, indexed_attestation.attesting_indices.items); } +/// Appends to out all the AggregatedSignatureSet for each attestation in the signed_block +/// Consumer need to free the pubkeys arrays in each AggregatedSignatureSet in out +/// TODO: consume in https://github.com/ChainSafe/state-transition-z/issues/72 pub fn attestationsSignatureSets(allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, signed_block: *const SignedBeaconBlock, out: std.ArrayList(AggregatedSignatureSet)) !void { const epoch_cache = cached_state.getEpochCache(); const attestation_items = signed_block.beaconBlock().beaconBlockBody().attestations().items(); switch (attestation_items) { .phase0 => |phase0_attestations| { - for (phase0_attestations) |attestation| { - const indexed_attestation = try epoch_cache.getIndexedAttestation(.{ .phase0 = attestation }); + for (phase0_attestations) |*attestation| { + const indexed_attestation = try epoch_cache.computeIndexedAttestationPhase0(attestation); + var attesting_indices = indexed_attestation.attesting_indices; + defer attesting_indices.deinit(allocator); const signature_set = try getIndexedAttestationSignatureSet(allocator, cached_state, indexed_attestation); try out.append(signature_set); } }, .electra => |electra_attestations| { - for (electra_attestations) |attestation| { - const indexed_attestation = try epoch_cache.getIndexedAttestation(.{ .electra = attestation }); + for (electra_attestations) |*attestation| { + const indexed_attestation = try epoch_cache.computeIndexedAttestationElectra(attestation); + var attesting_indices = indexed_attestation.attesting_indices; + defer attesting_indices.deinit(allocator); const signature_set = try getIndexedAttestationSignatureSet(allocator, cached_state, indexed_attestation); try out.append(signature_set); } diff --git a/src/state_transition/types/attestation.zig b/src/state_transition/types/attestation.zig index 3f449a7d4..f2c4ea63f 100644 --- a/src/state_transition/types/attestation.zig +++ b/src/state_transition/types/attestation.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const ssz = @import("consensus_types"); const AttestationData = ssz.primitive.AttestationData.Type; @@ -27,11 +28,6 @@ pub const AttestationItems = union(enum) { electra: []ssz.electra.Attestation.Type, }; -pub const Attestation = union(enum) { - phase0: ssz.phase0.Attestation.Type, - electra: ssz.electra.Attestation.Type, -}; - pub const IndexedAttestation = union(enum) { phase0: *const ssz.phase0.IndexedAttestation.Type, electra: *const ssz.electra.IndexedAttestation.Type, @@ -50,7 +46,7 @@ pub const IndexedAttestation = union(enum) { }; } - pub fn getAttestingIndices(self: *const IndexedAttestation) []ValidatorIndex { + pub fn getAttestingIndices(self: *const IndexedAttestation) std.ArrayListUnmanaged(ValidatorIndex) { return switch (self.*) { .phase0 => |indexed_attestation| indexed_attestation.attesting_indices, .electra => |indexed_attestation| indexed_attestation.attesting_indices, From 1bd597aea4bac82b9a3518874fc00c895e411d5d Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 09:38:54 +0700 Subject: [PATCH 02/72] fix: clone aggregation_bits for phase0 PendingAttestation --- src/state_transition/block/process_attestation_phase0.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index 6c1c61e01..bca7d697d 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const BeaconStateAllForks = @import("../types/beacon_state.zig").BeaconStateAllForks; const ssz = @import("consensus_types"); +const s = @import("ssz"); const preset = @import("preset").preset; const ForkSeq = @import("config").ForkSeq; const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; @@ -21,9 +22,13 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco try validateAttestation(*const Phase0Attestation, cached_state, attestation); + // should store a clone of aggregation_bits on Phase0 BeaconState to avoid double free error + var cloned_aggregation_bits: s.BitListType(preset.MAX_VALIDATORS_PER_COMMITTEE).Type = undefined; + try s.BitListType(preset.MAX_VALIDATORS_PER_COMMITTEE).clone(allocator, &attestation.aggregation_bits, &cloned_aggregation_bits); + const pending_attestation = PendingAttestation{ .data = data, - .aggregation_bits = attestation.aggregation_bits, + .aggregation_bits = cloned_aggregation_bits, .inclusion_delay = slot - data.slot, .proposer_index = try epoch_cache.getBeaconProposer(slot), }; From 0d598557b4f912f977d3ddd0c172910d7fb76df7 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 10:52:43 +0700 Subject: [PATCH 03/72] fix: computeDomain() --- src/config/beacon_config.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config/beacon_config.zig b/src/config/beacon_config.zig index 08e7aa61e..d6c1f7565 100644 --- a/src/config/beacon_config.zig +++ b/src/config/beacon_config.zig @@ -227,8 +227,12 @@ pub const BeaconConfig = struct { }; fn computeDomain(domain_type: DomainType, fork_version: Version, genesis_validators_root: Root, out: *[32]u8) !void { - try computeForkDataRoot(fork_version, genesis_validators_root, out); - std.mem.copyForwards(u8, out[0..], domain_type[0..]); + var fork_data_root: [32]u8 = undefined; + try computeForkDataRoot(fork_version, genesis_validators_root, &fork_data_root); + // 4 first bytes is domain_type + std.mem.copyForwards(u8, out[0..4], domain_type[0..4]); + // 28 next bytes is first 28 bytes of fork_data_root + std.mem.copyForwards(u8, out[4..], fork_data_root[0..28]); } fn computeForkDataRoot(current_version: Version, genesis_validators_root: Root, out: *[32]u8) !void { From 039886b621e332fcb079464c89de0ba5c5052c00 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 11:27:20 +0700 Subject: [PATCH 04/72] fix: handle processAttestationPhase0 failing partway --- src/state_transition/block/process_attestation_phase0.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index bca7d697d..8a3ca15e4 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -25,6 +25,12 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco // should store a clone of aggregation_bits on Phase0 BeaconState to avoid double free error var cloned_aggregation_bits: s.BitListType(preset.MAX_VALIDATORS_PER_COMMITTEE).Type = undefined; try s.BitListType(preset.MAX_VALIDATORS_PER_COMMITTEE).clone(allocator, &attestation.aggregation_bits, &cloned_aggregation_bits); + var appended: bool = false; + errdefer { + if (!appended) { + cloned_aggregation_bits.deinit(allocator); + } + } const pending_attestation = PendingAttestation{ .data = data, @@ -44,6 +50,8 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco } try state.previousEpochPendingAttestations().append(allocator, pending_attestation); } + appended = true; + var indexed_attestation: ssz.phase0.IndexedAttestation.Type = undefined; try epoch_cache.computeIndexedAttestationPhase0(attestation, &indexed_attestation); defer indexed_attestation.attesting_indices.deinit(allocator); From 242b0c9ad65bebe788af978c0dadf6934e385978 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 13:28:19 +0700 Subject: [PATCH 05/72] fix: equals api in isSlashableAttestationData() --- src/state_transition/utils/attestation.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/state_transition/utils/attestation.zig b/src/state_transition/utils/attestation.zig index bdfa21763..c52c77d9f 100644 --- a/src/state_transition/utils/attestation.zig +++ b/src/state_transition/utils/attestation.zig @@ -10,10 +10,9 @@ const Slot = ssz.primitive.Slot.Type; pub fn isSlashableAttestationData(data1: *const AttestationData, data2: *const AttestationData) bool { // Double vote - // TODO(bing): implement equals API. For now return skip - // if (!ssz.phase0.AttestationData.equals(data1, data2) and data1.target.epoch == data2.target.epoch) { - // return true; - // } + if (!ssz.phase0.AttestationData.equals(data1, data2) and data1.target.epoch == data2.target.epoch) { + return true; + } // Surround vote if (data1.source.epoch < data2.source.epoch and data2.target.epoch < data1.target.epoch) { return true; From 847d265266ad170379afdbb3b5fec0ef272ccb55 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 13:49:58 +0700 Subject: [PATCH 06/72] fix: map isValidDepositSignature() exactly to the spec --- .../block/process_deposit.zig | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/state_transition/block/process_deposit.zig b/src/state_transition/block/process_deposit.zig index aafa6778f..61ba5bd15 100644 --- a/src/state_transition/block/process_deposit.zig +++ b/src/state_transition/block/process_deposit.zig @@ -94,7 +94,7 @@ pub fn applyDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAllFor if (state.isPreElectra()) { if (is_new_validator) { - if (try isValidDepositSignature(config, pubkey, withdrawal_credentials, amount, signature)) { + if (isValidDepositSignature(config, pubkey, withdrawal_credentials, amount, signature)) { try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credentials, amount); } } else { @@ -112,7 +112,7 @@ pub fn applyDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAllFor }; if (is_new_validator) { - if (try isValidDepositSignature(config, pubkey, withdrawal_credentials, amount, signature)) { + if (isValidDepositSignature(config, pubkey, withdrawal_credentials, amount, signature)) { try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credentials, 0); try state.pendingDeposits().append(allocator, pending_deposit); } @@ -175,7 +175,9 @@ pub fn addValidatorToRegistry( try balances.append(allocator, amount); } -pub fn isValidDepositSignature(config: *const BeaconConfig, pubkey: BLSPubkey, withdrawal_credential: WithdrawalCredentials, amount: u64, deposit_signature: BLSSignature) !bool { +/// refer to https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/electra/beacon-chain.md#new-is_valid_deposit_signature +/// no need to return error union since consumer does not care about the reason of failure +pub fn isValidDepositSignature(config: *const BeaconConfig, pubkey: BLSPubkey, withdrawal_credential: WithdrawalCredentials, amount: u64, deposit_signature: BLSSignature) bool { // verify the deposit signature (proof of posession) which is not checked by the deposit contract const deposit_message = DepositMessage{ .pubkey = pubkey, @@ -187,14 +189,14 @@ pub fn isValidDepositSignature(config: *const BeaconConfig, pubkey: BLSPubkey, w // fork-agnostic domain since deposits are valid across forks var domain: Domain = undefined; - try computeDomain(DOMAIN_DEPOSIT, GENESIS_FORK_VERSION, ZERO_HASH, &domain); + computeDomain(DOMAIN_DEPOSIT, GENESIS_FORK_VERSION, ZERO_HASH, &domain) catch return false; var signing_root: Root = undefined; - try computeSigningRoot(ssz.phase0.DepositMessage, &deposit_message, domain, &signing_root); + computeSigningRoot(ssz.phase0.DepositMessage, &deposit_message, domain, &signing_root) catch return false; // Pubkeys must be checked for group + inf. This must be done only once when the validator deposit is processed - const public_key = try blst.PublicKey.uncompress(&pubkey); - try public_key.validate(); - const signature = try blst.Signature.uncompress(&deposit_signature); - try signature.validate(true); + const public_key = blst.PublicKey.uncompress(&pubkey) catch return false; + public_key.validate() catch return false; + const signature = blst.Signature.uncompress(&deposit_signature) catch return false; + signature.validate(true) catch return false; return verify(&signing_root, &public_key, &signature, null, null); } From 29848c5a888f44cfc8e877247ac4c9e29c839ea3 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 14:19:18 +0700 Subject: [PATCH 07/72] fix: consume isValidDepositSignature() without try --- src/state_transition/epoch/process_pending_deposits.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state_transition/epoch/process_pending_deposits.zig b/src/state_transition/epoch/process_pending_deposits.zig index ed2f0e816..d5fd4c688 100644 --- a/src/state_transition/epoch/process_pending_deposits.zig +++ b/src/state_transition/epoch/process_pending_deposits.zig @@ -125,11 +125,11 @@ fn applyPendingDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAll if (!is_validator_known) { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract - if (try isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { + if (isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credential, amount); } - if (try isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { + if (isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credential, amount); try cache.is_compounding_validator_arr.append(hasCompoundingWithdrawalCredential(withdrawal_credential)); // set balance, so that the next deposit of same pubkey will increase the balance correctly From d29c857f4f1a267d201d61c5c6a05d217d673ec2 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 14:20:11 +0700 Subject: [PATCH 08/72] fix: getProposerSlashingSignatureSets signing root --- src/state_transition/signature_sets/proposer_slashings.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state_transition/signature_sets/proposer_slashings.zig b/src/state_transition/signature_sets/proposer_slashings.zig index bd4083dbd..c503da09b 100644 --- a/src/state_transition/signature_sets/proposer_slashings.zig +++ b/src/state_transition/signature_sets/proposer_slashings.zig @@ -23,9 +23,9 @@ pub fn getProposerSlashingSignatureSets(cached_state: *const CachedBeaconStateAl const domain_1 = try config.getDomain(state.slot(), c.DOMAIN_BEACON_PROPOSER, signed_header_1.message.slot); const domain_2 = try config.getDomain(state.slot(), c.DOMAIN_BEACON_PROPOSER, signed_header_2.message.slot); var signing_root_1: [32]u8 = undefined; - try computeSigningRoot(ssz.phase0.SignedBeaconBlockHeader, &signed_header_1, domain_1, &signing_root_1); + try computeSigningRoot(ssz.phase0.BeaconBlockHeader, &signed_header_1.message, domain_1, &signing_root_1); var signing_root_2: [32]u8 = undefined; - try computeSigningRoot(ssz.phase0.SignedBeaconBlockHeader, &signed_header_2, domain_2, &signing_root_2); + try computeSigningRoot(ssz.phase0.BeaconBlockHeader, &signed_header_2.message, domain_2, &signing_root_2); result[0] = SingleSignatureSet{ .pubkey = epoch_cache.index_to_pubkey.items[signed_header_1.message.proposer_index], From 2be0eaa8822475e3890067f5dbda551481ca2891 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Oct 2025 14:31:47 +0700 Subject: [PATCH 09/72] fix: processVoluntaryExit() --- src/state_transition/block/process_voluntary_exit.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/state_transition/block/process_voluntary_exit.zig b/src/state_transition/block/process_voluntary_exit.zig index c136d3a66..5e8d268a0 100644 --- a/src/state_transition/block/process_voluntary_exit.zig +++ b/src/state_transition/block/process_voluntary_exit.zig @@ -13,9 +13,8 @@ pub fn processVoluntaryExit(cached_state: *CachedBeaconStateAllForks, signed_vol if (!try isValidVoluntaryExit(cached_state, signed_voluntary_exit, verify_signature)) { return error.InvalidVoluntaryExit; } - - var validator = cached_state.state.validators().items[signed_voluntary_exit.message.validator_index]; - try initiateValidatorExit(cached_state, &validator); + const validator = &cached_state.state.validators().items[signed_voluntary_exit.message.validator_index]; + try initiateValidatorExit(cached_state, validator); } pub fn isValidVoluntaryExit(cached_state: *CachedBeaconStateAllForks, signed_voluntary_exit: *const SignedVoluntaryExit, verify_signature: bool) !bool { From a493a2cab07be016f243fd7713e8ea77625fb8db Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 09:51:46 +0700 Subject: [PATCH 10/72] feat: implement ChainConfig for spec tests --- src/config/chain/chain_config.zig | 18 ++++++++ src/config/root.zig | 1 + .../test_utils/generate_state.zig | 42 +++++++++++++++++-- test/spec/runner/Operations.zig | 2 +- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/config/chain/chain_config.zig b/src/config/chain/chain_config.zig index 29dd8946b..871a29f2f 100644 --- a/src/config/chain/chain_config.zig +++ b/src/config/chain/chain_config.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const ssz = @import("consensus_types"); const Epoch = ssz.primitive.Epoch.Type; const Preset = @import("preset").Preset; @@ -91,3 +92,20 @@ pub const BlobScheduleEntry = struct { EPOCH: Epoch, MAX_BLOBS_PER_BLOCK: u64, }; + +pub fn mergeChainConfig(config: ChainConfig, fields: anytype) ChainConfig { + var merged = config; + inline for (std.meta.fields(@TypeOf(fields))) |field| { + @field(merged, field.name) = @field(fields, field.name); + } + return merged; +} + +test mergeChainConfig { + const mainnet_config = @import("./networks/mainnet.zig").mainnet_chain_config; + const old_altair_epoch = mainnet_config.ALTAIR_FORK_EPOCH; + const merged_config = mergeChainConfig(mainnet_config, .{ + .ALTAIR_FORK_EPOCH = old_altair_epoch + 1000, + }); + try std.testing.expect(merged_config.ALTAIR_FORK_EPOCH == old_altair_epoch + 1000); +} diff --git a/src/config/root.zig b/src/config/root.zig index f0e7cdd34..7d3c88f61 100644 --- a/src/config/root.zig +++ b/src/config/root.zig @@ -5,6 +5,7 @@ pub const ChainConfig = @import("./chain/chain_config.zig").ChainConfig; pub const ForkSeq = @import("./fork.zig").ForkSeq; pub const ForkInfo = @import("./fork.zig").ForkInfo; pub const forkSeqByForkName = @import("./fork.zig").forkSeqByForkName; +pub const mergeChainConfig = @import("./chain/chain_config.zig").mergeChainConfig; pub const TOTAL_FORKS = @import("./fork.zig").TOTAL_FORKS; pub const mainnet_chain_config = @import("./chain/networks/mainnet.zig").mainnet_chain_config; pub const minimal_chain_config = @import("./chain/networks/minimal.zig").minimal_chain_config; diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index 8eed9add0..351cb610d 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -1,6 +1,7 @@ const std = @import("std"); const blst = @import("blst"); const Allocator = std.mem.Allocator; +const ForkSeq = @import("config").ForkSeq; const mainnet_chain_config = @import("config").mainnet_chain_config; const minimal_chain_config = @import("config").minimal_chain_config; const ssz = @import("consensus_types"); @@ -8,10 +9,12 @@ const hex = @import("hex"); const ElectraBeaconState = ssz.electra.BeaconState.Type; const BLSPubkey = ssz.primitive.BLSPubkey.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; +const Epoch = ssz.primitive.Epoch.Type; const preset = @import("preset").preset; const active_preset = @import("preset").active_preset; const BeaconConfig = @import("config").BeaconConfig; const ChainConfig = @import("config").ChainConfig; +const mergeChainConfig = @import("config").mergeChainConfig; const state_transition = @import("../root.zig"); const CachedBeaconStateAllForks = state_transition.CachedBeaconStateAllForks; const BeaconStateAllForks = state_transition.BeaconStateAllForks; @@ -155,10 +158,10 @@ pub const TestCachedBeaconStateAllForks = struct { errdefer state.deinit(allocator); defer allocator.destroy(state); - return initFromState(allocator, state); + return initFromState(allocator, state, ForkSeq.electra, state.fork().epoch); } - pub fn initFromState(allocator: Allocator, state: *BeaconStateAllForks) !TestCachedBeaconStateAllForks { + pub fn initFromState(allocator: Allocator, state: *BeaconStateAllForks, fork: ForkSeq, fork_epoch: Epoch) !TestCachedBeaconStateAllForks { const owned_state = try allocator.create(BeaconStateAllForks); owned_state.* = state.*; @@ -166,7 +169,8 @@ pub const TestCachedBeaconStateAllForks = struct { const index_pubkey_cache = try allocator.create(Index2PubkeyCache); errdefer allocator.destroy(index_pubkey_cache); index_pubkey_cache.* = Index2PubkeyCache.init(allocator); - const config = try BeaconConfig.init(allocator, active_chain_config, owned_state.genesisValidatorsRoot()); + const chain_config = getConfig(active_chain_config, fork, fork_epoch); + const config = try BeaconConfig.init(allocator, chain_config, owned_state.genesisValidatorsRoot()); try syncPubkeys(owned_state.validators().items, pubkey_index_map, index_pubkey_cache); @@ -200,6 +204,38 @@ pub const TestCachedBeaconStateAllForks = struct { } }; +/// get a ChainConfig for spec test, refer to https://github.com/ChainSafe/lodestar/blob/v1.35.0/packages/beacon-node/test/utils/config.ts#L9 +pub fn getConfig(config: ChainConfig, fork: ForkSeq, fork_epoch: Epoch) ChainConfig { + switch (fork) { + .phase0 => return config, + .altair => return mergeChainConfig(config, .{ + .ALTAIR_FORK_EPOCH = fork_epoch, + }), + .bellatrix => return mergeChainConfig(config, .{ + .ALTAIR_FORK_EPOCH = 0, + .BELLATRIX_FORK_EPOCH = fork_epoch, + }), + .capella => return mergeChainConfig(config, .{ + .ALTAIR_FORK_EPOCH = 0, + .BELLATRIX_FORK_EPOCH = 0, + .CAPELLA_FORK_EPOCH = fork_epoch, + }), + .deneb => return mergeChainConfig(config, .{ + .ALTAIR_FORK_EPOCH = 0, + .BELLATRIX_FORK_EPOCH = 0, + .CAPELLA_FORK_EPOCH = 0, + .DENEB_FORK_EPOCH = fork_epoch, + }), + .electra => return mergeChainConfig(config, .{ + .ALTAIR_FORK_EPOCH = 0, + .BELLATRIX_FORK_EPOCH = 0, + .CAPELLA_FORK_EPOCH = 0, + .DENEB_FORK_EPOCH = 0, + .ELECTRA_FORK_EPOCH = fork_epoch, + }), + } +} + test TestCachedBeaconStateAllForks { const allocator = std.testing.allocator; var test_state = try TestCachedBeaconStateAllForks.init(allocator, 256); diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index 567cf8da5..f1b16f4db 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -112,7 +112,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); // init the post state if this is a "valid" test case From 991c0c84f97a95a0f4ef0e98d66c0b39bea020d7 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 09:58:36 +0700 Subject: [PATCH 11/72] fix: free pubkeys in processAttestationsAltair() --- src/state_transition/block/process_attestation_altair.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index dd1c407c8..999b76818 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -57,6 +57,7 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach // we can verify only that and nothing else. if (verify_signature) { const sig_set = try getAttestationWithIndicesSignatureSet(allocator, cached_state, &attestation.data, attestation.signature, attesting_indices.items); + defer allocator.free(sig_set.pubkeys); if (!try verifyAggregatedSignatureSet(&sig_set)) { return error.InvalidSignature; } From 11dc0254456a2752231589df2fcccbd4e2b35d78 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 10:21:35 +0700 Subject: [PATCH 12/72] fix: processAttestationsAltair for loop --- .../block/process_attestation_altair.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index 999b76818..907e33959 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -108,12 +108,11 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach } } } - - // 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); } + // 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); increaseBalance(state, try epoch_cache.getBeaconProposer(state_slot), proposer_reward); } From 41dea08858256b8eabad26ad2fe3a013886e67a0 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 10:43:15 +0700 Subject: [PATCH 13/72] fix: bellatrix processExecutionPayload() --- .../block/process_execution_payload.zig | 3 ++- src/state_transition/types/beacon_block.zig | 6 +++--- src/state_transition/types/beacon_state.zig | 17 +++++++++-------- .../types/execution_payload.zig | 16 ++++++++-------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/state_transition/block/process_execution_payload.zig b/src/state_transition/block/process_execution_payload.zig index 63cca5975..267d08595 100644 --- a/src/state_transition/block/process_execution_payload.zig +++ b/src/state_transition/block/process_execution_payload.zig @@ -52,7 +52,8 @@ 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(state)) { - if (!std.mem.eql(u8, &partial_payload.parent_hash, &partial_payload.block_hash)) { + const latest_header = state.latestExecutionPayloadHeader(); + if (!std.mem.eql(u8, &partial_payload.parent_hash, &latest_header.getBlockHash())) { return error.InvalidExecutionPayloadParentHash; } } diff --git a/src/state_transition/types/beacon_block.zig b/src/state_transition/types/beacon_block.zig index 94afbbe63..30ce7f67a 100644 --- a/src/state_transition/types/beacon_block.zig +++ b/src/state_transition/types/beacon_block.zig @@ -417,9 +417,9 @@ pub const BlindedBeaconBlockBody = union(enum) { // bellatrix fields pub fn executionPayloadHeader(self: *const BlindedBeaconBlockBody) ExecutionPayloadHeader { return switch (self.*) { - .capella => |body| .{ .capella = body.execution_payload_header }, - .deneb => |body| .{ .deneb = body.execution_payload_header }, - .electra => |body| .{ .electra = body.execution_payload_header }, + .capella => |body| .{ .capella = &body.execution_payload_header }, + .deneb => |body| .{ .deneb = &body.execution_payload_header }, + .electra => |body| .{ .electra = &body.execution_payload_header }, }; } diff --git a/src/state_transition/types/beacon_state.zig b/src/state_transition/types/beacon_state.zig index e2507415b..96b17de5f 100644 --- a/src/state_transition/types/beacon_state.zig +++ b/src/state_transition/types/beacon_state.zig @@ -515,21 +515,22 @@ pub const BeaconStateAllForks = union(enum) { } } - pub fn latestExecutionPayloadHeader(self: *const BeaconStateAllForks) *const ExecutionPayloadHeader { + pub fn latestExecutionPayloadHeader(self: *const BeaconStateAllForks) ExecutionPayloadHeader { return switch (self.*) { - .bellatrix => |state| &.{ .bellatrix = state.latest_execution_payload_header }, - .capella => |state| &.{ .capella = state.latest_execution_payload_header }, - .deneb, .electra => |state| &.{ .deneb = state.latest_execution_payload_header }, + .bellatrix => |state| .{ .bellatrix = &state.latest_execution_payload_header }, + .capella => |state| .{ .capella = &state.latest_execution_payload_header }, + .deneb => |state| .{ .deneb = &state.latest_execution_payload_header }, + .electra => |state| .{ .electra = &state.latest_execution_payload_header }, else => panic("latest_execution_payload_header is not available in {}", .{self}), }; } pub fn setLatestExecutionPayloadHeader(self: *BeaconStateAllForks, header: *const ExecutionPayloadHeader) void { switch (self.*) { - .bellatrix => |state| state.latest_execution_payload_header = header.*.bellatrix, - .capella => |state| state.latest_execution_payload_header = header.*.capella, - .deneb => |state| state.latest_execution_payload_header = header.*.deneb, - .electra => |state| state.latest_execution_payload_header = header.*.electra, + .bellatrix => |state| state.latest_execution_payload_header = header.*.bellatrix.*, + .capella => |state| state.latest_execution_payload_header = header.*.capella.*, + .deneb => |state| state.latest_execution_payload_header = header.*.deneb.*, + .electra => |state| state.latest_execution_payload_header = header.*.electra.*, else => panic("latest_execution_payload_header is not available in {}", .{self}), } } diff --git a/src/state_transition/types/execution_payload.zig b/src/state_transition/types/execution_payload.zig index c487bb866..2ce83d959 100644 --- a/src/state_transition/types/execution_payload.zig +++ b/src/state_transition/types/execution_payload.zig @@ -30,7 +30,7 @@ pub const ExecutionPayload = union(enum) { errdefer header.extra_data.deinit(allocator); try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); return .{ - .bellatrix = header, + .bellatrix = &header, }; }, .capella => |payload| { @@ -43,7 +43,7 @@ pub const ExecutionPayload = union(enum) { try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); try ssz.capella.Withdrawals.hashTreeRoot(allocator, &payload.withdrawals, &header.withdrawals_root); return .{ - .capella = header, + .capella = &header, }; }, .deneb => |payload| { @@ -58,7 +58,7 @@ pub const ExecutionPayload = union(enum) { header.blob_gas_used = payload.blob_gas_used; header.excess_blob_gas = payload.excess_blob_gas; return .{ - .deneb = header, + .deneb = &header, }; }, .electra => |payload| { @@ -74,7 +74,7 @@ pub const ExecutionPayload = union(enum) { header.blob_gas_used = payload.blob_gas_used; header.excess_blob_gas = payload.excess_blob_gas; return .{ - .electra = header, + .electra = &header, }; }, }; @@ -187,10 +187,10 @@ pub const ExecutionPayload = union(enum) { }; pub const ExecutionPayloadHeader = union(enum) { - bellatrix: ssz.bellatrix.ExecutionPayloadHeader.Type, - capella: ssz.capella.ExecutionPayloadHeader.Type, - deneb: ssz.deneb.ExecutionPayloadHeader.Type, - electra: ssz.electra.ExecutionPayloadHeader.Type, + bellatrix: *const ssz.bellatrix.ExecutionPayloadHeader.Type, + capella: *const ssz.capella.ExecutionPayloadHeader.Type, + deneb: *const ssz.deneb.ExecutionPayloadHeader.Type, + electra: *const ssz.electra.ExecutionPayloadHeader.Type, pub fn isCapellaPayloadHeader(self: *const ExecutionPayloadHeader) bool { return switch (self.*) { From 99f772504c20d798faa9a514a548df974ebe86b9 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 10:50:15 +0700 Subject: [PATCH 14/72] fix: Operations test runner processExecutionPayload() --- test/spec/runner/Operations.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index f1b16f4db..d97e257d6 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -182,7 +182,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime self.pre.allocator, self.pre.cached_state, .{ .regular = @unionInit(state_transition.BeaconBlockBody, @tagName(fork), &self.op) }, - .{ .data_availability_status = .available, .execution_payload_status = .valid }, + .{ .data_availability_status = .available, .execution_payload_status = if (valid) .valid else .invalid }, ); }, .proposer_slashing => { From b36bd5c3dbb6254166bd2bd04b25eb67bd9028d3 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 11:02:59 +0700 Subject: [PATCH 15/72] fix: init correct root in processBlsToExecutionChange() --- .../block/process_bls_to_execution_change.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/state_transition/block/process_bls_to_execution_change.zig b/src/state_transition/block/process_bls_to_execution_change.zig index 92d68522d..2a2f23fad 100644 --- a/src/state_transition/block/process_bls_to_execution_change.zig +++ b/src/state_transition/block/process_bls_to_execution_change.zig @@ -7,14 +7,15 @@ const c = @import("constants"); const digest = @import("../utils/sha256.zig").digest; const verifyBlsToExecutionChangeSignature = @import("../signature_sets/bls_to_execution_change.zig").verifyBlsToExecutionChangeSignature; -pub fn processBlsToExecutionChange(state: *CachedBeaconStateAllForks, signed_bls_to_execution_change: *const SignedBLSToExecutionChange) !void { +pub fn processBlsToExecutionChange(cached_state: *CachedBeaconStateAllForks, signed_bls_to_execution_change: *const SignedBLSToExecutionChange) !void { const address_change = signed_bls_to_execution_change.message; + const state = cached_state.state; - try isValidBlsToExecutionChange(state, signed_bls_to_execution_change, true); + try isValidBlsToExecutionChange(cached_state, signed_bls_to_execution_change, true); - var new_withdrawal_credentials: Root = undefined; + var new_withdrawal_credentials: Root = [_]u8{0} ** 32; const validator_index = address_change.validator_index; - var validator = state.state.validators().items[validator_index]; + var validator = &state.validators().items[validator_index]; new_withdrawal_credentials[0] = c.ETH1_ADDRESS_WITHDRAWAL_PREFIX; @memcpy(new_withdrawal_credentials[12..], &address_change.to_execution_address); From e8733fb2546d88d8783741ee03490379fe1b5239 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 15:34:37 +0700 Subject: [PATCH 16/72] fix: pass payload_withdrawals_root to processWithdrawals() --- src/state_transition/block/process_block.zig | 24 +++++++------------ .../block/process_withdrawals.zig | 10 ++++++++ test/int/process_withdrawals.zig | 7 ++++-- test/spec/runner/Operations.zig | 7 +++++- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index fb2c227d2..723d76c60 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const ForkSeq = @import("config").ForkSeq; const ssz = @import("consensus_types"); +const Root = ssz.primitive.Root.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; const preset = @import("preset").preset; const BeaconBlock = @import("../types/beacon_block.zig").BeaconBlock; @@ -56,23 +57,16 @@ pub fn processBlock( defer withdrawals_result.withdrawals.clearRetainingCapacity(); const body = block.beaconBlockBody(); - switch (body) { - .regular => |b| { + const payload_withdrawals_root = switch (body) { + .regular => |b| blk: { const actual_withdrawals = b.executionPayload().getWithdrawals(); - std.debug.assert(withdrawals_result.withdrawals.items.len == actual_withdrawals.items.len); - for (withdrawals_result.withdrawals.items, actual_withdrawals.items) |expected, actual| { - std.debug.assert(ssz.capella.Withdrawal.equals(&expected, &actual)); - } + var root: Root = undefined; + try ssz.capella.Withdrawals.hashTreeRoot(allocator, &actual_withdrawals, &root); + break :blk root; }, - .blinded => |b| { - const header = b.executionPayloadHeader(); - var expected: [32]u8 = undefined; - try ssz.capella.Withdrawals.hashTreeRoot(allocator, &withdrawals_result.withdrawals, &expected); - var actual = header.getWithdrawalsRoot(); - std.debug.assert(std.mem.eql(u8, &expected, &actual)); - }, - } - try processWithdrawals(cached_state, withdrawals_result); + .blinded => |b| b.executionPayloadHeader().getWithdrawalsRoot(), + }; + try processWithdrawals(allocator, cached_state, withdrawals_result, payload_withdrawals_root); } try processExecutionPayload( diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 0de4010b6..82925aea7 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const ssz = @import("consensus_types"); +const Root = ssz.primitive.Root.Type; const preset = @import("preset").preset; const c = @import("constants"); const ForkSeq = @import("config").ForkSeq; @@ -23,8 +24,10 @@ pub const WithdrawalsResult = struct { }; pub fn processWithdrawals( + allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, expected_withdrawals_result: WithdrawalsResult, + payload_withdrawals_root: Root, ) !void { const state = cached_state.state; // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) @@ -32,6 +35,13 @@ pub fn processWithdrawals( const expected_withdrawals = expected_withdrawals_result.withdrawals.items; const num_withdrawals = expected_withdrawals.len; + var expected_withdrawals_root: [32]u8 = undefined; + try ssz.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]; decreaseBalance(state, withdrawal.validator_index, withdrawal.amount); diff --git a/test/int/process_withdrawals.zig b/test/int/process_withdrawals.zig index fbf5ff376..49bcd5d04 100644 --- a/test/int/process_withdrawals.zig +++ b/test/int/process_withdrawals.zig @@ -13,12 +13,14 @@ test "process withdrawals - sanity" { var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); defer withdrawal_balances.deinit(); + var root: Root = undefined; + try ssz.capella.Withdrawals.hashTreeRoot(allocator, &withdrawals_result.withdrawals, &root); + try getExpectedWithdrawals(allocator, &withdrawals_result, &withdrawal_balances, test_state.cached_state); - try processWithdrawals(test_state.cached_state, withdrawals_result); + try processWithdrawals(allocator, test_state.cached_state, withdrawals_result, root); } const std = @import("std"); - const state_transition = @import("state_transition"); const preset = @import("preset").preset; const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; @@ -26,5 +28,6 @@ const processWithdrawals = state_transition.processWithdrawals; const getExpectedWithdrawals = state_transition.getExpectedWithdrawals; const WithdrawalsResult = state_transition.WithdrawalsResult; const ssz = @import("consensus_types"); +const Root = ssz.primitive.Root.Type; const Withdrawals = ssz.capella.Withdrawals.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index d97e257d6..271364e5e 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -1,4 +1,5 @@ const ssz = @import("consensus_types"); +const Root = ssz.primitive.Root.Type; const ForkSeq = @import("config").ForkSeq; const Preset = @import("preset").Preset; const preset = @import("preset").preset; @@ -211,7 +212,11 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime try state_transition.getExpectedWithdrawals(self.pre.allocator, &withdrawals_result, &withdrawal_balances, self.pre.cached_state); defer withdrawals_result.withdrawals.deinit(self.pre.allocator); - try state_transition.processWithdrawals(self.pre.cached_state, withdrawals_result); + var payload_withdrawals_root: Root = undefined; + // self.op is ExecutionPayload in this case + try ssz.capella.Withdrawals.hashTreeRoot(self.pre.allocator, &self.op.withdrawals, &payload_withdrawals_root); + + try state_transition.processWithdrawals(self.pre.allocator, self.pre.cached_state, withdrawals_result, payload_withdrawals_root); }, } } From ef300a63614694074193096b4cf6874efe66ad58 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Oct 2025 15:41:19 +0700 Subject: [PATCH 17/72] fix: update next_withdrawal_index in processWithdrawals() --- src/state_transition/block/process_withdrawals.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 82925aea7..529379a71 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -70,7 +70,7 @@ pub fn processWithdrawals( } else { // expected withdrawals came up short in the bound, so we move nextWithdrawalValidatorIndex to // the next post the bound - next_withdrawal_index.* = (state.nextWithdrawalValidatorIndex().* + preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) % state.validators().items.len; + next_withdrawal_index.* = (next_withdrawal_index.* + preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) % state.validators().items.len; } } From 1ceb59bf800ff4331be47934f44e8e002347a78f Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 25 Oct 2025 12:50:19 +0700 Subject: [PATCH 18/72] fix: processWithdrawals() and getExpectedWithdrawals() misuse variables --- src/state_transition/block/process_withdrawals.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 529379a71..372320c68 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -23,6 +23,10 @@ pub const WithdrawalsResult = struct { processed_partial_withdrawals_count: usize = 0, }; +/// right now for the implementation we pass in processBlock() +/// for the spec, we pass in params from Operations.zig +/// TODO: spec and implementation should be the same +/// refer to https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#modified-process_withdrawals pub fn processWithdrawals( allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, @@ -63,14 +67,15 @@ pub fn processWithdrawals( } // Update the nextWithdrawalValidatorIndex + const nextWithdrawalValidatorIndex = state.nextWithdrawalValidatorIndex(); if (expected_withdrawals.len == preset.MAX_WITHDRAWALS_PER_PAYLOAD) { // All slots filled, nextWithdrawalValidatorIndex should be validatorIndex having next turn - next_withdrawal_index.* = + nextWithdrawalValidatorIndex.* = (expected_withdrawals[expected_withdrawals.len - 1].validator_index + 1) % state.validators().items.len; } else { // expected withdrawals came up short in the bound, so we move nextWithdrawalValidatorIndex to // the next post the bound - next_withdrawal_index.* = (next_withdrawal_index.* + preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) % state.validators().items.len; + nextWithdrawalValidatorIndex.* = (nextWithdrawalValidatorIndex.* + preset.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) % state.validators().items.len; } } @@ -172,6 +177,7 @@ pub fn getExpectedWithdrawals( .address = execution_address, .amount = balance, }); + withdrawal_index += 1; } else if ((effective_balance == if (state.isPostElectra()) getMaxEffectiveBalance(withdrawal_credentials) else From a19884cc5c7bb3f94f1b0cb8a5b4677424693af3 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 25 Oct 2025 13:06:22 +0700 Subject: [PATCH 19/72] fix: use validator ptr in switchToCompoundingValidator() --- src/state_transition/utils/electra.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/utils/electra.zig b/src/state_transition/utils/electra.zig index 031ff28cd..924d4cfb1 100644 --- a/src/state_transition/utils/electra.zig +++ b/src/state_transition/utils/electra.zig @@ -25,7 +25,7 @@ pub fn hasExecutionWithdrawalCredential(withdrawal_credentials: WithdrawalCreden } pub fn switchToCompoundingValidator(allocator: Allocator, state_cache: *CachedBeaconStateAllForks, index: ValidatorIndex) !void { - var validator = state_cache.state.validators().items[index]; + var validator = &state_cache.state.validators().items[index]; // directly modifying the byte leads to ssz.primitive missing the modification resulting into // wrong root compute, although slicing can be avoided but anyway this is not going From 731293844789a9a475c99870017fc2440311dfed Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 25 Oct 2025 14:18:38 +0700 Subject: [PATCH 20/72] fix: cleanup resource when no post state --- src/state_transition/cache/epoch_cache.zig | 9 +++++++-- src/state_transition/cache/sync_committee_cache.zig | 8 +++++++- src/state_transition/test_utils/generate_state.zig | 1 + src/state_transition/utils/epoch_shuffling.zig | 1 + test/spec/runner/Operations.zig | 11 +++++++++-- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index e09143383..b86b9c00c 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -240,8 +240,13 @@ pub const EpochCache = struct { const sync_proposer_reward = sync_participant_reward * PROPOSER_WEIGHT_FACTOR; const base_reward_pre_increment = computeBaseRewardPerIncrement(total_active_balance_increments); const skip_sync_committee_cache = if (option) |opt| opt.skip_sync_committee_cache else !after_altair_fork; - const current_sync_committee_indexed = if (skip_sync_committee_cache) SyncCommitteeCacheAllForks.initEmpty() else try SyncCommitteeCacheAllForks.initSyncCommittee(allocator, state.currentSyncCommittee(), pubkey_to_index); - const next_sync_committee_indexed = if (skip_sync_committee_cache) SyncCommitteeCacheAllForks.initEmpty() else try SyncCommitteeCacheAllForks.initSyncCommittee(allocator, state.nextSyncCommittee(), pubkey_to_index); + var current_sync_committee_indexed = if (skip_sync_committee_cache) SyncCommitteeCacheAllForks.initEmpty() else try SyncCommitteeCacheAllForks.initSyncCommittee(allocator, state.currentSyncCommittee(), pubkey_to_index); + var next_sync_committee_indexed = if (skip_sync_committee_cache) SyncCommitteeCacheAllForks.initEmpty() else try SyncCommitteeCacheAllForks.initSyncCommittee(allocator, state.nextSyncCommittee(), pubkey_to_index); + + errdefer { + current_sync_committee_indexed.deinit(); + next_sync_committee_indexed.deinit(); + } // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the // active validator indices set changes in size. Validators change active status only when: diff --git a/src/state_transition/cache/sync_committee_cache.zig b/src/state_transition/cache/sync_committee_cache.zig index 0361a2862..36b25cd08 100644 --- a/src/state_transition/cache/sync_committee_cache.zig +++ b/src/state_transition/cache/sync_committee_cache.zig @@ -78,7 +78,13 @@ const SyncCommitteeCache = struct { errdefer allocator.destroy(validator_index_map); validator_index_map.* = SyncComitteeValidatorIndexMap.init(allocator); - errdefer validator_index_map.deinit(); + errdefer { + var value_iterator = validator_index_map.valueIterator(); + while (value_iterator.next()) |value| { + value.deinit(); + } + validator_index_map.deinit(); + } try computeSyncCommitteeMap(allocator, validator_indices, validator_index_map); diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index 351cb610d..402c860cd 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -166,6 +166,7 @@ pub const TestCachedBeaconStateAllForks = struct { owned_state.* = state.*; const pubkey_index_map = try PubkeyIndexMap.init(allocator); + errdefer allocator.destroy(pubkey_index_map); const index_pubkey_cache = try allocator.create(Index2PubkeyCache); errdefer allocator.destroy(index_pubkey_cache); index_pubkey_cache.* = Index2PubkeyCache.init(allocator); diff --git a/src/state_transition/utils/epoch_shuffling.zig b/src/state_transition/utils/epoch_shuffling.zig index 13afba1ba..05eb3a383 100644 --- a/src/state_transition/utils/epoch_shuffling.zig +++ b/src/state_transition/utils/epoch_shuffling.zig @@ -35,6 +35,7 @@ pub const EpochShuffling = struct { pub fn init(allocator: Allocator, seed: [32]u8, epoch: Epoch, active_indices: []const ValidatorIndex) !*EpochShuffling { const shuffling = try allocator.alloc(ValidatorIndex, active_indices.len); + errdefer allocator.free(shuffling); std.mem.copyForwards(ValidatorIndex, shuffling, active_indices); try unshuffleList(shuffling, seed[0..], preset.SHUFFLE_ROUND_COUNT); const committees = try buildCommitteesFromShuffling(allocator, shuffling); diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index 271364e5e..427601a06 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -104,17 +104,24 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime // init the pre state const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + var transfered_pre_state: bool = false; errdefer { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); + if (!transfered_pre_state) { + ForkTypes.BeaconState.deinit(allocator, pre_state); + allocator.destroy(pre_state); + } } pre_state.* = ForkTypes.BeaconState.default_value; try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); + transfered_pre_state = true; + var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); + errdefer tc.pre.deinit(); + // init the post state if this is a "valid" test case if (valid) { From 443faa82dd9c7994304cfbd98e47f739c845d3d8 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 27 Oct 2025 10:17:21 +0700 Subject: [PATCH 21/72] fix: write Operations test case without .valid --- test/spec/runner/Operations.zig | 27 +++++++++++++++++---------- test/spec/writer/Operations.zig | 4 +--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index 427601a06..d3e807773 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -66,13 +66,14 @@ pub const Operation = enum { pub const Handler = Operation; -pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime valid: bool) type { +pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const ForkTypes = @field(ssz, fork.forkName()); const OpType = @field(ForkTypes, operation.operationObject()); return struct { pre: TestCachedBeaconStateAllForks, - post: if (valid) BeaconStateAllForks else void, + // a null post state means the test is expected to fail + post: ?BeaconStateAllForks, op: OpType.Type, bls_setting: BlsSetting, @@ -122,9 +123,15 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime errdefer tc.pre.deinit(); - // init the post state if this is a "valid" test case - - if (valid) { + tc.post = null; + const post_exist = if (dir.statFile("post.ssz_snappy")) |_| true else |err| blk: { + if (err == error.FileNotFound) { + break :blk false; + } else { + return err; + } + }; + if (post_exist) { const post_state = try allocator.create(ForkTypes.BeaconState.Type); errdefer { ForkTypes.BeaconState.deinit(allocator, post_state); @@ -142,8 +149,8 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime OpType.deinit(self.pre.allocator, &self.op); } self.pre.deinit(); - if (valid) { - self.post.deinit(self.pre.allocator); + if (self.post) |*post| { + post.deinit(self.pre.allocator); } } @@ -190,7 +197,7 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime self.pre.allocator, self.pre.cached_state, .{ .regular = @unionInit(state_transition.BeaconBlockBody, @tagName(fork), &self.op) }, - .{ .data_availability_status = .available, .execution_payload_status = if (valid) .valid else .invalid }, + .{ .data_availability_status = .available, .execution_payload_status = if (self.post != null) .valid else .invalid }, ); }, .proposer_slashing => { @@ -229,9 +236,9 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime } pub fn runTest(self: *Self) !void { - if (valid) { + if (self.post) |post| { try self.process(); - try expectEqualBeaconStates(self.post, self.pre.cached_state.state.*); + try expectEqualBeaconStates(post, self.pre.cached_state.state.*); } else { self.process() catch |err| { if (err == error.SkipZigTest) { diff --git a/test/spec/writer/Operations.zig b/test/spec/writer/Operations.zig index e334a8e09..41889f25f 100644 --- a/test/spec/writer/Operations.zig +++ b/test/spec/writer/Operations.zig @@ -31,7 +31,7 @@ const test_template = \\ defer allocator.free(test_dir_name); \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; \\ - \\ try Operations.TestCase(.{s}, .{s}, {}).execute(allocator, test_dir); + \\ try Operations.TestCase(.{s}, .{s}).execute(allocator, test_dir); \\}} \\ \\ @@ -47,7 +47,6 @@ pub fn writeTest( handler: Handler, test_case_name: []const u8, ) !void { - const valid = !std.mem.startsWith(u8, test_case_name, "invalid"); try writer.print(test_template, .{ @tagName(fork), @tagName(handler), @@ -59,6 +58,5 @@ pub fn writeTest( @tagName(fork), @tagName(handler), - valid, }); } From dc05a5e0ddc42bdb9232e74b61aa7ea51f4b7609 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 27 Oct 2025 10:40:15 +0700 Subject: [PATCH 22/72] fix: electra processWithdrawalRequest() validator ptr --- src/state_transition/block/process_withdrawal_request.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/state_transition/block/process_withdrawal_request.zig b/src/state_transition/block/process_withdrawal_request.zig index 83191f744..c9e8f1878 100644 --- a/src/state_transition/block/process_withdrawal_request.zig +++ b/src/state_transition/block/process_withdrawal_request.zig @@ -37,7 +37,7 @@ pub fn processWithdrawalRequest(allocator: std.mem.Allocator, cached_state: *Cac // note that we don't need to check for 6110 unfinalized vals as they won't be eligible for withdraw/exit anyway const validator_index = pubkey_to_index.get(&withdrawal_request.validator_pubkey) orelse return; - var validator = validators.items[validator_index]; + const validator = &validators.items[validator_index]; if (!isValidatorEligibleForWithdrawOrExit(validator, &withdrawal_request.source_address, cached_state)) { return; } @@ -49,7 +49,7 @@ pub fn processWithdrawalRequest(allocator: std.mem.Allocator, cached_state: *Cac if (is_full_exit_request) { // only exit validator if it has no pending withdrawals in the queue if (pending_balance_to_withdraw == 0) { - try initiateValidatorExit(cached_state, &validator); + try initiateValidatorExit(cached_state, validator); } return; } @@ -76,7 +76,7 @@ pub fn processWithdrawalRequest(allocator: std.mem.Allocator, cached_state: *Cac } } -fn isValidatorEligibleForWithdrawOrExit(validator: Validator, source_address: []const u8, cached_state: *const CachedBeaconStateAllForks) bool { +fn isValidatorEligibleForWithdrawOrExit(validator: *const Validator, source_address: []const u8, cached_state: *const CachedBeaconStateAllForks) bool { const withdrawal_credentials = validator.withdrawal_credentials; const address = withdrawal_credentials[12..]; const epoch_cache = cached_state.getEpochCache(); @@ -85,7 +85,7 @@ fn isValidatorEligibleForWithdrawOrExit(validator: Validator, source_address: [] return (hasExecutionWithdrawalCredential(withdrawal_credentials) and std.mem.eql(u8, address, source_address) and - isActiveValidator(&validator, current_epoch) and + isActiveValidator(validator, current_epoch) and validator.exit_epoch == c.FAR_FUTURE_EPOCH and current_epoch >= validator.activation_epoch + config.chain.SHARD_COMMITTEE_PERIOD); } From 3cd28bfc1379d95bf24023a8008773860d8f3e73 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 27 Oct 2025 13:22:16 +0700 Subject: [PATCH 23/72] fix: Sanity test memory leak and remove .valid --- test/spec/runner/Sanity.zig | 48 +++++++++++++++++++++++++------------ test/spec/writer/Sanity.zig | 3 +-- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig index 4852f0c9a..3de22f16e 100644 --- a/test/spec/runner/Sanity.zig +++ b/test/spec/runner/Sanity.zig @@ -55,15 +55,20 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { // Load pre state const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + var transfered_pre_state: bool = false; errdefer { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); + if (!transfered_pre_state) { + ForkTypes.BeaconState.deinit(allocator, pre_state); + allocator.destroy(pre_state); + } } pre_state.* = ForkTypes.BeaconState.default_value; try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); + transfered_pre_state = true; var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); + errdefer tc.pre.deinit(); // Load post state const post_state = try allocator.create(ForkTypes.BeaconState.Type); @@ -99,13 +104,14 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { }; } -pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { +pub fn BlocksTestCase(comptime fork: ForkSeq) type { const ForkTypes = @field(ssz, fork.forkName()); const SignedBeaconBlock = @field(ForkTypes, "SignedBeaconBlock"); return struct { pre: TestCachedBeaconStateAllForks, - post: if (valid) BeaconStateAllForks else void, + // a null post state means the test is expected to fail + post: ?BeaconStateAllForks, blocks: []SignedBeaconBlock.Type, const Self = @This(); @@ -138,15 +144,20 @@ pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { // Load pre state const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + var transfered_pre_state: bool = false; errdefer { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); + if (!transfered_pre_state) { + ForkTypes.BeaconState.deinit(allocator, pre_state); + allocator.destroy(pre_state); + } } pre_state.* = ForkTypes.BeaconState.default_value; try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); + transfered_pre_state = true; var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); + errdefer tc.pre.deinit(); // Load blocks tc.blocks = try allocator.alloc(SignedBeaconBlock.Type, blocks_count); @@ -158,13 +169,20 @@ pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { } for (tc.blocks, 0..) |*block, i| { block.* = SignedBeaconBlock.default_value; - const block_filename = try std.fmt.allocPrint(allocator, "block_{d}.ssz_snappy", .{i}); + const block_filename = try std.fmt.allocPrint(allocator, "blocks_{d}.ssz_snappy", .{i}); defer allocator.free(block_filename); try loadSszValue(SignedBeaconBlock, allocator, dir, block_filename, block); } - // Load post state if valid - if (valid) { + tc.post = null; + const post_exist = if (dir.statFile("post.ssz_snappy")) |_| true else |err| blk: { + if (err == error.FileNotFound) { + break :blk false; + } else { + return err; + } + }; + if (post_exist) { const post_state = try allocator.create(ForkTypes.BeaconState.Type); errdefer { ForkTypes.BeaconState.deinit(allocator, post_state); @@ -186,8 +204,8 @@ pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { } self.pre.allocator.free(self.blocks); self.pre.deinit(); - if (valid) { - self.post.deinit(self.pre.allocator); + if (self.post) |*post| { + post.deinit(self.pre.allocator); } } @@ -208,9 +226,9 @@ pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { } pub fn runTest(self: *Self) !void { - if (valid) { + if (self.post) |post| { try self.process(); - try expectEqualBeaconStates(self.post, self.pre.cached_state.state.*); + try expectEqualBeaconStates(post, self.pre.cached_state.state.*); } else { self.process() catch |err| { if (err == error.SkipZigTest) { diff --git a/test/spec/writer/Sanity.zig b/test/spec/writer/Sanity.zig index 0bf44d709..7c6ea4d46 100644 --- a/test/spec/writer/Sanity.zig +++ b/test/spec/writer/Sanity.zig @@ -47,10 +47,9 @@ pub fn writeTest( handler: Handler, test_case_name: []const u8, ) !void { - const valid = !std.mem.startsWith(u8, test_case_name, "invalid"); const execute_call = switch (handler) { .slots => std.fmt.allocPrint(std.heap.page_allocator, "try Sanity.SlotsTestCase(.{s}).execute(allocator, test_dir);", .{@tagName(fork)}) catch unreachable, - .blocks => std.fmt.allocPrint(std.heap.page_allocator, "try Sanity.BlocksTestCase(.{s}, {}).execute(allocator, test_dir);", .{ @tagName(fork), valid }) catch unreachable, + .blocks => std.fmt.allocPrint(std.heap.page_allocator, "try Sanity.BlocksTestCase(.{s}).execute(allocator, test_dir);", .{@tagName(fork)}) catch unreachable, }; defer std.heap.page_allocator.free(execute_call); try writer.print(test_template, .{ From a67e15fd4676e52878dd9490383684c7dcbfc374 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 27 Oct 2025 13:47:05 +0700 Subject: [PATCH 24/72] fix: only upgrade state if fork transition --- src/state_transition/state_transition.zig | 24 ++++++++++++++------- src/state_transition/types/beacon_state.zig | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index 414d2c716..f081e640a 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -59,11 +59,18 @@ pub fn processSlotsWithTransientCache( const validator_count = post_state.epoch_cache_ref.get().current_shuffling.get().active_indices.len; - // TODO: do not always allocate - var reused_epoch_transition_cache = try ReusedEpochTransitionCache.init(allocator, validator_count); - defer reused_epoch_transition_cache.deinit(); + const post_epoch = computeEpochAtSlot(slot); + const run_epoch_transition = post_epoch > post_state.getEpochCache().epoch; + var reused_epoch_transition_cache = if (run_epoch_transition) try ReusedEpochTransitionCache.init(allocator, validator_count) else null; var epoch_transition_cache: EpochTransitionCache = undefined; - defer epoch_transition_cache.deinit(); + defer { + if (reused_epoch_transition_cache) |*rec| { + rec.deinit(); + } + if (run_epoch_transition) { + epoch_transition_cache.deinit(); + } + } while (cached_state.slot() < slot) { try processSlot(allocator, post_state); @@ -73,7 +80,8 @@ pub fn processSlotsWithTransientCache( // const epochTransitionTimer = metrics?.epochTransitionTime.startTimer(); // TODO(bing): metrics: time beforeProcessEpoch - try EpochTransitionCache.beforeProcessEpoch(allocator, post_state, &reused_epoch_transition_cache, &epoch_transition_cache); + std.debug.assert(reused_epoch_transition_cache != null); + try EpochTransitionCache.beforeProcessEpoch(allocator, post_state, &reused_epoch_transition_cache.?, &epoch_transition_cache); try processEpoch(allocator, post_state, &epoch_transition_cache); // TODO(bing): registerValidatorStatuses @@ -89,9 +97,9 @@ pub fn processSlotsWithTransientCache( //epochTransitionTimer const state_epoch = computeEpochAtSlot(cached_state.slot()); - for (post_state.config.forks_descending_epoch_order) |f| { - if (state_epoch == f.epoch) { - _ = try post_state.state.upgrade(allocator); + inline for (post_state.config.forks_descending_epoch_order) |f| { + if (post_state.state.forkSeq().lt(f.fork_seq) and state_epoch == f.epoch) { + _ = try post_state.state.upgradeUnsafe(allocator); break; // no need to check all forks once one hits } } diff --git a/src/state_transition/types/beacon_state.zig b/src/state_transition/types/beacon_state.zig index 96b17de5f..9a99bd64e 100644 --- a/src/state_transition/types/beacon_state.zig +++ b/src/state_transition/types/beacon_state.zig @@ -642,8 +642,8 @@ pub const BeaconStateAllForks = union(enum) { /// Allocates a new `state` of the next fork, clones all fields of the current `state` to it and assigns `self` to it. /// Destroys the old `state`. /// - /// Caller must free upgraded state. - pub fn upgrade(self: *BeaconStateAllForks, allocator: std.mem.Allocator) !*BeaconStateAllForks { + /// Caller must make sure an upgrade is needed by checking BeaconConfig then free upgraded state. + pub fn upgradeUnsafe(self: *BeaconStateAllForks, allocator: std.mem.Allocator) !*BeaconStateAllForks { switch (self.*) { .phase0 => |state| { self.* = .{ From a65a7b02658129ddd9ec8ee243f815449887ba70 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 27 Oct 2025 15:01:24 +0700 Subject: [PATCH 25/72] fix: EffectiveBalanceIncrements usage --- src/state_transition/cache/epoch_cache.zig | 4 ++-- src/state_transition/epoch/get_attestation_deltas.zig | 3 ++- src/state_transition/epoch/process_epoch.zig | 4 +++- src/state_transition/slot/process_slot.zig | 6 +++--- src/state_transition/state_transition.zig | 6 +++--- src/state_transition/utils/seed.zig | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index b86b9c00c..0623512ff 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -397,8 +397,8 @@ pub const EpochCache = struct { } /// Utility method to return SyncCommitteeCache so that consumers don't have to deal with ".get()" call - pub fn getEffectiveBalanceIncrements(self: *const EpochCache) *const EffectiveBalanceIncrements { - return &self.effective_balance_increment.get(); + pub fn getEffectiveBalanceIncrements(self: *const EpochCache) EffectiveBalanceIncrements { + return self.effective_balance_increment.get(); } pub fn afterProcessEpoch(self: *EpochCache, cached_state: *const CachedBeaconStateAllForks, epoch_transition_cache: *const EpochTransitionCache) !void { diff --git a/src/state_transition/epoch/get_attestation_deltas.zig b/src/state_transition/epoch/get_attestation_deltas.zig index 35d91aba1..525b45f28 100644 --- a/src/state_transition/epoch/get_attestation_deltas.zig +++ b/src/state_transition/epoch/get_attestation_deltas.zig @@ -77,7 +77,8 @@ pub fn getAttestationDeltas(allocator: Allocator, cached_state: *const CachedBea defer reward_penalty_item_cache.deinit(); const effective_balance_increments = epoch_cache.getEffectiveBalanceIncrements(); - std.debug.assert(flags.len == effective_balance_increments.items.len); + std.debug.assert(flags.len == state.validators().items.len); + std.debug.assert(flags.len <= effective_balance_increments.items.len); for (0..flags.len) |i| { const flag = flags[i]; const effective_balance_increment = effective_balance_increments.items[i]; diff --git a/src/state_transition/epoch/process_epoch.zig b/src/state_transition/epoch/process_epoch.zig index 0b4bf442d..df88d497e 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -60,7 +60,9 @@ pub fn processEpoch(allocator: std.mem.Allocator, cached_state: *CachedBeaconSta try processParticipationFlagUpdates(cached_state, allocator); } - try processSyncCommitteeUpdates(allocator, cached_state); + if (state.isPostAltair()) { + try processSyncCommitteeUpdates(allocator, cached_state); + } // TODO(fulu) // processProposerLookahead(fork, state); diff --git a/src/state_transition/slot/process_slot.zig b/src/state_transition/slot/process_slot.zig index fc684f347..c3e2ae755 100644 --- a/src/state_transition/slot/process_slot.zig +++ b/src/state_transition/slot/process_slot.zig @@ -17,13 +17,13 @@ pub fn processSlot(allocator: Allocator, cached_state: *CachedBeaconStateAllFork // Cache latest block header state root var latest_block_header = state.latestBlockHeader(); - if (!std.mem.eql(u8, &latest_block_header.state_root, &ZERO_HASH)) { + if (std.mem.eql(u8, &latest_block_header.state_root, &ZERO_HASH)) { latest_block_header.state_root = previous_state_root; } // Cache block root var previous_block_root: Root = undefined; try ssz.phase0.BeaconBlockHeader.hashTreeRoot(latest_block_header, &previous_block_root); - const state_block_roots = state.blockRoots(); - @memcpy(state_block_roots[state.slot() % preset.SLOTS_PER_HISTORICAL_ROOT][0..], previous_block_root[0..]); + const block_roots = state.blockRoots(); + @memcpy(block_roots[state.slot() % preset.SLOTS_PER_HISTORICAL_ROOT][0..], previous_block_root[0..]); } diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index f081e640a..852e2c197 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -172,18 +172,18 @@ pub fn stateTransition( // Verify state root if (opts.verify_state_root) { - var out: [32]u8 = undefined; + var post_state_root: [32]u8 = undefined; // const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ // source: StateHashTreeRootSource.stateTransition, // }); - try post_state.state.hashTreeRoot(allocator, &out); + try post_state.state.hashTreeRoot(allocator, &post_state_root); // hashTreeRootTimer?.(); const block_state_root = switch (block) { .regular => |b| b.stateRoot(), .blinded => |b| b.stateRoot(), }; - if (!std.mem.eql(u8, &out, &block_state_root)) { + if (!std.mem.eql(u8, &post_state_root, &block_state_root)) { return error.InvalidStateRoot; } } diff --git a/src/state_transition/utils/seed.zig b/src/state_transition/utils/seed.zig index 612daaa3b..ce880e872 100644 --- a/src/state_transition/utils/seed.zig +++ b/src/state_transition/utils/seed.zig @@ -53,7 +53,7 @@ test "computeProposers - sanity" { try computeProposers(allocator, ForkSeq.electra, epoch_seed, 0, active_indices[0..], effective_balance_increments, &out); } -pub fn getNextSyncCommitteeIndices(allocator: Allocator, state: *const BeaconStateAllForks, active_indices: []const ValidatorIndex, effective_balance_increments: *const EffectiveBalanceIncrements, out: []ValidatorIndex) !void { +pub fn getNextSyncCommitteeIndices(allocator: Allocator, state: *const BeaconStateAllForks, active_indices: []const ValidatorIndex, effective_balance_increments: EffectiveBalanceIncrements, out: []ValidatorIndex) !void { const rand_byte_count: ByteCount = if (state.isPostElectra()) ByteCount.Two else ByteCount.One; const max_effective_balance: u64 = if (state.isPostElectra()) preset.MAX_EFFECTIVE_BALANCE_ELECTRA else preset.MAX_EFFECTIVE_BALANCE; From 76551f38cdf0a8f6d699784d80590bf9b9c4224a Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 09:39:28 +0700 Subject: [PATCH 26/72] fix: invalid state root due to processBlockHeader() --- src/state_transition/block/process_block_header.zig | 2 +- src/state_transition/types/signed_block.zig | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/state_transition/block/process_block_header.zig b/src/state_transition/block/process_block_header.zig index 710e3ebf9..a5f51fe42 100644 --- a/src/state_transition/block/process_block_header.zig +++ b/src/state_transition/block/process_block_header.zig @@ -37,7 +37,7 @@ pub fn processBlockHeader(allocator: Allocator, cached_state: *const CachedBeaco return error.BlockParentRootMismatch; } var body_root: [32]u8 = undefined; - try block.hashTreeRoot(allocator, &body_root); + try block.beaconBlockBody().hashTreeRoot(allocator, &body_root); // cache current block as the new latest block const state_latest_block_header = state.latestBlockHeader(); const latest_block_header: BeaconBlockHeader = .{ diff --git a/src/state_transition/types/signed_block.zig b/src/state_transition/types/signed_block.zig index 840d5fa43..f2494dd6a 100644 --- a/src/state_transition/types/signed_block.zig +++ b/src/state_transition/types/signed_block.zig @@ -85,6 +85,12 @@ pub const SignedBlock = union(enum) { inline .regular, .blinded => |b| b.blsToExecutionChanges(), }; } + + pub fn hashTreeRoot(self: *const Body, allocator: std.mem.Allocator, out: *Root) !void { + return switch (self.*) { + inline .regular, .blinded => |b| b.hashTreeRoot(allocator, out), + }; + } }; pub fn message(self: *const SignedBlock) Block { From aa304e7014a9e064786b776029e247f9ee969e20 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 09:59:24 +0700 Subject: [PATCH 27/72] fix: sanity BlocksTestCase deinit prestate inside blocks loop --- test/spec/runner/Sanity.zig | 46 +++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig index 3de22f16e..b08f9ccbb 100644 --- a/test/spec/runner/Sanity.zig +++ b/test/spec/runner/Sanity.zig @@ -5,6 +5,7 @@ const Preset = @import("preset").Preset; const state_transition = @import("state_transition"); const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; const BeaconStateAllForks = state_transition.BeaconStateAllForks; +const CachedBeaconStateAllForks = state_transition.CachedBeaconStateAllForks; const test_case = @import("../test_case.zig"); const loadSszValue = test_case.loadSszSnappyValue; const expectEqualBeaconStates = test_case.expectEqualBeaconStates; @@ -209,28 +210,43 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { } } - pub fn process(self: *Self) !void { - var state = self.pre.cached_state; - for (self.blocks) |*block| { + pub fn process(self: *Self) !*CachedBeaconStateAllForks { + var post_state: *CachedBeaconStateAllForks = self.pre.cached_state; + for (self.blocks, 0..) |*block, i| { const signed_block = @unionInit(state_transition.SignedBeaconBlock, @tagName(fork), block); - state = try state_transition.state_transition.stateTransition( - self.pre.allocator, - state, - .{ - .regular = &signed_block, - }, - .{}, - ); + { + const new_post_state = try state_transition.state_transition.stateTransition( + self.pre.allocator, + post_state, + .{ + .regular = &signed_block, + }, + .{}, + ); + + // don't deinit the initial pre state, we do it in deinit() + const to_destroy = if (i > 0) post_state else null; + post_state = new_post_state; + if (to_destroy) |state| { + state.deinit(); + self.pre.allocator.destroy(state); + } + } } - self.pre.cached_state = state; + + return post_state; } pub fn runTest(self: *Self) !void { if (self.post) |post| { - try self.process(); - try expectEqualBeaconStates(post, self.pre.cached_state.state.*); + const actual = try self.process(); + try expectEqualBeaconStates(post, actual.state.*); + defer { + actual.deinit(); + self.pre.allocator.destroy(actual); + } } else { - self.process() catch |err| { + _ = self.process() catch |err| { if (err == error.SkipZigTest) { return err; } From eb1b956739c68516df145249f5f07588f1d1b381 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 10:49:27 +0700 Subject: [PATCH 28/72] fix: load slots.yaml --- test/spec/runner/Sanity.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig index b08f9ccbb..2495bdd85 100644 --- a/test/spec/runner/Sanity.zig +++ b/test/spec/runner/Sanity.zig @@ -52,7 +52,7 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { const slots_content = try slots_file.readToEndAlloc(allocator, 1024); defer allocator.free(slots_content); // Parse YAML for slots (simplified; assume single value) - tc.slots = std.fmt.parseInt(u64, std.mem.trim(u8, slots_content, " \n"), 10) catch 0; + tc.slots = std.fmt.parseInt(u64, std.mem.trim(u8, slots_content, "... \n"), 10) catch 0; // Load pre state const pre_state = try allocator.create(ForkTypes.BeaconState.Type); From e096549a3fcf56d583e99e17ef51b6b1bc942bea Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 13:22:23 +0700 Subject: [PATCH 29/72] fix: use validator ptr where suitable --- src/state_transition/block/slash_validator.zig | 7 ++----- .../epoch/process_effective_balance_updates.zig | 2 +- src/state_transition/epoch/process_registry_updates.zig | 7 +++---- src/state_transition/state_transition.zig | 2 ++ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/state_transition/block/slash_validator.zig b/src/state_transition/block/slash_validator.zig index 63c7f53ee..14dc64268 100644 --- a/src/state_transition/block/slash_validator.zig +++ b/src/state_transition/block/slash_validator.zig @@ -21,17 +21,14 @@ pub fn slashValidator( const epoch = epoch_cache.epoch; const effective_balance_increments = epoch_cache.effective_balance_increment; - var validator = state.validators().items[slashed_index]; + var validator = &state.validators().items[slashed_index]; // TODO: Bellatrix initiateValidatorExit validators.update() with the one below - try initiateValidatorExit(cached_state, &validator); + try initiateValidatorExit(cached_state, validator); validator.slashed = true; validator.withdrawable_epoch = @max(validator.withdrawable_epoch, epoch + preset.EPOCHS_PER_SLASHINGS_VECTOR); - const validators = state.validators(); - validators.items[slashed_index] = validator; - const effective_balance = validator.effective_balance; // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: diff --git a/src/state_transition/epoch/process_effective_balance_updates.zig b/src/state_transition/epoch/process_effective_balance_updates.zig index 28f37d11d..f9b7dcb7a 100644 --- a/src/state_transition/epoch/process_effective_balance_updates.zig +++ b/src/state_transition/epoch/process_effective_balance_updates.zig @@ -51,7 +51,7 @@ pub fn processEffectiveBalanceUpdates(cached_state: *CachedBeaconStateAllForks, { // Update the state tree // Should happen rarely, so it's fine to update the tree - var validator = validators.items[i]; + var validator = &validators.items[i]; effective_balance = @min( balance - (balance % preset.EFFECTIVE_BALANCE_INCREMENT), effective_balance_limit, diff --git a/src/state_transition/epoch/process_registry_updates.zig b/src/state_transition/epoch/process_registry_updates.zig index 08e6227a7..ebbd81f9a 100644 --- a/src/state_transition/epoch/process_registry_updates.zig +++ b/src/state_transition/epoch/process_registry_updates.zig @@ -19,9 +19,8 @@ pub fn processRegistryUpdates(cached_state: *CachedBeaconStateAllForks, cache: * for (cache.indices_to_eject.items) |index| { // set validator exit epoch and withdrawable epoch // TODO: Figure out a way to quickly set properties on the validators tree - var validator = validators.items[index]; - try initiateValidatorExit(cached_state, &validator); - validators.items[index] = validator; + const validator = &validators.items[index]; + try initiateValidatorExit(cached_state, validator); } // set new activation eligibilities @@ -36,7 +35,7 @@ pub fn processRegistryUpdates(cached_state: *CachedBeaconStateAllForks, cache: * // dequeue validators for activation up to churn limit for (0..len) |i| { const validator_index = cache.indices_eligible_for_activation.items[i]; - var validator = validators.items[validator_index]; + const validator = &validators.items[validator_index]; // placement in queue is finalized if (validator.activation_eligibility_epoch > finality_epoch) { // remaining validators all have an activationEligibilityEpoch that is higher anyway, break early diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index 852e2c197..cdb677667 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -90,6 +90,8 @@ pub fn processSlotsWithTransientCache( try post_state.epoch_cache_ref.get().afterProcessEpoch(post_state, &epoch_transition_cache); // post_state.commit + var root: Root = undefined; + try cached_state.hashTreeRoot(allocator, &root); } else { cached_state.slotPtr().* += 1; } From 3f6c9a56d38eb2414296247d1ae273fb2d4bd6a2 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 13:42:18 +0700 Subject: [PATCH 30/72] fix: BeaconState.rotateEpochParticipations() --- src/state_transition/types/beacon_state.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/types/beacon_state.zig b/src/state_transition/types/beacon_state.zig index 9a99bd64e..20049ead9 100644 --- a/src/state_transition/types/beacon_state.zig +++ b/src/state_transition/types/beacon_state.zig @@ -458,7 +458,7 @@ pub const BeaconStateAllForks = union(enum) { inline else => |state| { state.previous_epoch_participation.clearRetainingCapacity(); try state.previous_epoch_participation.appendSlice(allocator, state.current_epoch_participation.items); - state.current_epoch_participation.clearRetainingCapacity(); + @memset(state.current_epoch_participation.items, 0); }, } } From 842c886d5a571916f0b62fbc193eaec0e87d54ea Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 14:55:06 +0700 Subject: [PATCH 31/72] fix: processEffectiveBalanceUpdates integer underflow --- .../epoch/process_effective_balance_updates.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/state_transition/epoch/process_effective_balance_updates.zig b/src/state_transition/epoch/process_effective_balance_updates.zig index f9b7dcb7a..e8f699d50 100644 --- a/src/state_transition/epoch/process_effective_balance_updates.zig +++ b/src/state_transition/epoch/process_effective_balance_updates.zig @@ -63,19 +63,18 @@ pub fn processEffectiveBalanceUpdates(cached_state: *CachedBeaconStateAllForks, // TODO: describe issue. Compute progressive target balances // Must update target balances for consistency, see comments below if (state.isPostAltair()) { - const delta_effective_balance_increment = new_effective_balance_increment - effective_balance_increment; const previous_epoch_participation = state.previousEpochParticipations().items; const current_epoch_participation = state.currentEpochParticipations().items; if (!validator.slashed) { if (previous_epoch_participation[i] & TIMELY_TARGET == TIMELY_TARGET) { - epoch_cache.previous_target_unslashed_balance_increments += delta_effective_balance_increment; + epoch_cache.previous_target_unslashed_balance_increments += new_effective_balance_increment - effective_balance_increment; } // currentTargetUnslashedBalanceIncrements is transfered to previousTargetUnslashedBalanceIncrements in afterEpochTransitionCache // at epoch transition of next epoch (in EpochTransitionCache), prevTargetUnslStake is calculated based on newEffectiveBalanceIncrement if (current_epoch_participation[i] & TIMELY_TARGET == TIMELY_TARGET) { - epoch_cache.current_target_unslashed_balance_increments += delta_effective_balance_increment; + epoch_cache.current_target_unslashed_balance_increments += new_effective_balance_increment - effective_balance_increment; } } } From 9bf59f3b63ff828de50e2291cd8f305a7e11c94b Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 15:18:24 +0700 Subject: [PATCH 32/72] fix: EpochCache afterProcessEpoch epoch_after_upcoming --- src/state_transition/cache/epoch_cache.zig | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index 0623512ff..e99ced37d 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -133,8 +133,6 @@ pub const EpochCache = struct { epoch: Epoch, - next_epoch: Epoch, - pub fn createFromState(allocator: Allocator, state: *const BeaconStateAllForks, immutable_data: EpochCacheImmutableData, option: ?EpochCacheOpts) !*EpochCache { const config = immutable_data.config; const pubkey_to_index = immutable_data.pubkey_to_index; @@ -312,7 +310,6 @@ pub const EpochCache = struct { .next_sync_committee_indexed = try SyncCommitteeCacheRc.init(allocator, next_sync_committee_indexed), .sync_period = computeSyncPeriodAtEpoch(current_epoch), .epoch = current_epoch, - .next_epoch = next_epoch, }; return epoch_cache_ptr; @@ -368,7 +365,6 @@ pub const EpochCache = struct { .next_sync_committee_indexed = self.next_sync_committee_indexed.acquire(), .sync_period = self.sync_period, .epoch = self.epoch, - .next_epoch = self.next_epoch, }; const epoch_cache_ptr = try allocator.create(EpochCache); @@ -403,7 +399,8 @@ pub const EpochCache = struct { pub fn afterProcessEpoch(self: *EpochCache, cached_state: *const CachedBeaconStateAllForks, epoch_transition_cache: *const EpochTransitionCache) !void { const state = cached_state.state; - const upcoming_epoch = self.next_epoch; + const upcoming_epoch = self.epoch + 1; + const epoch_after_upcoming = upcoming_epoch + 1; // move current to previous self.previous_shuffling.release(); @@ -417,7 +414,7 @@ pub const EpochCache = struct { self.allocator, state, next_shuffling_active_indices, - upcoming_epoch, + epoch_after_upcoming, ); self.next_shuffling = try EpochShufflingRc.init(self.allocator, next_shuffling); @@ -625,7 +622,7 @@ pub const EpochCache = struct { const previous_epoch = if (self.epoch == GENESIS_EPOCH) GENESIS_EPOCH else self.epoch - 1; const shuffling = if (epoch == previous_epoch) self.getPreviousShuffling() - else if (epoch == self.epoch) self.getCurrentShuffling() else if (epoch == self.next_epoch) + else if (epoch == self.epoch) self.getCurrentShuffling() else if (epoch == self.epoch + 1) self.getNextEpochShuffling() else null; From 4815ffe30b1fddea3fb186be87f0763239c2a476 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 16:05:27 +0700 Subject: [PATCH 33/72] fix: test:state_transition --- src/state_transition/test_utils/generate_state.zig | 2 +- src/state_transition/types/beacon_state.zig | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index 402c860cd..cb737d4f3 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -123,7 +123,7 @@ pub fn generateElectraState(allocator: Allocator, chain_config: ChainConfig, val beacon_state.* = .{ .electra = electra_state }; const validators = beacon_state.validators(); var next_sync_committee_indices: [preset.SYNC_COMMITTEE_SIZE]ValidatorIndex = undefined; - try getNextSyncCommitteeIndices(allocator, beacon_state, active_validator_indices.items, &effective_balance_increments, &next_sync_committee_indices); + try getNextSyncCommitteeIndices(allocator, beacon_state, active_validator_indices.items, effective_balance_increments, &next_sync_committee_indices); var next_sync_committee_pubkeys: [preset.SYNC_COMMITTEE_SIZE]BLSPubkey = undefined; var next_sync_committee_pubkeys_slices: [preset.SYNC_COMMITTEE_SIZE]blst.PublicKey = undefined; diff --git a/src/state_transition/types/beacon_state.zig b/src/state_transition/types/beacon_state.zig index 20049ead9..0eb21c8e5 100644 --- a/src/state_transition/types/beacon_state.zig +++ b/src/state_transition/types/beacon_state.zig @@ -757,9 +757,9 @@ test "upgrade state - sanity" { phase0_state.* = ssz.phase0.BeaconState.default_value; var phase0 = BeaconStateAllForks{ .phase0 = phase0_state }; - var altair = try phase0.upgrade(allocator); - const bellatrix = try altair.upgrade(allocator); - const capella = try bellatrix.upgrade(allocator); - var deneb = try capella.upgrade(allocator); + var altair = try phase0.upgradeUnsafe(allocator); + const bellatrix = try altair.upgradeUnsafe(allocator); + const capella = try bellatrix.upgradeUnsafe(allocator); + var deneb = try capella.upgradeUnsafe(allocator); defer deneb.deinit(allocator); } From 81679a6d6ce7dbacf21542194cc832b89db23114 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 28 Oct 2025 17:22:08 +0700 Subject: [PATCH 34/72] fix: parent root in electra block int test --- src/state_transition/test_utils/generate_block.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/test_utils/generate_block.zig b/src/state_transition/test_utils/generate_block.zig index 20d860e5b..45e749e13 100644 --- a/src/state_transition/test_utils/generate_block.zig +++ b/src/state_transition/test_utils/generate_block.zig @@ -67,7 +67,7 @@ pub fn generateElectraBlock(allocator: Allocator, cached_state: *const CachedBea .slot = state.slot() + 1, // value is generated after running real state transition int test .proposer_index = 41, - .parent_root = try hex.hexToRoot("0x0833505580088dab43dab615abbdaa7c914a5f4ebeca79332a9373d5b25daeac"), + .parent_root = try hex.hexToRoot("0x4e647394b6f96c1cd44938483ddf14d89b35d3f67586a59cbfd410a56efbb2b1"), // this could be computed later .state_root = [_]u8{0} ** 32, .body = .{ From 0c194f190ee1b59d2d02deed5644f8422427c96b Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 29 Oct 2025 10:19:33 +0700 Subject: [PATCH 35/72] fix: processPendingDeposit pending_deposit sliceFrom() --- .../process_effective_balance_updates.zig | 3 ++- .../epoch/process_pending_deposits.zig | 24 ++++++++++--------- src/state_transition/state_transition.zig | 1 + 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/state_transition/epoch/process_effective_balance_updates.zig b/src/state_transition/epoch/process_effective_balance_updates.zig index e8f699d50..9ce53b28b 100644 --- a/src/state_transition/epoch/process_effective_balance_updates.zig +++ b/src/state_transition/epoch/process_effective_balance_updates.zig @@ -85,7 +85,8 @@ pub fn processEffectiveBalanceUpdates(cached_state: *CachedBeaconStateAllForks, } // TODO: Do this in afterEpochTransitionCache, looping a Uint8Array should be very cheap - if (cache.is_active_next_epoch[i]) { + // post-electra we may add new validator to registry in processPendingDeposits() + if (i < cache.is_active_next_epoch.len and cache.is_active_next_epoch[i]) { // We track nextEpochTotalActiveBalanceByIncrement as ETH to fit total network balance in a JS number (53 bits) next_epoch_total_active_balance_by_increment += effective_balance_increment; } diff --git a/src/state_transition/epoch/process_pending_deposits.zig b/src/state_transition/epoch/process_pending_deposits.zig index d5fd4c688..b56f16871 100644 --- a/src/state_transition/epoch/process_pending_deposits.zig +++ b/src/state_transition/epoch/process_pending_deposits.zig @@ -93,8 +93,9 @@ pub fn processPendingDeposits(allocator: Allocator, cached_state: *CachedBeaconS } if (next_deposit_index > 0) { - try pending_deposits.resize(allocator, pending_deposits_len - next_deposit_index); - @memcpy(pending_deposits.items[0..], pending_deposits.items[next_deposit_index..]); + const new_len = pending_deposits_len - next_deposit_index; + @memcpy(pending_deposits.items[0..new_len], pending_deposits.items[next_deposit_index..]); + try pending_deposits.resize(allocator, new_len); } // TODO: consider doing this for TreeView @@ -115,7 +116,7 @@ pub fn processPendingDeposits(allocator: Allocator, cached_state: *CachedBeaconS fn applyPendingDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAllForks, deposit: PendingDeposit, cache: *EpochTransitionCache) !void { const epoch_cache = cached_state.getEpochCache(); const state = cached_state.state; - const validator_index = epoch_cache.getValidatorIndex(&deposit.pubkey) orelse return error.ValidatorNotFound; + const validator_index = epoch_cache.getValidatorIndex(&deposit.pubkey) orelse null; const pubkey = deposit.pubkey; // TODO: is this withdrawal_credential(s) the same to spec? const withdrawal_credential = deposit.withdrawal_credentials; @@ -125,10 +126,6 @@ fn applyPendingDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAll if (!is_validator_known) { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract - if (isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { - try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credential, amount); - } - if (isValidDepositSignature(cached_state.config, pubkey, withdrawal_credential, amount, signature)) { try addValidatorToRegistry(allocator, cached_state, pubkey, withdrawal_credential, amount); try cache.is_compounding_validator_arr.append(hasCompoundingWithdrawalCredential(withdrawal_credential)); @@ -140,10 +137,15 @@ fn applyPendingDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAll } } } else { - // Increase balance - increaseBalance(state, validator_index, amount); - if (cache.balances) |*balances| { - balances.items[validator_index] += amount; + if (validator_index) |val_idx| { + // Increase balance + increaseBalance(state, val_idx, amount); + if (cache.balances) |*balances| { + balances.items[val_idx] += amount; + } + } else { + // should not happen since we checked in isValidatorKnown() above + return error.UnexpectedNullValidatorIndex; } } } diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index cdb677667..b26f39751 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -61,6 +61,7 @@ pub fn processSlotsWithTransientCache( const post_epoch = computeEpochAtSlot(slot); const run_epoch_transition = post_epoch > post_state.getEpochCache().epoch; + // TODO: should init at global level and reuse var reused_epoch_transition_cache = if (run_epoch_transition) try ReusedEpochTransitionCache.init(allocator, validator_count) else null; var epoch_transition_cache: EpochTransitionCache = undefined; defer { From 34a151dc43edde24f64dc42dd89a2cad654385c8 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 29 Oct 2025 14:00:36 +0700 Subject: [PATCH 36/72] fix: is_compounding_validator_arr double free --- .../cache/epoch_transition_cache.zig | 106 +++++++++++------- src/state_transition/root.zig | 2 +- src/state_transition/state_transition.zig | 32 ++---- test/int/cache/epoch_transition_cache.zig | 14 +-- .../process_effective_balance_updates.zig | 1 + test/int/epoch/process_epoch.zig | 1 + test/int/epoch/process_eth1_data_reset.zig | 1 + .../process_historical_summaries_update.zig | 1 + test/int/epoch/process_inactivity_updates.zig | 1 + ...process_justification_and_finalization.zig | 1 + .../process_participation_flag_updates.zig | 15 +-- .../epoch/process_pending_consolidations.zig | 1 + test/int/epoch/process_pending_deposits.zig | 1 + test/int/epoch/process_randao_mixes_reset.zig | 1 + test/int/epoch/process_registry_updates.zig | 1 + .../epoch/process_rewards_and_penalties.zig | 1 + test/int/epoch/process_slashings.zig | 1 + test/int/epoch/process_slashings_reset.zig | 1 + .../epoch/process_sync_committee_updates.zig | 15 +-- test/int/epoch/test_runner.zig | 30 +++-- test/int/state_transition.zig | 2 + 21 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/state_transition/cache/epoch_transition_cache.zig b/src/state_transition/cache/epoch_transition_cache.zig index 9cc22c055..e87283c31 100644 --- a/src/state_transition/cache/epoch_transition_cache.zig +++ b/src/state_transition/cache/epoch_transition_cache.zig @@ -41,7 +41,8 @@ const ValidatorActivation = struct { const ValidatorActivationList = std.ArrayList(ValidatorActivation); /// this is a cache that's never gc'd, it is used to store data that is reused across multiple epochs -pub const ReusedEpochTransitionCache = struct { +const ReusedEpochTransitionCache = struct { + allocator: Allocator, is_active_prev_epoch: BoolArray, is_active_current_epoch: BoolArray, is_active_next_epoch: BoolArray, @@ -62,8 +63,8 @@ pub const ReusedEpochTransitionCache = struct { penalties: U64Array, pub fn init(allocator: Allocator, validator_count: usize) !ReusedEpochTransitionCache { - // TODO: should we allocate more than validator_count? return .{ + .allocator = allocator, .is_active_prev_epoch = try BoolArray.initCapacity(allocator, validator_count), .is_active_current_epoch = try BoolArray.initCapacity(allocator, validator_count), .is_active_next_epoch = try BoolArray.initCapacity(allocator, validator_count), @@ -79,6 +80,25 @@ pub const ReusedEpochTransitionCache = struct { }; } + pub fn resize(self: *ReusedEpochTransitionCache, validator_count: usize) !void { + try self.is_active_prev_epoch.resize(validator_count); + try self.is_active_current_epoch.resize(validator_count); + try self.is_active_next_epoch.resize(validator_count); + try self.proposer_indices.resize(validator_count); + try self.inclusion_delays.resize(validator_count); + try self.flags.resize(validator_count); + try self.next_epoch_shuffling_active_validator_indices.resize(validator_count); + try self.is_compounding_validator_arr.resize(validator_count); + try self.previous_epoch_participation.resize(validator_count); + try self.current_epoch_participation.resize(validator_count); + try self.rewards.resize(validator_count); + try self.penalties.resize(validator_count); + + @memset(self.is_active_prev_epoch.items, true); + @memset(self.is_active_current_epoch.items, true); + @memset(self.is_active_next_epoch.items, true); + } + pub fn deinit(self: *ReusedEpochTransitionCache) void { self.is_active_prev_epoch.deinit(); self.is_active_current_epoch.deinit(); @@ -95,6 +115,39 @@ pub const ReusedEpochTransitionCache = struct { } }; +var _reused_cache: ?*ReusedEpochTransitionCache = null; +var _reused_lock: std.Thread.Mutex = std.Thread.Mutex{}; + +fn getReusedEpochTransitionCache(allocator: Allocator, validator_count: usize) !*ReusedEpochTransitionCache { + _reused_lock.lock(); + defer _reused_lock.unlock(); + + if (_reused_cache) |cache| { + try cache.resize(validator_count); + return cache; + } + _reused_cache = try allocator.create(ReusedEpochTransitionCache); + errdefer { + allocator.destroy(_reused_cache.?); + _reused_cache = null; + } + _reused_cache.?.* = try ReusedEpochTransitionCache.init(allocator, validator_count); + try _reused_cache.?.resize(validator_count); + return _reused_cache.?; +} + +pub fn deinitReusedEpochTransitionCache() void { + _reused_lock.lock(); + defer _reused_lock.unlock(); + + if (_reused_cache) |cache| { + const allocator = cache.allocator; + cache.deinit(); + allocator.destroy(cache); + _reused_cache = null; + } +} + pub const EpochTransitionCacheOpts = struct { /// Assert progressive balances the same in the cache. assert_correct_progressive_balances: bool = false, @@ -121,8 +174,8 @@ pub const EpochTransitionCache = struct { inclusion_delays: []const usize, // this is borrowed from ReusedEpochTransitionCache flags: []const u8, - // this is borrowed from ReusedEpochTransitionCache, we append it in processPendingDeposits() - is_compounding_validator_arr: BoolArray, + // this is borrowed from ReusedEpochTransitionCache, we append it in processPendingDeposits() so it needs to be mutable and avoid stale pointer in ReusedEpochTransitionCache.deinit() + is_compounding_validator_arr: *BoolArray, rewards: []u64, penalties: []u64, balances: ?U64Array, @@ -136,7 +189,8 @@ pub const EpochTransitionCache = struct { is_active_next_epoch: []const bool, // TODO: no need EpochTransitionCacheOpts for zig version - pub fn beforeProcessEpoch(allocator: Allocator, cached_state: *CachedBeaconStateAllForks, reused_cache: *ReusedEpochTransitionCache, out: *EpochTransitionCache) !void { + // this is the same to beforeProcessEpoch in typesript version + pub fn init(allocator: Allocator, cached_state: *CachedBeaconStateAllForks) !*EpochTransitionCache { const config = cached_state.config; var epoch_cache = cached_state.getEpochCache(); const state = cached_state.state; @@ -157,43 +211,14 @@ pub const EpochTransitionCache = struct { var total_active_stake_by_increment: u64 = 0; const validator_count = state.validators().items.len; - try reused_cache.next_epoch_shuffling_active_validator_indices.resize(validator_count); - var next_epoch_shuffling_active_indices_length: usize = 0; - // pre-fill with true (most validators are active) - try reused_cache.is_active_prev_epoch.resize(validator_count); - try reused_cache.is_active_current_epoch.resize(validator_count); - try reused_cache.is_active_next_epoch.resize(validator_count); - @memset(reused_cache.is_active_prev_epoch.items, true); - @memset(reused_cache.is_active_current_epoch.items, true); - @memset(reused_cache.is_active_next_epoch.items, true); - - // this will be populated in processRewardsAndPenalties() - try reused_cache.rewards.resize(validator_count); - try reused_cache.penalties.resize(validator_count); - - // During the epoch transition, additional data is precomputed to avoid traversing any state a second - // time. Attestations are a big part of this, and each validator has a "status" to represent its - // precomputed participation. - // - proposerIndex: number; // -1 when not included by any proposer, for phase0 only so it's declared inside phase0 block below - // - inclusionDelay: number;// for phase0 only so it's declared inside phase0 block below - // - flags: number; // bitfield of AttesterFlags - try reused_cache.flags.resize(validator_count); - // flags.fill(0); - // flags will be zero'd out below - // In the first loop, set slashed+eligibility - // In the second loop, set participation flags - // TODO: optimize by combining the two loops - // likely will require splitting into phase0 and post-phase0 versions - - if (fork_seq.isPostElectra()) { - try reused_cache.is_compounding_validator_arr.resize(validator_count); - } // Clone before being mutated in processEffectiveBalanceUpdates try epoch_cache.beforeEpochTransition(); const effective_balances_by_increments = epoch_cache.getEffectiveBalanceIncrements().items; + var next_epoch_shuffling_active_indices_length: usize = 0; + var reused_cache = try getReusedEpochTransitionCache(allocator, validator_count); for (0..validator_count) |i| { const validator = state.validators().items[i]; var flag: u8 = 0; @@ -424,7 +449,10 @@ pub const EpochTransitionCache = struct { try indices_eligible_for_activation.append(activation.validator_index); } - out.* = .{ + const epoch_transition_cache = try allocator.create(EpochTransitionCache); + errdefer allocator.destroy(epoch_transition_cache); + + epoch_transition_cache.* = .{ .prev_epoch = prev_epoch, .current_epoch = current_epoch, .total_active_stake_by_increment = total_active_stake_by_increment, @@ -446,12 +474,14 @@ pub const EpochTransitionCache = struct { .proposer_indices = reused_cache.proposer_indices.items, .inclusion_delays = reused_cache.inclusion_delays.items, .flags = reused_cache.flags.items, - .is_compounding_validator_arr = reused_cache.is_compounding_validator_arr, + .is_compounding_validator_arr = &reused_cache.is_compounding_validator_arr, .rewards = reused_cache.rewards.items, .penalties = reused_cache.penalties.items, // Will be assigned in processRewardsAndPenalties() .balances = null, }; + + return epoch_transition_cache; } pub fn deinit(self: *EpochTransitionCache) void { diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index 30b50dbf4..4a152e1ee 100644 --- a/src/state_transition/root.zig +++ b/src/state_transition/root.zig @@ -18,7 +18,6 @@ pub const committee_indices = @import("./utils/committee_indices.zig"); pub const Index2PubkeyCache = @import("./cache/pubkey_cache.zig").Index2PubkeyCache; pub const syncPubkeys = @import("./cache/pubkey_cache.zig").syncPubkeys; -pub const ReusedEpochTransitionCache = @import("./cache/epoch_transition_cache.zig").ReusedEpochTransitionCache; pub const EpochTransitionCache = @import("./cache/epoch_transition_cache.zig").EpochTransitionCache; pub const processEpoch = @import("./epoch/process_epoch.zig").processEpoch; pub const processJustificationAndFinalization = @import("./epoch/process_justification_and_finalization.zig").processJustificationAndFinalization; @@ -62,6 +61,7 @@ pub const processConsolidationRequest = @import("./block/process_consolidation_r // utils pub const getBlockRootAtSlot = @import("./utils/block_root.zig").getBlockRootAtSlot; pub const computeStartSlotAtEpoch = @import("./utils/epoch.zig").computeStartSlotAtEpoch; +pub const deinitStateTransition = @import("./state_transition.zig").deinitStateTransition; pub const WithdrawalsResult = @import("./block/process_withdrawals.zig").WithdrawalsResult; diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index b26f39751..d11ea75bf 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -30,6 +30,7 @@ const ReusedEpochTransitionCache = @import("cache/epoch_transition_cache.zig").R const processEpoch = @import("epoch/process_epoch.zig").processEpoch; const computeEpochAtSlot = @import("utils/epoch.zig").computeEpochAtSlot; const processSlot = @import("slot/process_slot.zig").processSlot; +const deinitReusedEpochTransitionCache = @import("cache/epoch_transition_cache.zig").deinitReusedEpochTransitionCache; const SignedBlock = @import("types/signed_block.zig").SignedBlock; @@ -57,22 +58,6 @@ pub fn processSlotsWithTransientCache( var cached_state = post_state.state; if (cached_state.slot() > slot) return error.outdatedSlot; - const validator_count = post_state.epoch_cache_ref.get().current_shuffling.get().active_indices.len; - - const post_epoch = computeEpochAtSlot(slot); - const run_epoch_transition = post_epoch > post_state.getEpochCache().epoch; - // TODO: should init at global level and reuse - var reused_epoch_transition_cache = if (run_epoch_transition) try ReusedEpochTransitionCache.init(allocator, validator_count) else null; - var epoch_transition_cache: EpochTransitionCache = undefined; - defer { - if (reused_epoch_transition_cache) |*rec| { - rec.deinit(); - } - if (run_epoch_transition) { - epoch_transition_cache.deinit(); - } - } - while (cached_state.slot() < slot) { try processSlot(allocator, post_state); @@ -81,15 +66,18 @@ pub fn processSlotsWithTransientCache( // const epochTransitionTimer = metrics?.epochTransitionTime.startTimer(); // TODO(bing): metrics: time beforeProcessEpoch - std.debug.assert(reused_epoch_transition_cache != null); - try EpochTransitionCache.beforeProcessEpoch(allocator, post_state, &reused_epoch_transition_cache.?, &epoch_transition_cache); - try processEpoch(allocator, post_state, &epoch_transition_cache); + var epoch_transition_cache = try EpochTransitionCache.init(allocator, post_state); + defer { + epoch_transition_cache.deinit(); + allocator.destroy(epoch_transition_cache); + } + try processEpoch(allocator, post_state, epoch_transition_cache); // TODO(bing): registerValidatorStatuses cached_state.slotPtr().* += 1; - try post_state.epoch_cache_ref.get().afterProcessEpoch(post_state, &epoch_transition_cache); + try post_state.epoch_cache_ref.get().afterProcessEpoch(post_state, epoch_transition_cache); // post_state.commit var root: Root = undefined; try cached_state.hashTreeRoot(allocator, &root); @@ -193,3 +181,7 @@ pub fn stateTransition( return post_state; } + +pub fn deinitStateTransition() void { + deinitReusedEpochTransitionCache(); +} diff --git a/test/int/cache/epoch_transition_cache.zig b/test/int/cache/epoch_transition_cache.zig index 5c0c52bef..9b5415d0e 100644 --- a/test/int/cache/epoch_transition_cache.zig +++ b/test/int/cache/epoch_transition_cache.zig @@ -9,16 +9,16 @@ test "EpochTransitionCache.beforeProcessEpoch" { const allocator = std.testing.allocator; const validator_count_arr = &.{ 256, 10_000 }; - // this is created once per runtime, cannot put inside TestCachedBeaconStateAllForks - var reused_epoch_transition_cache = try ReusedEpochTransitionCache.init(allocator, validator_count_arr[0]); - defer reused_epoch_transition_cache.deinit(); - inline for (validator_count_arr) |validator_count| { var test_state = try TestCachedBeaconStateAllForks.init(allocator, validator_count); defer test_state.deinit(); - var epoch_transition_cache: EpochTransitionCache = undefined; - try EpochTransitionCache.beforeProcessEpoch(allocator, test_state.cached_state, &reused_epoch_transition_cache, &epoch_transition_cache); - defer epoch_transition_cache.deinit(); + var epoch_transition_cache = try EpochTransitionCache.init(allocator, test_state.cached_state); + defer { + epoch_transition_cache.deinit(); + allocator.destroy(epoch_transition_cache); + } } + + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_effective_balance_updates.zig b/test/int/epoch/process_effective_balance_updates.zig index 54993513f..42c960422 100644 --- a/test/int/epoch/process_effective_balance_updates.zig +++ b/test/int/epoch/process_effective_balance_updates.zig @@ -15,4 +15,5 @@ test "processEffectiveBalanceUpdates - sanity" { .void_return = false, }, ).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_epoch.zig b/test/int/epoch/process_epoch.zig index 8107498b0..8f461d504 100644 --- a/test/int/epoch/process_epoch.zig +++ b/test/int/epoch/process_epoch.zig @@ -12,4 +12,5 @@ test "processEpoch - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_eth1_data_reset.zig b/test/int/epoch/process_eth1_data_reset.zig index f3c680f1f..6e55a1b78 100644 --- a/test/int/epoch/process_eth1_data_reset.zig +++ b/test/int/epoch/process_eth1_data_reset.zig @@ -12,4 +12,5 @@ test "processEth1DataReset - sanity" { .err_return = false, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_historical_summaries_update.zig b/test/int/epoch/process_historical_summaries_update.zig index 54a66ad9f..0402c057c 100644 --- a/test/int/epoch/process_historical_summaries_update.zig +++ b/test/int/epoch/process_historical_summaries_update.zig @@ -12,4 +12,5 @@ test "processHistoricalSummariesUpdate - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_inactivity_updates.zig b/test/int/epoch/process_inactivity_updates.zig index f1b8d4f84..2ae88127f 100644 --- a/test/int/epoch/process_inactivity_updates.zig +++ b/test/int/epoch/process_inactivity_updates.zig @@ -12,4 +12,5 @@ test "processInactivityUpdates - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_justification_and_finalization.zig b/test/int/epoch/process_justification_and_finalization.zig index 34b5299c1..b737a40bd 100644 --- a/test/int/epoch/process_justification_and_finalization.zig +++ b/test/int/epoch/process_justification_and_finalization.zig @@ -12,4 +12,5 @@ test "processJustificationAndFinalization - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_participation_flag_updates.zig b/test/int/epoch/process_participation_flag_updates.zig index 1711953b2..22a716f09 100644 --- a/test/int/epoch/process_participation_flag_updates.zig +++ b/test/int/epoch/process_participation_flag_updates.zig @@ -2,7 +2,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const state_transition = @import("state_transition"); const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; -const ReusedEpochTransitionCache = state_transition.ReusedEpochTransitionCache; const EpochTransitionCache = state_transition.EpochTransitionCache; const processParticipationFlagUpdates = state_transition.processParticipationFlagUpdates; // this function runs without EpochTransionCache so cannot use getTestProcessFn @@ -11,22 +10,10 @@ test "processParticipationFlagUpdates - sanity" { const allocator = std.testing.allocator; const validator_count_arr = &.{ 256, 10_000 }; - var reused_epoch_transition_cache = try ReusedEpochTransitionCache.init(allocator, validator_count_arr[0]); - defer reused_epoch_transition_cache.deinit(); - inline for (validator_count_arr) |validator_count| { var test_state = try TestCachedBeaconStateAllForks.init(allocator, validator_count); defer test_state.deinit(); - - var epoch_transition_cache: EpochTransitionCache = undefined; - try EpochTransitionCache.beforeProcessEpoch( - allocator, - test_state.cached_state, - &reused_epoch_transition_cache, - &epoch_transition_cache, - ); - defer epoch_transition_cache.deinit(); - try processParticipationFlagUpdates(test_state.cached_state, allocator); } + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_pending_consolidations.zig b/test/int/epoch/process_pending_consolidations.zig index d3f31200a..60b60a623 100644 --- a/test/int/epoch/process_pending_consolidations.zig +++ b/test/int/epoch/process_pending_consolidations.zig @@ -12,4 +12,5 @@ test "processPendingConsolidations - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_pending_deposits.zig b/test/int/epoch/process_pending_deposits.zig index e360ac751..0494c83de 100644 --- a/test/int/epoch/process_pending_deposits.zig +++ b/test/int/epoch/process_pending_deposits.zig @@ -15,4 +15,5 @@ test "processPendingDeposits - sanity" { // .no_void_return = false, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_randao_mixes_reset.zig b/test/int/epoch/process_randao_mixes_reset.zig index c52b59a09..ae986161a 100644 --- a/test/int/epoch/process_randao_mixes_reset.zig +++ b/test/int/epoch/process_randao_mixes_reset.zig @@ -15,4 +15,5 @@ test "processRandaoMixesReset - sanity" { .void_return = true, }, ).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_registry_updates.zig b/test/int/epoch/process_registry_updates.zig index e90124b81..565273e78 100644 --- a/test/int/epoch/process_registry_updates.zig +++ b/test/int/epoch/process_registry_updates.zig @@ -12,4 +12,5 @@ test "processRegistryUpdates - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_rewards_and_penalties.zig b/test/int/epoch/process_rewards_and_penalties.zig index 36fc74978..e35fe02f5 100644 --- a/test/int/epoch/process_rewards_and_penalties.zig +++ b/test/int/epoch/process_rewards_and_penalties.zig @@ -12,4 +12,5 @@ test "processRewardsAndPenalties - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_slashings.zig b/test/int/epoch/process_slashings.zig index 6d33985cc..58dcaaa46 100644 --- a/test/int/epoch/process_slashings.zig +++ b/test/int/epoch/process_slashings.zig @@ -12,4 +12,5 @@ test "processSlashings - sanity" { .err_return = true, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_slashings_reset.zig b/test/int/epoch/process_slashings_reset.zig index 60f17b981..2ce2a239f 100644 --- a/test/int/epoch/process_slashings_reset.zig +++ b/test/int/epoch/process_slashings_reset.zig @@ -12,4 +12,5 @@ test "processSlashingsReset - sanity" { .err_return = false, .void_return = true, }).testProcessEpochFn(); + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/process_sync_committee_updates.zig b/test/int/epoch/process_sync_committee_updates.zig index 16fe8a795..9f02a7c1d 100644 --- a/test/int/epoch/process_sync_committee_updates.zig +++ b/test/int/epoch/process_sync_committee_updates.zig @@ -2,7 +2,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; const state_transition = @import("state_transition"); -const ReusedEpochTransitionCache = state_transition.ReusedEpochTransitionCache; const EpochTransitionCache = state_transition.EpochTransitionCache; const processSyncCommitteeUpdates = state_transition.processSyncCommitteeUpdates; // this function runs without EpochTransionCache so cannot use getTestProcessFn @@ -11,22 +10,10 @@ test "processSyncCommitteeUpdates - sanity" { const allocator = std.testing.allocator; const validator_count_arr = &.{ 256, 10_000 }; - var reused_epoch_transition_cache = try ReusedEpochTransitionCache.init(allocator, validator_count_arr[0]); - defer reused_epoch_transition_cache.deinit(); - inline for (validator_count_arr) |validator_count| { var test_state = try TestCachedBeaconStateAllForks.init(allocator, validator_count); defer test_state.deinit(); - - var epoch_transition_cache: EpochTransitionCache = undefined; - try EpochTransitionCache.beforeProcessEpoch( - allocator, - test_state.cached_state, - &reused_epoch_transition_cache, - &epoch_transition_cache, - ); - defer epoch_transition_cache.deinit(); - try processSyncCommitteeUpdates(allocator, test_state.cached_state); } + defer state_transition.deinitStateTransition(); } diff --git a/test/int/epoch/test_runner.zig b/test/int/epoch/test_runner.zig index 5ab303cfc..5bd095565 100644 --- a/test/int/epoch/test_runner.zig +++ b/test/int/epoch/test_runner.zig @@ -2,7 +2,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; const state_transition = @import("state_transition"); -const ReusedEpochTransitionCache = state_transition.ReusedEpochTransitionCache; const EpochTransitionCache = state_transition.EpochTransitionCache; pub const TestOpt = struct { @@ -17,52 +16,49 @@ pub fn TestRunner(process_epoch_fn: anytype, opt: TestOpt) type { const allocator = std.testing.allocator; const validator_count_arr = &.{ 256, 10_000 }; - var reused_epoch_transition_cache = try ReusedEpochTransitionCache.init(allocator, validator_count_arr[0]); - defer reused_epoch_transition_cache.deinit(); - inline for (validator_count_arr) |validator_count| { var test_state = try TestCachedBeaconStateAllForks.init(allocator, validator_count); defer test_state.deinit(); - var epoch_transition_cache: EpochTransitionCache = undefined; - try EpochTransitionCache.beforeProcessEpoch( + var epoch_transition_cache = try EpochTransitionCache.init( allocator, test_state.cached_state, - &reused_epoch_transition_cache, - &epoch_transition_cache, ); - defer epoch_transition_cache.deinit(); + defer { + epoch_transition_cache.deinit(); + allocator.destroy(epoch_transition_cache); + } if (opt.void_return) { if (opt.err_return) { // with try if (opt.alloc) { - try process_epoch_fn(allocator, test_state.cached_state, &epoch_transition_cache); + try process_epoch_fn(allocator, test_state.cached_state, epoch_transition_cache); } else { - try process_epoch_fn(test_state.cached_state, &epoch_transition_cache); + try process_epoch_fn(test_state.cached_state, epoch_transition_cache); } } else { // no try if (opt.alloc) { - process_epoch_fn(allocator, test_state.cached_state, &epoch_transition_cache); + process_epoch_fn(allocator, test_state.cached_state, epoch_transition_cache); } else { - process_epoch_fn(test_state.cached_state, &epoch_transition_cache); + process_epoch_fn(test_state.cached_state, epoch_transition_cache); } } } else { if (opt.err_return) { // with try if (opt.alloc) { - _ = try process_epoch_fn(allocator, test_state.cached_state, &epoch_transition_cache); + _ = try process_epoch_fn(allocator, test_state.cached_state, epoch_transition_cache); } else { - _ = try process_epoch_fn(test_state.cached_state, &epoch_transition_cache); + _ = try process_epoch_fn(test_state.cached_state, epoch_transition_cache); } } else { // no try if (opt.alloc) { - _ = process_epoch_fn(allocator, test_state.cached_state, &epoch_transition_cache); + _ = process_epoch_fn(allocator, test_state.cached_state, epoch_transition_cache); } else { - _ = process_epoch_fn(test_state.cached_state, &epoch_transition_cache); + _ = process_epoch_fn(test_state.cached_state, epoch_transition_cache); } } } diff --git a/test/int/state_transition.zig b/test/int/state_transition.zig index 9484ef2ac..1aa112b5d 100644 --- a/test/int/state_transition.zig +++ b/test/int/state_transition.zig @@ -61,4 +61,6 @@ test "state transition - electra block" { } } } + + defer state_transition.deinitStateTransition(); } From a38cb718ef21e3a55a0a262a7f21c0c1f34a72b9 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 29 Oct 2025 14:55:12 +0700 Subject: [PATCH 37/72] fix: memory leak if stateTransition() failing partway --- src/state_transition/cache/state_cache.zig | 4 +++- test/spec/runner/Sanity.zig | 27 +++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/state_transition/cache/state_cache.zig b/src/state_transition/cache/state_cache.zig index 795c093f4..253fd9832 100644 --- a/src/state_transition/cache/state_cache.zig +++ b/src/state_transition/cache/state_cache.zig @@ -51,11 +51,13 @@ pub const CachedBeaconStateAllForks = struct { pub fn clone(self: *CachedBeaconStateAllForks, allocator: Allocator) !*CachedBeaconStateAllForks { const cached_state = try allocator.create(CachedBeaconStateAllForks); errdefer allocator.destroy(cached_state); + const epoch_cache_ref = self.epoch_cache_ref.acquire(); + errdefer epoch_cache_ref.release(); cached_state.* = .{ .allocator = allocator, .config = self.config, - .epoch_cache_ref = self.epoch_cache_ref.acquire(), + .epoch_cache_ref = epoch_cache_ref, .state = try self.state.clone(allocator), }; return cached_state; diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig index 2495bdd85..e4fbd9440 100644 --- a/test/spec/runner/Sanity.zig +++ b/test/spec/runner/Sanity.zig @@ -34,7 +34,10 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { var tc = try Self.init(allocator, dir); - defer tc.deinit(); + defer { + tc.deinit(); + state_transition.deinitStateTransition(); + } try tc.runTest(); } @@ -119,7 +122,10 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { var tc = try Self.init(allocator, dir); - defer tc.deinit(); + defer { + tc.deinit(); + state_transition.deinitStateTransition(); + } try tc.runTest(); } @@ -215,6 +221,13 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { for (self.blocks, 0..) |*block, i| { const signed_block = @unionInit(state_transition.SignedBeaconBlock, @tagName(fork), block); { + // if error, clean pre_state of stateTransition() function + errdefer { + if (i > 0) { + post_state.deinit(); + self.pre.allocator.destroy(post_state); + } + } const new_post_state = try state_transition.state_transition.stateTransition( self.pre.allocator, post_state, @@ -225,11 +238,13 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { ); // don't deinit the initial pre state, we do it in deinit() - const to_destroy = if (i > 0) post_state else null; + const to_destroy = post_state; post_state = new_post_state; - if (to_destroy) |state| { - state.deinit(); - self.pre.allocator.destroy(state); + + // clean post_state of stateTransition() function + if (i > 0) { + to_destroy.deinit(); + self.pre.allocator.destroy(to_destroy); } } } From 0ad403b5bf655a3a1f7a2256488ca7c11b64b0de Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 29 Oct 2025 15:04:14 +0700 Subject: [PATCH 38/72] fix: isValidIndexedAttestationIndices() check sort index --- src/state_transition/block/is_valid_indexed_attestation.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/state_transition/block/is_valid_indexed_attestation.zig b/src/state_transition/block/is_valid_indexed_attestation.zig index 17d3e57bc..acb756a73 100644 --- a/src/state_transition/block/is_valid_indexed_attestation.zig +++ b/src/state_transition/block/is_valid_indexed_attestation.zig @@ -38,8 +38,10 @@ pub fn isValidIndexedAttestationIndices(cached_state: *const CachedBeaconStateAl // Just check if they are monotonically increasing, // instead of creating a set and sorting it. Should be (O(n)) instead of O(n log(n)) var prev: ValidatorIndex = 0; - for (indices) |index| { - if (index <= prev) return false; + for (indices, 0..) |index, i| { + if (i >= 1 and index <= prev) { + return false; + } prev = index; } From 8eb7545f2bd7904f2d95ff22403e3cdc9fda76b4 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 09:24:31 +0700 Subject: [PATCH 39/72] fix: computeSyncParticipantReward() --- src/state_transition/utils/math.zig | 5 +++++ src/state_transition/utils/sync_committee.zig | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 src/state_transition/utils/math.zig diff --git a/src/state_transition/utils/math.zig b/src/state_transition/utils/math.zig new file mode 100644 index 000000000..a7c28aaab --- /dev/null +++ b/src/state_transition/utils/math.zig @@ -0,0 +1,5 @@ +pub inline fn intSqrt(x: u64) u64 { + const x_f64: f64 = @floatFromInt(x); + const sqrt_f64: f64 = @sqrt(x_f64); + return @intFromFloat(sqrt_f64); +} diff --git a/src/state_transition/utils/sync_committee.zig b/src/state_transition/utils/sync_committee.zig index 9cec680f8..f1fb03935 100644 --- a/src/state_transition/utils/sync_committee.zig +++ b/src/state_transition/utils/sync_committee.zig @@ -12,6 +12,7 @@ const SyncCommittee = ssz.altair.SyncCommittee.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; const PublicKey = ssz.primitive.BLSPubkey.Type; const ForkSeq = @import("config").ForkSeq; +const intSqrt = @import("../utils/math.zig").intSqrt; pub const getNextSyncCommitteeIndices = @import("./seed.zig").getNextSyncCommitteeIndices; const SyncCommitteeInfo = struct { @@ -46,15 +47,13 @@ pub fn getNextSyncCommittee(allocator: Allocator, state: *const BeaconStateAllFo pub fn computeSyncParticipantReward(total_active_balance_increments: u64) u64 { const total_active_balance = total_active_balance_increments * preset.EFFECTIVE_BALANCE_INCREMENT; - const base_reward_per_increment = @divFloor((preset.EFFECTIVE_BALANCE_INCREMENT * preset.BASE_REWARD_FACTOR), total_active_balance); + const base_reward_per_increment = @divFloor((preset.EFFECTIVE_BALANCE_INCREMENT * preset.BASE_REWARD_FACTOR), intSqrt(total_active_balance)); const total_base_rewards = base_reward_per_increment * total_active_balance_increments; const max_participant_rewards = @divFloor(@divFloor(total_base_rewards * c.SYNC_REWARD_WEIGHT, c.WEIGHT_DENOMINATOR), preset.SLOTS_PER_EPOCH); return @divFloor(max_participant_rewards, preset.SYNC_COMMITTEE_SIZE); } pub fn computeBaseRewardPerIncrement(total_active_stake_by_increment: u64) u64 { - const total_active_stake: f64 = @floatFromInt(total_active_stake_by_increment * preset.EFFECTIVE_BALANCE_INCREMENT); - const total_active_stake_sqrt_f64: f64 = @sqrt(total_active_stake); - const total_active_stake_sqrt: u64 = @intFromFloat(total_active_stake_sqrt_f64); + const total_active_stake_sqrt = intSqrt(total_active_stake_by_increment * preset.EFFECTIVE_BALANCE_INCREMENT); return @divFloor((preset.EFFECTIVE_BALANCE_INCREMENT * preset.BASE_REWARD_FACTOR), total_active_stake_sqrt); } From f7e193c0ed3f377488142eba1966a64acd23b7ca Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 09:42:44 +0700 Subject: [PATCH 40/72] fix: proposer_weight_factor f64 const --- src/state_transition/cache/epoch_cache.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index e99ced37d..6a0532077 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -60,7 +60,10 @@ pub const EpochCacheOpts = struct { skip_sync_pubkeys: bool, }; -pub const PROPOSER_WEIGHT_FACTOR = c.PROPOSER_WEIGHT / (c.WEIGHT_DENOMINATOR - c.PROPOSER_WEIGHT); +const proposer_weight: f64 = @floatFromInt(c.PROPOSER_WEIGHT); +const weight_denominator: f64 = @floatFromInt(c.WEIGHT_DENOMINATOR); + +pub const proposer_weight_factor: f64 = proposer_weight / (weight_denominator - proposer_weight); /// an EpochCache is shared by multiple CachedBeaconStateAllForks instances /// a CachedBeaconStateAllForks should increase the reference count of EpochCache when it is created @@ -235,7 +238,8 @@ pub const EpochCache = struct { // Values syncParticipantReward, syncProposerReward, baseRewardPerIncrement are only used after altair. // However, since they are very cheap to compute they are computed always to simplify upgradeState function. const sync_participant_reward = computeSyncParticipantReward(total_active_balance_increments); - const sync_proposer_reward = sync_participant_reward * PROPOSER_WEIGHT_FACTOR; + const sync_participant_reward_f64: f64 = @floatFromInt(sync_participant_reward); + const sync_proposer_reward: u64 = @intFromFloat(std.math.floor(sync_participant_reward_f64 * proposer_weight_factor)); const base_reward_pre_increment = computeBaseRewardPerIncrement(total_active_balance_increments); const skip_sync_committee_cache = if (option) |opt| opt.skip_sync_committee_cache else !after_altair_fork; var current_sync_committee_indexed = if (skip_sync_committee_cache) SyncCommitteeCacheAllForks.initEmpty() else try SyncCommitteeCacheAllForks.initSyncCommittee(allocator, state.currentSyncCommittee(), pubkey_to_index); @@ -434,7 +438,8 @@ pub const EpochCache = struct { self.total_active_balance_increments = epoch_transition_cache.next_epoch_total_active_balance_by_increment; if (upcoming_epoch >= self.config.chain.ALTAIR_FORK_EPOCH) { self.sync_participant_reward = computeSyncParticipantReward(self.total_active_balance_increments); - self.sync_proposer_reward = @intCast(self.sync_participant_reward * PROPOSER_WEIGHT_FACTOR); + const sync_participant_reward_f64: f64 = @floatFromInt(self.sync_participant_reward); + self.sync_proposer_reward = @intFromFloat(std.math.floor(sync_participant_reward_f64 * proposer_weight_factor)); self.base_reward_per_increment = computeBaseRewardPerIncrement(self.total_active_balance_increments); } From 7c03a039620d36518f644cc7fc54e6e56a534275 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 09:43:12 +0700 Subject: [PATCH 41/72] fix: processSyncAggregate() --- src/state_transition/block/process_sync_committee.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/block/process_sync_committee.zig b/src/state_transition/block/process_sync_committee.zig index 14c012f3e..c0ead329e 100644 --- a/src/state_transition/block/process_sync_committee.zig +++ b/src/state_transition/block/process_sync_committee.zig @@ -69,7 +69,7 @@ pub fn processSyncAggregate( } else { // Negative rewards for non participants if (index == proposer_index) { - balances.items[proposer_index] = @max(0, proposer_balance - sync_participant_reward); + proposer_balance = @max(0, proposer_balance - sync_participant_reward); } else { decreaseBalance(state, index, sync_participant_reward); } From d67614b36dcbfc515f7ffb55fb633cfd8b4bab97 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 10:45:35 +0700 Subject: [PATCH 42/72] fix: processAttestationsAltair increase proposer reward --- src/state_transition/block/process_attestation_altair.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index 907e33959..c2383ad7d 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -78,8 +78,8 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach // At epoch boundary, 100% of attestations belong to previous epoch // so we want to update the participation flag tree in batch - // Note ParticipationFlags type uses option {setBitwiseOR: true}, .set() does a |= operation - epoch_participation[validator_index] = flags_attestation; + // no setBitwiseOR implemented in zig ssz, so we do it manually here + epoch_participation[validator_index] = flags_attestation | flags; // Returns flags that are NOT set before (~ bitwise NOT) AND are set after const flags_new_set = ~flags & flags_attestation; @@ -113,9 +113,8 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach 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); - - increaseBalance(state, try epoch_cache.getBeaconProposer(state_slot), proposer_reward); } + increaseBalance(state, try epoch_cache.getBeaconProposer(state_slot), proposer_reward); } pub fn getAttestationParticipationStatus(state: *const BeaconStateAllForks, data: ssz.phase0.AttestationData.Type, inclusion_delay: u64, current_epoch: Epoch, root_cache: *RootCache) !u8 { From 8fcc8eb14e196b7ad95461fa17ba0c874cbfb1c8 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 13:21:37 +0700 Subject: [PATCH 43/72] fix: isExecutionEnabled() --- src/state_transition/utils/execution.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/state_transition/utils/execution.zig b/src/state_transition/utils/execution.zig index 6c977120f..9aae8c6e4 100644 --- a/src/state_transition/utils/execution.zig +++ b/src/state_transition/utils/execution.zig @@ -21,9 +21,9 @@ pub fn isExecutionEnabled(state: *const BeaconStateAllForks, block: *const Signe const body = b.beaconBlock().beaconBlockBody(); return switch (body) { - .capella => |bd| ssz.capella.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.capella.ExecutionPayloadHeader.default_value), - .deneb => |bd| ssz.deneb.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.deneb.ExecutionPayloadHeader.default_value), - .electra => |bd| ssz.electra.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.electra.ExecutionPayloadHeader.default_value), + .capella => |bd| !ssz.capella.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.capella.ExecutionPayloadHeader.default_value), + .deneb => |bd| !ssz.deneb.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.deneb.ExecutionPayloadHeader.default_value), + .electra => |bd| !ssz.electra.ExecutionPayloadHeader.equals(&bd.execution_payload_header, &ssz.electra.ExecutionPayloadHeader.default_value), }; }, .regular => |b| { @@ -31,10 +31,10 @@ pub fn isExecutionEnabled(state: *const BeaconStateAllForks, block: *const Signe return switch (body) { .phase0, .altair => @panic("Unsupported"), - .bellatrix => |bd| ssz.bellatrix.ExecutionPayload.equals(&bd.execution_payload, &ssz.bellatrix.ExecutionPayload.default_value), - .capella => |bd| ssz.capella.ExecutionPayload.equals(&bd.execution_payload, &ssz.capella.ExecutionPayload.default_value), - .deneb => |bd| ssz.deneb.ExecutionPayload.equals(&bd.execution_payload, &ssz.deneb.ExecutionPayload.default_value), - .electra => |bd| ssz.electra.ExecutionPayload.equals(&bd.execution_payload, &ssz.electra.ExecutionPayload.default_value), + .bellatrix => |bd| !ssz.bellatrix.ExecutionPayload.equals(&bd.execution_payload, &ssz.bellatrix.ExecutionPayload.default_value), + .capella => |bd| !ssz.capella.ExecutionPayload.equals(&bd.execution_payload, &ssz.capella.ExecutionPayload.default_value), + .deneb => |bd| !ssz.deneb.ExecutionPayload.equals(&bd.execution_payload, &ssz.deneb.ExecutionPayload.default_value), + .electra => |bd| !ssz.electra.ExecutionPayload.equals(&bd.execution_payload, &ssz.electra.ExecutionPayload.default_value), }; }, } From 8e6ddfaa6d5a0d0e11a11189aec343572a9dbc87 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 13:33:26 +0700 Subject: [PATCH 44/72] fix: deinit withdrawals_result --- src/state_transition/block/process_block.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/state_transition/block/process_block.zig b/src/state_transition/block/process_block.zig index 723d76c60..449b6d7bc 100644 --- a/src/state_transition/block/process_block.zig +++ b/src/state_transition/block/process_block.zig @@ -46,6 +46,7 @@ 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 (state.isPostCapella()) { + // TODO: given max withdrawals of MAX_WITHDRAWALS_PER_PAYLOAD, can use fixed size array instead of heap alloc var withdrawals_result = WithdrawalsResult{ .withdrawals = try Withdrawals.initCapacity( allocator, preset.MAX_WITHDRAWALS_PER_PAYLOAD, @@ -54,7 +55,7 @@ pub fn processBlock( defer withdrawal_balances.deinit(); try getExpectedWithdrawals(allocator, &withdrawals_result, &withdrawal_balances, cached_state); - defer withdrawals_result.withdrawals.clearRetainingCapacity(); + defer withdrawals_result.withdrawals.deinit(allocator); const body = block.beaconBlockBody(); const payload_withdrawals_root = switch (body) { From bb93b564e714b986f6881d95eeb9cfe1652457cb Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 14:05:21 +0700 Subject: [PATCH 45/72] feat: run spec tests on CI --- .github/workflows/CI.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cfa3a1e84..ab7124e71 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -90,3 +90,23 @@ jobs: - name: Run Int Tests run: | zig build test:int + # It takes time to download and run spec tests, so we only run them on ubuntu-latest + - name: Restore spec tests cache + uses: actions/cache@v4 + with: + path: test/spec/spec_tests + key: spec-test-data-${{ hashFiles('build.zig') }} # spec test version is defined in build.zig + if: matrix.os == 'ubuntu-latest' + - name: Download Spec Tests + run: | + zig build run:download_spec_tests + if: matrix.os == 'ubuntu-latest' + - name: Write Spec Tests + run: | + zig build run:write_spec_tests + if: matrix.os == 'ubuntu-latest' + - name: Run Spec Tests + run: | + zig build test:spec_tests + if: matrix.os == 'ubuntu-latest' + From f0260ff066d16e05d0c4fce9e6b024ca06f1f2b0 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 14:27:49 +0700 Subject: [PATCH 46/72] fix: create test_case folder --- test/spec/write_spec_tests.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 94cf28ddf..be90fd9fa 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -27,6 +27,7 @@ fn TestWriter(comptime kind: RunnerKind) type { pub fn main() !void { const test_case_dir = "test/spec/test_case/"; + try std.fs.cwd().makeDir(test_case_dir); inline for (supported_test_runners) |kind| { const test_case_file = test_case_dir ++ @tagName(kind) ++ "_tests.zig"; From 57a9fa72c39395076980ad6321a57f10235ee39f Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 14:59:31 +0700 Subject: [PATCH 47/72] chore: new spec test job --- .github/workflows/CI.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ab7124e71..c0bec30c1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -90,7 +90,19 @@ jobs: - name: Run Int Tests run: | zig build test:int - # It takes time to download and run spec tests, so we only run them on ubuntu-latest + spec-test: + name: spec test + # It takes time to download and run spec tests, so we only run them on ubuntu-latest + runs-on: ubuntu-latest + needs: build-test + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + cache-key: ${{ matrix.os }}-${{ env.ZIG_VERSION }} - name: Restore spec tests cache uses: actions/cache@v4 with: From 05604096dda454c342731a41be571f7704653f90 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 15:05:48 +0700 Subject: [PATCH 48/72] fix: remove conditional statement --- .github/workflows/CI.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c0bec30c1..4e7788114 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -108,17 +108,12 @@ jobs: with: path: test/spec/spec_tests key: spec-test-data-${{ hashFiles('build.zig') }} # spec test version is defined in build.zig - if: matrix.os == 'ubuntu-latest' - name: Download Spec Tests run: | zig build run:download_spec_tests - if: matrix.os == 'ubuntu-latest' - name: Write Spec Tests run: | zig build run:write_spec_tests - if: matrix.os == 'ubuntu-latest' - name: Run Spec Tests run: | zig build test:spec_tests - if: matrix.os == 'ubuntu-latest' - From 91be677e747dc9f7c734eb83d9b6f67999911999 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 30 Oct 2025 15:39:32 +0700 Subject: [PATCH 49/72] fix: missing minimal config definition --- src/constants/root.zig | 1 + src/preset/preset.zig | 2 +- src/state_transition/block/process_consolidation_request.zig | 5 +++-- src/state_transition/block/process_deposit.zig | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/constants/root.zig b/src/constants/root.zig index 2f21f955b..de369d741 100644 --- a/src/constants/root.zig +++ b/src/constants/root.zig @@ -5,6 +5,7 @@ pub const DEPOSIT_CONTRACT_TREE_DEPTH = std.math.pow(usize, 2, 5); // 32 pub const JUSTIFICATION_BITS_LENGTH = 4; pub const ZERO_HASH = [_]u8{0} ** 32; pub const ZERO_HASH_HEX = "0x0000000000000000000000000000000000000000000000000000000000000000"; +pub const GENESIS_SLOT = 0; // Withdrawal prefixes // Since the prefixes are just 1 byte, we define and use them as number diff --git a/src/preset/preset.zig b/src/preset/preset.zig index 6b2ee54dc..1a3379d31 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -82,7 +82,6 @@ const PresetMainnet = struct { pub const MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8; pub const DEPOSIT_CONTRACT_TREE_DEPTH = 32; pub const GENESIS_SLOT = 0; - pub const FAR_FUTURE_EPOCH = 18_446_744_073_709_551_615; // 2*64 -1; pub const MAX_PENDING_DEPOSITS_PER_EPOCH = 16; }; @@ -151,6 +150,7 @@ const PresetMinimal = struct { pub const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD = 16; pub const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD = 2; pub const WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA = 4096; + pub const MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA = 4096; pub const FIELD_ELEMENTS_PER_CELL = 64; pub const FIELD_ELEMENTS_PER_EXT_BLOB = 8192; pub const KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH = 4; diff --git a/src/state_transition/block/process_consolidation_request.zig b/src/state_transition/block/process_consolidation_request.zig index 6d0473771..8d04da021 100644 --- a/src/state_transition/block/process_consolidation_request.zig +++ b/src/state_transition/block/process_consolidation_request.zig @@ -2,6 +2,7 @@ const std = @import("std"); const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const ssz = @import("consensus_types"); const preset = @import("preset").preset; +const c = @import("constants"); const ConsolidationRequest = ssz.electra.ConsolidationRequest.Type; const PendingConsolidation = ssz.electra.PendingConsolidation.Type; const hasEth1WithdrawalCredential = @import("../utils/capella.zig").hasEth1WithdrawalCredential; @@ -80,7 +81,7 @@ pub fn processConsolidationRequest( } // Verify exits for source and target have not been initiated - if (source_validator.exit_epoch != preset.FAR_FUTURE_EPOCH or target_validator.exit_epoch != preset.FAR_FUTURE_EPOCH) { + if (source_validator.exit_epoch != c.FAR_FUTURE_EPOCH or target_validator.exit_epoch != c.FAR_FUTURE_EPOCH) { return; } @@ -139,7 +140,7 @@ fn isValidSwitchToCompoundRequest(cached_state: *const CachedBeaconStateAllForks } // Verify exit for source has not been initiated - if (source_validator.exit_epoch != preset.FAR_FUTURE_EPOCH) { + if (source_validator.exit_epoch != c.FAR_FUTURE_EPOCH) { return false; } diff --git a/src/state_transition/block/process_deposit.zig b/src/state_transition/block/process_deposit.zig index 61ba5bd15..c8232f155 100644 --- a/src/state_transition/block/process_deposit.zig +++ b/src/state_transition/block/process_deposit.zig @@ -63,7 +63,7 @@ pub fn processDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAllF if (!verifyMerkleBranch( deposit_data_root, &deposit.proof, - preset.DEPOSIT_CONTRACT_TREE_DEPTH + 1, + c.DEPOSIT_CONTRACT_TREE_DEPTH + 1, state.eth1DepositIndex(), state.eth1Data().deposit_root, )) { @@ -108,7 +108,7 @@ pub fn applyDeposit(allocator: Allocator, cached_state: *CachedBeaconStateAllFor .withdrawal_credentials = withdrawal_credentials, .amount = amount, .signature = signature, - .slot = preset.GENESIS_SLOT, // Use GENESIS_SLOT to distinguish from a pending deposit request + .slot = c.GENESIS_SLOT, // Use GENESIS_SLOT to distinguish from a pending deposit request }; if (is_new_validator) { From ab4cc95599bc659501f90406e86bd7db82941adc Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 10:01:53 +0700 Subject: [PATCH 50/72] fix: various fixes for minimal --- src/config/beacon_config.zig | 2 +- src/state_transition/epoch/process_epoch.zig | 2 +- src/state_transition/epoch/process_eth1_data_reset.zig | 5 +++-- .../epoch/process_pending_consolidations.zig | 6 +++++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/config/beacon_config.zig b/src/config/beacon_config.zig index d6c1f7565..21072f37b 100644 --- a/src/config/beacon_config.zig +++ b/src/config/beacon_config.zig @@ -213,7 +213,7 @@ pub const BeaconConfig = struct { } pub fn getDomainForVoluntaryExit(self: *const BeaconConfig, state_slot: Slot, message_slot: ?Slot) ![32]u8 { - const domain = if (state_slot < self.chain.DENEB_FORK_EPOCH * preset.SLOTS_PER_EPOCH) { + const domain = if (state_slot / preset.SLOTS_PER_EPOCH < self.chain.DENEB_FORK_EPOCH) { return self.getDomain(state_slot, DOMAIN_VOLUNTARY_EXIT, message_slot); } else { return self.getDomainByForkSeq(ForkSeq.capella, DOMAIN_VOLUNTARY_EXIT); diff --git a/src/state_transition/epoch/process_epoch.zig b/src/state_transition/epoch/process_epoch.zig index df88d497e..fe3f94e28 100644 --- a/src/state_transition/epoch/process_epoch.zig +++ b/src/state_transition/epoch/process_epoch.zig @@ -35,7 +35,7 @@ pub fn processEpoch(allocator: std.mem.Allocator, cached_state: *CachedBeaconSta try processRewardsAndPenalties(allocator, cached_state, cache); - processEth1DataReset(cached_state, cache); + processEth1DataReset(allocator, cached_state, cache); if (state.isPostElectra()) { try processPendingDeposits(allocator, cached_state, cache); diff --git a/src/state_transition/epoch/process_eth1_data_reset.zig b/src/state_transition/epoch/process_eth1_data_reset.zig index 06b81acd8..e58f3c931 100644 --- a/src/state_transition/epoch/process_eth1_data_reset.zig +++ b/src/state_transition/epoch/process_eth1_data_reset.zig @@ -1,3 +1,4 @@ +const Allocator = @import("std").mem.Allocator; const ssz = @import("consensus_types"); const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const EpochTransitionCache = @import("../cache/epoch_transition_cache.zig").EpochTransitionCache; @@ -5,13 +6,13 @@ const preset = @import("preset").preset; const EPOCHS_PER_ETH1_VOTING_PERIOD = preset.EPOCHS_PER_ETH1_VOTING_PERIOD; /// Reset eth1DataVotes tree every `EPOCHS_PER_ETH1_VOTING_PERIOD`. -pub fn processEth1DataReset(cached_state: *CachedBeaconStateAllForks, cache: *const EpochTransitionCache) void { +pub fn processEth1DataReset(allocator: Allocator, cached_state: *CachedBeaconStateAllForks, cache: *const EpochTransitionCache) void { const next_epoch = cache.current_epoch + 1; // reset eth1 data votes if (next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0) { const state = cached_state.state; const state_eth1_data_votes = state.eth1DataVotes(); - @memcpy(state_eth1_data_votes.items, ssz.phase0.Eth1DataVotes.default_value.items); + state_eth1_data_votes.clearAndFree(allocator); } } diff --git a/src/state_transition/epoch/process_pending_consolidations.zig b/src/state_transition/epoch/process_pending_consolidations.zig index b240c8d84..2e49c9293 100644 --- a/src/state_transition/epoch/process_pending_consolidations.zig +++ b/src/state_transition/epoch/process_pending_consolidations.zig @@ -52,7 +52,11 @@ pub fn processPendingConsolidations(allocator: Allocator, cached_state: *CachedB if (next_pending_consolidation > 0) { const new_pending_consolidations = pending_consolidations.items[next_pending_consolidation..]; + // cannot use memcpy due to overlap + const items = pending_consolidations.items; + for (0..new_pending_consolidations.len) |i| { + items[i] = items[i + next_pending_consolidation]; + } try pending_consolidations.resize(allocator, new_pending_consolidations.len); - @memcpy(pending_consolidations.items[0..], new_pending_consolidations[0..]); } } From 05c4fcd18632e27ef1bc05d58c6eaafbf547abc0 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 10:02:17 +0700 Subject: [PATCH 51/72] fix: ignore error if test/spec_test_case already exist --- test/spec/write_spec_tests.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index be90fd9fa..7975ae9c6 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -27,7 +27,10 @@ fn TestWriter(comptime kind: RunnerKind) type { pub fn main() !void { const test_case_dir = "test/spec/test_case/"; - try std.fs.cwd().makeDir(test_case_dir); + std.fs.cwd().makeDir(test_case_dir) catch |err| { + if (err != error.PathAlreadyExists) return err; + // ignore if the directory already exists + }; inline for (supported_test_runners) |kind| { const test_case_file = test_case_dir ++ @tagName(kind) ++ "_tests.zig"; From 5583474d4e325a1577cf7107dc565a72febcb741 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 10:17:44 +0700 Subject: [PATCH 52/72] fix: remove PENDING_PARTIAL_WITHDRAWALS_LIMIT from constant, use preset --- src/constants/root.zig | 1 - src/state_transition/block/process_withdrawal_request.zig | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/constants/root.zig b/src/constants/root.zig index de369d741..fd7a2b445 100644 --- a/src/constants/root.zig +++ b/src/constants/root.zig @@ -109,7 +109,6 @@ 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 PENDING_PARTIAL_WITHDRAWALS_LIMIT = 134_217_728; pub const CURRENT_SYNC_COMMITTEE_GINDEX = 54; pub const EXECUTION_PAYLOAD_GINDEX = 25; diff --git a/src/state_transition/block/process_withdrawal_request.zig b/src/state_transition/block/process_withdrawal_request.zig index c9e8f1878..ef6fc90b9 100644 --- a/src/state_transition/block/process_withdrawal_request.zig +++ b/src/state_transition/block/process_withdrawal_request.zig @@ -27,7 +27,7 @@ pub fn processWithdrawalRequest(allocator: std.mem.Allocator, cached_state: *Cac const is_full_exit_request = amount == c.FULL_EXIT_REQUEST_AMOUNT; // If partial withdrawal queue is full, only full exits are processed - if (pending_partial_withdrawals.items.len >= c.PENDING_PARTIAL_WITHDRAWALS_LIMIT and + if (pending_partial_withdrawals.items.len >= preset.PENDING_PARTIAL_WITHDRAWALS_LIMIT and !is_full_exit_request) { return; From b8c4ddee3ce2e386d216be6148fbd4242334ce6e Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 10:37:57 +0700 Subject: [PATCH 53/72] fix: processEth1DataReset int test --- test/int/epoch/process_eth1_data_reset.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/int/epoch/process_eth1_data_reset.zig b/test/int/epoch/process_eth1_data_reset.zig index 6e55a1b78..41d2c4d07 100644 --- a/test/int/epoch/process_eth1_data_reset.zig +++ b/test/int/epoch/process_eth1_data_reset.zig @@ -8,7 +8,7 @@ const TestRunner = @import("./test_runner.zig").TestRunner; test "processEth1DataReset - sanity" { try TestRunner(state_transition.processEth1DataReset, .{ - .alloc = false, + .alloc = true, .err_return = false, .void_return = true, }).testProcessEpochFn(); From 5a80728d55b1d0f71a47b6d44162c1ad29896c82 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 11:20:49 +0700 Subject: [PATCH 54/72] fix: processAttestation() minimal spec tests --- .../block/process_attestation_altair.zig | 15 ++++++++------- .../block/process_attestation_phase0.zig | 17 ++++++++++------- .../block/process_attestations.zig | 4 ++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index c2383ad7d..6240473d3 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -25,10 +25,11 @@ const TIMELY_TARGET = 1 << c.TIMELY_TARGET_FLAG_INDEX; const TIMELY_HEAD = 1 << c.TIMELY_HEAD_FLAG_INDEX; const SLOTS_PER_EPOCH_SQRT = std.math.sqrt(preset.SLOTS_PER_EPOCH); -/// AT = AttestationType -/// for phase0 it's `ssz.phase0.Attestation.Type` -/// for electra it's `ssz.electra.Attestation.Type` -pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, comptime AT: type, attestations: []AT, verify_signature: bool) !void { +pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const CachedBeaconStateAllForks, attestations: anytype, verify_signature: bool) !void { + // AT = AttestationType + // for phase0 it's `ssz.phase0.Attestation.Type` + // for electra it's `ssz.electra.Attestation.Type` + const AT = @typeInfo(@TypeOf(attestations)).pointer.child; const state = cached_state.state; const epoch_cache = cached_state.getEpochCache(); const effective_balance_increments = epoch_cache.effective_balance_increment.get().items; @@ -44,12 +45,12 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach // let newSeenAttestersEffectiveBalance = 0; var proposer_reward: u64 = 0; - for (attestations) |attestation| { + for (attestations) |*attestation| { const data = attestation.data; - try validateAttestation(AT, cached_state, attestation); + try validateAttestation(cached_state, attestation); // Retrieve the validator indices from the attestation participation bitfield - const attesting_indices = try if (AT == Phase0Attestation) epoch_cache.getAttestingIndicesPhase0(&attestation) else epoch_cache.getAttestingIndicesElectra(&attestation); + const attesting_indices = try if (AT == Phase0Attestation) epoch_cache.getAttestingIndicesPhase0(attestation) else epoch_cache.getAttestingIndicesElectra(attestation); defer attesting_indices.deinit(); // this check is done last because its the most expensive (if signature verification is toggled on) diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index 8a3ca15e4..a277a5e1e 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -20,7 +20,7 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco const slot = state.slot(); const data = attestation.data; - try validateAttestation(*const Phase0Attestation, cached_state, attestation); + try validateAttestation(cached_state, attestation); // should store a clone of aggregation_bits on Phase0 BeaconState to avoid double free error var cloned_aggregation_bits: s.BitListType(preset.MAX_VALIDATORS_PER_COMMITTEE).Type = undefined; @@ -62,10 +62,13 @@ pub fn processAttestationPhase0(allocator: Allocator, cached_state: *CachedBeaco } /// AT could be either Phase0Attestation or ElectraAttestation -pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconStateAllForks, attestation: AT) !void { +pub fn validateAttestation(cached_state: *const CachedBeaconStateAllForks, attestation: anytype) !void { + const T = @typeInfo(@TypeOf(attestation)).pointer.child; + std.debug.assert(T == Phase0Attestation or T == ElectraAttestation); + const is_electra = T == ElectraAttestation; const epoch_cache = cached_state.getEpochCache(); const state = cached_state.state; - const slot = state.slot(); + const state_slot = state.slot(); const data = attestation.data; const computed_epoch = computeEpochAtSlot(data.slot); const committee_count = try epoch_cache.getCommitteeCountPerSlot(computed_epoch); @@ -79,12 +82,12 @@ pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconS } // post deneb, the attestations are valid till end of next epoch - if (!(data.slot + preset.MIN_ATTESTATION_INCLUSION_DELAY <= slot and isTimelyTarget(state, slot - data.slot))) { + if (!(data.slot + preset.MIN_ATTESTATION_INCLUSION_DELAY <= state_slot and isTimelyTarget(state, state_slot - data.slot))) { return error.InvalidAttestationSlotNotWithInInclusionWindow; } // same to fork >= ForkSeq.electra but more type safe - if (AT == ElectraAttestation) { + if (is_electra) { if (data.index != 0) { return error.InvalidAttestationNonZeroDataIndex; } @@ -108,7 +111,7 @@ pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconS // instead of implementing/calling getBeaconCommittees(slot, committee_indices.items), we call getBeaconCommittee(slot, index) var committee_offset: usize = 0; for (committee_indices) |committee_index| { - const committee_validators = try epoch_cache.getBeaconCommittee(slot, committee_index); + const committee_validators = try epoch_cache.getBeaconCommittee(data.slot, committee_index); if (committee_offset + committee_validators.len > aggregation_bits_array.len) { return error.InvalidAttestationCommitteeAggregationBitsLengthTooShort; } @@ -138,7 +141,7 @@ pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconS return error.InvalidAttestationInvalidCommitteeIndex; } - const committee = try epoch_cache.getBeaconCommittee(slot, data.index); + const committee = try epoch_cache.getBeaconCommittee(data.slot, data.index); if (attestation.aggregation_bits.bit_len != committee.len) { return error.InvalidAttestationInvalidAggregationBitLen; } diff --git a/src/state_transition/block/process_attestations.zig b/src/state_transition/block/process_attestations.zig index 14c2ebc5c..ef03a8df5 100644 --- a/src/state_transition/block/process_attestations.zig +++ b/src/state_transition/block/process_attestations.zig @@ -18,7 +18,7 @@ pub fn processAttestations(allocator: Allocator, cached_state: *CachedBeaconStat .phase0 => |attestations_phase0| { if (state.isPostAltair()) { // altair to deneb - try processAttestationsAltair(allocator, cached_state, ssz.phase0.Attestation.Type, attestations_phase0.items, verify_signatures); + try processAttestationsAltair(allocator, cached_state, attestations_phase0.items, verify_signatures); } else { // phase0 for (attestations_phase0.items) |attestation| { @@ -27,7 +27,7 @@ pub fn processAttestations(allocator: Allocator, cached_state: *CachedBeaconStat } }, .electra => |attestations_electra| { - try processAttestationsAltair(allocator, cached_state, ssz.electra.Attestation.Type, attestations_electra.items, verify_signatures); + try processAttestationsAltair(allocator, cached_state, attestations_electra.items, verify_signatures); }, } } From df56c40b1526402f186662a4b04aca67859e507e Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 13:37:51 +0700 Subject: [PATCH 55/72] fix: active indices to compute proposers --- src/state_transition/cache/epoch_cache.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index 6a0532077..a4a24254c 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -424,7 +424,7 @@ pub const EpochCache = struct { var upcoming_proposer_seed: [32]u8 = undefined; try getSeed(state, upcoming_epoch, c.DOMAIN_BEACON_PROPOSER, &upcoming_proposer_seed); - try computeProposers(self.allocator, self.config.forkSeqAtEpoch(upcoming_epoch), upcoming_proposer_seed, upcoming_epoch, next_shuffling_active_indices, self.effective_balance_increment.get(), &self.proposers); + try computeProposers(self.allocator, self.config.forkSeqAtEpoch(upcoming_epoch), upcoming_proposer_seed, upcoming_epoch, self.current_shuffling.get().active_indices, self.effective_balance_increment.get(), &self.proposers); self.churn_limit = getChurnLimit(self.config, self.current_shuffling.get().active_indices.len); self.activation_churn_limit = getActivationChurnLimit(self.config, self.config.forkSeq(state.slot()), self.current_shuffling.get().active_indices.len); From cd09f4405900bace5a6f2931f67685ec89b4b7bc Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 13:52:23 +0700 Subject: [PATCH 56/72] fix: run minimal spec tests on CI --- .github/workflows/CI.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4e7788114..23b353a0c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -108,12 +108,15 @@ jobs: with: path: test/spec/spec_tests key: spec-test-data-${{ hashFiles('build.zig') }} # spec test version is defined in build.zig - - name: Download Spec Tests + - name: Download spec tests run: | zig build run:download_spec_tests - - name: Write Spec Tests + - name: Write spec tests run: | zig build run:write_spec_tests - - name: Run Spec Tests + - name: Run spec tests - minimal run: | - zig build test:spec_tests + zig build test:spec_tests -Dpreset=minimal + - name: Run spec tests - mainnet + run: | + zig build test:spec_tests -Dpreset=mainnet From e7f50b4163089c540078a4b66b8a1bf083f9d0a5 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 15:12:05 +0700 Subject: [PATCH 57/72] chore: loadPreState() and loadPostState() --- test/spec/runner/Operations.zig | 50 ++++--------------- test/spec/runner/Sanity.zig | 86 ++++++++------------------------- test/spec/test_case.zig | 51 ++++++++++++++++++- 3 files changed, 80 insertions(+), 107 deletions(-) diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig index d3e807773..bec56a6e2 100644 --- a/test/spec/runner/Operations.zig +++ b/test/spec/runner/Operations.zig @@ -10,6 +10,7 @@ const BeaconStateAllForks = state_transition.BeaconStateAllForks; const Withdrawals = ssz.capella.Withdrawals.Type; const WithdrawalsResult = state_transition.WithdrawalsResult; const test_case = @import("../test_case.zig"); +const TestCaseUtils = test_case.TestCaseUtils; const loadSszValue = test_case.loadSszSnappyValue; const loadBlsSetting = test_case.loadBlsSetting; const expectEqualBeaconStates = test_case.expectEqualBeaconStates; @@ -68,6 +69,7 @@ pub const Handler = Operation; pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { const ForkTypes = @field(ssz, fork.forkName()); + const tc_utils = TestCaseUtils(fork); const OpType = @field(ForkTypes, operation.operationObject()); return struct { @@ -93,8 +95,15 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { .op = OpType.default_value, .bls_setting = loadBlsSetting(allocator, dir), }; - // init the op + // load pre state + tc.pre = try tc_utils.loadPreState(allocator, dir); + errdefer tc.pre.deinit(); + + // load pre state + tc.post = try tc_utils.loadPostState(allocator, dir); + + // load the op try loadSszValue(OpType, allocator, dir, comptime operation.inputName() ++ ".ssz_snappy", &tc.op); errdefer { if (comptime @hasDecl(OpType, "deinit")) { @@ -102,45 +111,6 @@ pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation) type { } } - // init the pre state - - const pre_state = try allocator.create(ForkTypes.BeaconState.Type); - var transfered_pre_state: bool = false; - errdefer { - if (!transfered_pre_state) { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); - } - } - pre_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); - - transfered_pre_state = true; - - var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); - - errdefer tc.pre.deinit(); - - tc.post = null; - const post_exist = if (dir.statFile("post.ssz_snappy")) |_| true else |err| blk: { - if (err == error.FileNotFound) { - break :blk false; - } else { - return err; - } - }; - if (post_exist) { - const post_state = try allocator.create(ForkTypes.BeaconState.Type); - errdefer { - ForkTypes.BeaconState.deinit(allocator, post_state); - allocator.destroy(post_state); - } - post_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "post.ssz_snappy", post_state); - tc.post = try BeaconStateAllForks.init(fork, post_state); - } return tc; } diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig index e4fbd9440..3f68d1724 100644 --- a/test/spec/runner/Sanity.zig +++ b/test/spec/runner/Sanity.zig @@ -9,6 +9,7 @@ const CachedBeaconStateAllForks = state_transition.CachedBeaconStateAllForks; const test_case = @import("../test_case.zig"); const loadSszValue = test_case.loadSszSnappyValue; const expectEqualBeaconStates = test_case.expectEqualBeaconStates; +const TestCaseUtils = test_case.TestCaseUtils; /// https://github.com/ethereum/consensus-specs/blob/master/tests/formats/sanity/README.md pub const Handler = enum { @@ -23,7 +24,7 @@ pub const Handler = enum { }; pub fn SlotsTestCase(comptime fork: ForkSeq) type { - const ForkTypes = @field(ssz, fork.forkName()); + const tc_utils = TestCaseUtils(fork); return struct { pre: TestCachedBeaconStateAllForks, @@ -49,7 +50,15 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { .slots = 0, }; - // Load slots + // load pre state + tc.pre = try tc_utils.loadPreState(allocator, dir); + errdefer tc.pre.deinit(); + + // load post state + tc.post = try tc_utils.loadPostState(allocator, dir) orelse + return error.PostStateNotFound; + + // load slots var slots_file = try dir.openFile("slots.yaml", .{}); defer slots_file.close(); const slots_content = try slots_file.readToEndAlloc(allocator, 1024); @@ -57,33 +66,6 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { // Parse YAML for slots (simplified; assume single value) tc.slots = std.fmt.parseInt(u64, std.mem.trim(u8, slots_content, "... \n"), 10) catch 0; - // Load pre state - const pre_state = try allocator.create(ForkTypes.BeaconState.Type); - var transfered_pre_state: bool = false; - errdefer { - if (!transfered_pre_state) { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); - } - } - pre_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); - transfered_pre_state = true; - - var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); - errdefer tc.pre.deinit(); - - // Load post state - const post_state = try allocator.create(ForkTypes.BeaconState.Type); - errdefer { - ForkTypes.BeaconState.deinit(allocator, post_state); - allocator.destroy(post_state); - } - post_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "post.ssz_snappy", post_state); - tc.post = try BeaconStateAllForks.init(fork, post_state); - return tc; } @@ -110,6 +92,7 @@ pub fn SlotsTestCase(comptime fork: ForkSeq) type { pub fn BlocksTestCase(comptime fork: ForkSeq) type { const ForkTypes = @field(ssz, fork.forkName()); + const tc_utils = TestCaseUtils(fork); const SignedBeaconBlock = @field(ForkTypes, "SignedBeaconBlock"); return struct { @@ -137,6 +120,13 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { .blocks = undefined, }; + // load pre state + tc.pre = try tc_utils.loadPreState(allocator, dir); + errdefer tc.pre.deinit(); + + // load post state + tc.post = try tc_utils.loadPostState(allocator, dir); + // Load meta.yaml for blocks_count var meta_file = try dir.openFile("meta.yaml", .{}); defer meta_file.close(); @@ -149,24 +139,7 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { break :blk std.fmt.parseInt(usize, std.mem.trim(u8, num_str, " "), 10) catch 1; } else 1; - // Load pre state - const pre_state = try allocator.create(ForkTypes.BeaconState.Type); - var transfered_pre_state: bool = false; - errdefer { - if (!transfered_pre_state) { - ForkTypes.BeaconState.deinit(allocator, pre_state); - allocator.destroy(pre_state); - } - } - pre_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); - transfered_pre_state = true; - - var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); - tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); - errdefer tc.pre.deinit(); - - // Load blocks + // load blocks tc.blocks = try allocator.alloc(SignedBeaconBlock.Type, blocks_count); errdefer { for (tc.blocks) |*block| { @@ -181,25 +154,6 @@ pub fn BlocksTestCase(comptime fork: ForkSeq) type { try loadSszValue(SignedBeaconBlock, allocator, dir, block_filename, block); } - tc.post = null; - const post_exist = if (dir.statFile("post.ssz_snappy")) |_| true else |err| blk: { - if (err == error.FileNotFound) { - break :blk false; - } else { - return err; - } - }; - if (post_exist) { - const post_state = try allocator.create(ForkTypes.BeaconState.Type); - errdefer { - ForkTypes.BeaconState.deinit(allocator, post_state); - allocator.destroy(post_state); - } - post_state.* = ForkTypes.BeaconState.default_value; - try loadSszValue(ForkTypes.BeaconState, allocator, dir, "post.ssz_snappy", post_state); - tc.post = try BeaconStateAllForks.init(fork, post_state); - } - return tc; } diff --git a/test/spec/test_case.zig b/test/spec/test_case.zig index 7f7e20ed2..d518b4966 100644 --- a/test/spec/test_case.zig +++ b/test/spec/test_case.zig @@ -1,9 +1,13 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const snappy = @import("snappy"); const ForkSeq = @import("config").ForkSeq; const isFixedType = @import("ssz").isFixedType; -const BeaconStateAllForks = @import("state_transition").BeaconStateAllForks; +const state_transition = @import("state_transition"); +const BeaconStateAllForks = state_transition.BeaconStateAllForks; +const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; +const ssz = @import("consensus_types"); const consensus_types = @import("consensus_types"); const phase0 = consensus_types.phase0; const altair = consensus_types.altair; @@ -25,6 +29,51 @@ pub const BlsSetting = enum { } }; +pub fn TestCaseUtils(comptime fork: ForkSeq) type { + const ForkTypes = @field(ssz, fork.forkName()); + return struct { + pub fn loadPreState(allocator: Allocator, dir: std.fs.Dir) !TestCachedBeaconStateAllForks { + const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + var transfered_pre_state: bool = false; + errdefer { + if (!transfered_pre_state) { + ForkTypes.BeaconState.deinit(allocator, pre_state); + allocator.destroy(pre_state); + } + } + pre_state.* = ForkTypes.BeaconState.default_value; + try loadSszSnappyValue(ForkTypes.BeaconState, allocator, dir, "pre.ssz_snappy", pre_state); + transfered_pre_state = true; + + var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); + return try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks, fork, pre_state_all_forks.fork().epoch); + } + + /// consumer should deinit the returned state and destroy the pointer + pub fn loadPostState(allocator: Allocator, dir: std.fs.Dir) !?BeaconStateAllForks { + const post_exist = if (dir.statFile("post.ssz_snappy")) |_| true else |err| blk: { + if (err == error.FileNotFound) { + break :blk false; + } else { + return err; + } + }; + if (post_exist) { + const post_state = try allocator.create(ForkTypes.BeaconState.Type); + errdefer { + ForkTypes.BeaconState.deinit(allocator, post_state); + allocator.destroy(post_state); + } + post_state.* = ForkTypes.BeaconState.default_value; + try loadSszSnappyValue(ForkTypes.BeaconState, allocator, dir, "post.ssz_snappy", post_state); + return try BeaconStateAllForks.init(fork, post_state); + } else { + return null; + } + } + }; +} + pub fn loadBlsSetting(allocator: std.mem.Allocator, dir: std.fs.Dir) BlsSetting { var file = dir.openFile("meta.yaml", .{}) catch return .default; defer file.close(); From 2d24e30223709537a7314321588f02e950156a06 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 31 Oct 2025 15:43:09 +0700 Subject: [PATCH 58/72] chore: lower case spec test file names --- src/state_transition/block/process_withdrawals.zig | 2 +- test/spec/runner/{Operations.zig => operations.zig} | 0 test/spec/runner/{Sanity.zig => sanity.zig} | 0 test/spec/write_spec_tests.zig | 4 ++-- test/spec/writer/{Operations.zig => operations.zig} | 4 ++-- test/spec/writer/{Sanity.zig => sanity.zig} | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) rename test/spec/runner/{Operations.zig => operations.zig} (100%) rename test/spec/runner/{Sanity.zig => sanity.zig} (100%) rename test/spec/writer/{Operations.zig => operations.zig} (93%) rename test/spec/writer/{Sanity.zig => sanity.zig} (94%) diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 372320c68..d2c534c03 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -24,7 +24,7 @@ pub const WithdrawalsResult = struct { }; /// right now for the implementation we pass in processBlock() -/// for the spec, we pass in params from Operations.zig +/// for the spec, we pass in params from operations.zig /// TODO: spec and implementation should be the same /// refer to https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#modified-process_withdrawals pub fn processWithdrawals( diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/operations.zig similarity index 100% rename from test/spec/runner/Operations.zig rename to test/spec/runner/operations.zig diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/sanity.zig similarity index 100% rename from test/spec/runner/Sanity.zig rename to test/spec/runner/sanity.zig diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 7975ae9c6..6df673528 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -19,8 +19,8 @@ const supported_test_runners = [_]RunnerKind{ fn TestWriter(comptime kind: RunnerKind) type { return switch (kind) { - .operations => @import("./writer/Operations.zig"), - .sanity => @import("./writer/Sanity.zig"), + .operations => @import("./writer/operations.zig"), + .sanity => @import("./writer/sanity.zig"), else => @compileError("Unsupported test runner"), }; } diff --git a/test/spec/writer/Operations.zig b/test/spec/writer/operations.zig similarity index 93% rename from test/spec/writer/Operations.zig rename to test/spec/writer/operations.zig index 41889f25f..d6649f1ca 100644 --- a/test/spec/writer/Operations.zig +++ b/test/spec/writer/operations.zig @@ -2,7 +2,7 @@ const std = @import("std"); const spec_test_options = @import("spec_test_options"); const ForkSeq = @import("config").ForkSeq; const Preset = @import("preset").Preset; -const Handler = @import("../runner/Operations.zig").Handler; +const Handler = @import("../runner/operations.zig").Handler; pub const handlers = std.enums.values(Handler); @@ -14,7 +14,7 @@ pub const header = \\const ForkSeq = @import("config").ForkSeq; \\const active_preset = @import("preset").active_preset; \\const spec_test_options = @import("spec_test_options"); - \\const Operations = @import("../runner/Operations.zig"); + \\const Operations = @import("../runner/operations.zig"); \\ \\const allocator = std.testing.allocator; \\ diff --git a/test/spec/writer/Sanity.zig b/test/spec/writer/sanity.zig similarity index 94% rename from test/spec/writer/Sanity.zig rename to test/spec/writer/sanity.zig index 7c6ea4d46..c16394132 100644 --- a/test/spec/writer/Sanity.zig +++ b/test/spec/writer/sanity.zig @@ -2,7 +2,7 @@ const std = @import("std"); const spec_test_options = @import("spec_test_options"); const ForkSeq = @import("config").ForkSeq; const Preset = @import("preset").Preset; -const Handler = @import("../runner/Sanity.zig").Handler; +const Handler = @import("../runner/sanity.zig").Handler; pub const handlers = std.enums.values(Handler); @@ -14,7 +14,7 @@ pub const header = \\const ForkSeq = @import("config").ForkSeq; \\const active_preset = @import("preset").active_preset; \\const spec_test_options = @import("spec_test_options"); - \\const Sanity = @import("../runner/Sanity.zig"); + \\const Sanity = @import("../runner/sanity.zig"); \\ \\const allocator = std.testing.allocator; \\ From 8a0876f89fd2ace247b00277f271fbcfb7af1dcb Mon Sep 17 00:00:00 2001 From: grapebaba Date: Wed, 5 Nov 2025 18:06:29 +0800 Subject: [PATCH 59/72] feat: init merkle proof spec test Signed-off-by: grapebaba --- build.zig | 1 + test/spec/root.zig | 1 + test/spec/runner/merkle_proof.zig | 206 ++++++++++++++++++++++++++++++ test/spec/runner_kind.zig | 8 ++ test/spec/write_spec_tests.zig | 49 ++++--- test/spec/writer/merkle_proof.zig | 63 +++++++++ 6 files changed, 312 insertions(+), 16 deletions(-) create mode 100644 test/spec/runner/merkle_proof.zig create mode 100644 test/spec/writer/merkle_proof.zig diff --git a/build.zig b/build.zig index 9156fda78..03dd7d697 100644 --- a/build.zig +++ b/build.zig @@ -326,4 +326,5 @@ pub fn build(b: *std.Build) void { module_spec_tests.addImport("state_transition", module_state_transition); module_spec_tests.addImport("ssz", dep_ssz.module("ssz")); module_spec_tests.addImport("blst", dep_blst.module("blst")); + module_spec_tests.addImport("persistent_merkle_tree", dep_ssz.module("persistent_merkle_tree")); } diff --git a/test/spec/root.zig b/test/spec/root.zig index 619e1d2f4..35a74ce06 100644 --- a/test/spec/root.zig +++ b/test/spec/root.zig @@ -4,6 +4,7 @@ const testing = @import("std").testing; comptime { + testing.refAllDecls(@import("./test_case/merkle_proof_tests.zig")); testing.refAllDecls(@import("./test_case/operations_tests.zig")); testing.refAllDecls(@import("./test_case/sanity_tests.zig")); } diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig new file mode 100644 index 000000000..26b282783 --- /dev/null +++ b/test/spec/runner/merkle_proof.zig @@ -0,0 +1,206 @@ +const std = @import("std"); +const ct = @import("consensus_types"); +const ForkSeq = @import("config").ForkSeq; +const preset_mod = @import("preset"); +const test_case = @import("../test_case.zig"); +const loadSszValue = test_case.loadSszSnappyValue; + +const Root = ct.primitive.Root.Type; +const pmt = @import("persistent_merkle_tree"); +const Node = pmt.Node; +const NodeId = Node.Id; +const Gindex = pmt.Gindex; + +pub const Handler = enum { + single_merkle_proof, + + pub fn suiteName(self: Handler) []const u8 { + return @tagName(self); + } +}; + +const MerkleProof = struct { + leaf: Root, + leaf_index: u64, + branch: []Root, + + pub fn deinit(self: *MerkleProof, allocator: std.mem.Allocator) void { + allocator.free(self.branch); + } +}; + +pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { + _ = handler; + const ForkTypes = @field(ct, fork.forkName()); + const BeaconBlockBody = ForkTypes.BeaconBlockBody; + const KzgCommitment = ct.primitive.KZGCommitment; + + return struct { + body: BeaconBlockBody.Type, + proof: MerkleProof, + + const Self = @This(); + + pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { + var tc = try Self.init(allocator, dir); + defer tc.deinit(allocator); + + try tc.runTest(allocator); + } + + fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { + var body = BeaconBlockBody.default_value; + errdefer { + if (comptime @hasDecl(BeaconBlockBody, "deinit")) { + BeaconBlockBody.deinit(allocator, &body); + } + } + try loadSszValue(BeaconBlockBody, allocator, dir, "object.ssz_snappy", &body); + + const proof = try loadProof(allocator, dir); + errdefer proof.deinit(allocator); + + return .{ + .body = body, + .proof = proof, + }; + } + + fn deinit(self: *Self, allocator: std.mem.Allocator) void { + self.proof.deinit(allocator); + if (comptime @hasDecl(BeaconBlockBody, "deinit")) { + BeaconBlockBody.deinit(allocator, &self.body); + } + } + + fn runTest(self: *Self, allocator: std.mem.Allocator) !void { + try verifyLeaf(self); + try verifyBranch(allocator, self); + } + + fn verifyLeaf(self: *Self) !void { + // TODO: handle post-Fulu forks where blob_kzg_commitments is a list root, similar to Lodestar merkleProof tests. + const leaf_gindex_value = preset_mod.KZG_COMMITMENT_GINDEX0; + const actual_leaf_index: u64 = @intCast(leaf_gindex_value); + + try std.testing.expectEqual(self.proof.leaf_index, actual_leaf_index); + + if (self.body.blob_kzg_commitments.items.len == 0) { + return error.EmptyBlobKzgCommitments; + } + + var actual_leaf: Root = undefined; + try KzgCommitment.hashTreeRoot(&self.body.blob_kzg_commitments.items[0], &actual_leaf); + + try std.testing.expect(std.mem.eql(u8, &self.proof.leaf, &actual_leaf)); + } + + fn verifyBranch(allocator: std.mem.Allocator, self: *Self) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + + var pool = try Node.Pool.init(allocator, 2048); + defer pool.deinit(); + + const root_node = try BeaconBlockBody.tree.fromValue(arena_allocator, &pool, &self.body); + + var actual_branch: std.ArrayListUnmanaged(Root) = .empty; + defer actual_branch.deinit(allocator); + + try buildBranch(&pool, root_node, preset_mod.KZG_COMMITMENT_GINDEX0, allocator, &actual_branch); + + try std.testing.expectEqualSlices(Root, self.proof.branch, actual_branch.items); + } + + fn buildBranch( + pool: *Node.Pool, + root_node: Node.Id, + leaf_gindex_value: usize, + allocator: std.mem.Allocator, + branch_out: *std.ArrayListUnmanaged(Root), + ) !void { + // TODO: switch to a persistent_merkle_tree helper if/when one exists (e.g. getSingleProof). + const leaf_gindex = Gindex.fromUint(@as(Gindex.Uint, @intCast(leaf_gindex_value))); + + var current = leaf_gindex; + while (@intFromEnum(current) > 1) { + const sibling = if ((@intFromEnum(current) & 1) == 0) + @as(Gindex, @enumFromInt(@intFromEnum(current) + 1)) + else + @as(Gindex, @enumFromInt(@intFromEnum(current) - 1)); + + const sibling_node = try NodeId.getNode(root_node, pool, sibling); + const sibling_root = sibling_node.getRoot(pool); + try branch_out.append(allocator, sibling_root.*); + + current = @as(Gindex, @enumFromInt(@intFromEnum(current) >> 1)); + } + } + + fn loadProof(allocator: std.mem.Allocator, dir: std.fs.Dir) !MerkleProof { + var file = try dir.openFile("proof.yaml", .{}); + defer file.close(); + + const contents = try file.readToEndAlloc(allocator, 4096); + defer allocator.free(contents); + + return parseProofYaml(allocator, contents); + } + + fn parseProofYaml(allocator: std.mem.Allocator, contents: []const u8) !MerkleProof { + var proof = MerkleProof{ + .leaf = undefined, + .leaf_index = 0, + .branch = &.{}, + }; + + var branch: std.ArrayListUnmanaged(Root) = .empty; + errdefer branch.deinit(allocator); + + var leaf_parsed = false; + var index_parsed = false; + + var iter = std.mem.tokenizeScalar(u8, contents, '\n'); + while (iter.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \r\t"); + if (trimmed.len == 0) continue; + + if (std.mem.startsWith(u8, trimmed, "leaf:")) { + const value_slice = std.mem.trim(u8, trimmed["leaf:".len..], " \t"); + proof.leaf = try parseHexRoot(value_slice); + leaf_parsed = true; + } else if (std.mem.startsWith(u8, trimmed, "leaf_index:")) { + const value_slice = std.mem.trim(u8, trimmed["leaf_index:".len..], " \t"); + proof.leaf_index = try std.fmt.parseInt(u64, value_slice, 10); + index_parsed = true; + } else if (trimmed[0] == '-') { + const value_slice = std.mem.trim(u8, trimmed[1..], " '\t"); + const branch_value = try parseHexRoot(value_slice); + try branch.append(allocator, branch_value); + } + } + + if (!leaf_parsed or !index_parsed) { + return error.InvalidProof; + } + + proof.branch = try branch.toOwnedSlice(allocator); + return proof; + } + + fn parseHexRoot(raw_value: []const u8) !Root { + var value = std.mem.trim(u8, raw_value, " '\t\""); + if (std.mem.startsWith(u8, value, "0x")) { + value = value[2..]; + } + if (value.len != 64) { + return error.InvalidHexLength; + } + + var out: Root = undefined; + _ = try std.fmt.hexToBytes(out[0..], value); + return out; + } + }; +} diff --git a/test/spec/runner_kind.zig b/test/spec/runner_kind.zig index e3c10f2ba..eca3d1575 100644 --- a/test/spec/runner_kind.zig +++ b/test/spec/runner_kind.zig @@ -3,9 +3,17 @@ const std = @import("std"); pub const RunnerKind = enum { epoch_processing, finality, + merkle_proof, operations, random, rewards, sanity, shuffling, + + pub fn hasSuiteCase(comptime self: RunnerKind) bool { + return switch (self) { + .merkle_proof => true, + else => false, + }; + } }; diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 6df673528..473eb0ce9 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -13,12 +13,14 @@ const supported_forks = [_]ForkSeq{ }; const supported_test_runners = [_]RunnerKind{ + .merkle_proof, .operations, .sanity, }; fn TestWriter(comptime kind: RunnerKind) type { return switch (kind) { + .merkle_proof => @import("./writer/merkle_proof.zig"), .operations => @import("./writer/operations.zig"), .sanity => @import("./writer/sanity.zig"), else => @compileError("Unsupported test runner"), @@ -27,10 +29,7 @@ fn TestWriter(comptime kind: RunnerKind) type { pub fn main() !void { const test_case_dir = "test/spec/test_case/"; - std.fs.cwd().makeDir(test_case_dir) catch |err| { - if (err != error.PathAlreadyExists) return err; - // ignore if the directory already exists - }; + try std.fs.cwd().makePath(test_case_dir); inline for (supported_test_runners) |kind| { const test_case_file = test_case_dir ++ @tagName(kind) ++ "_tests.zig"; @@ -43,6 +42,7 @@ pub fn main() !void { { const test_root_file = "test/spec/root.zig"; + try std.fs.cwd().makePath("test/spec"); const out = try std.fs.cwd().createFile(test_root_file, .{}); defer out.close(); const writer = out.writer().any(); @@ -87,22 +87,39 @@ pub fn writeTests( defer preset_dir.close(); inline for (forks) |fork| { - var fork_dir = try preset_dir.openDir(@tagName(fork) ++ "/" ++ @tagName(kind), .{}); - defer fork_dir.close(); + const fork_path = @tagName(fork) ++ "/" ++ @tagName(kind); + const maybe_fork_dir = preset_dir.openDir(fork_path, .{ .iterate = true }) catch |err| switch (err) { + error.FileNotFound => null, + else => return err, + }; + + if (maybe_fork_dir) |dir| { + var fork_dir = dir; + defer fork_dir.close(); - inline for (TestWriter(kind).handlers) |handler| { - st: { - var suite_dir = fork_dir.openDir(comptime handler.suiteName(), .{ .iterate = true }) catch break :st; + inline for (TestWriter(kind).handlers) |handler| handler_loop: { + var suite_dir = fork_dir.openDir(comptime handler.suiteName(), .{ .iterate = true }) catch |err| switch (err) { + error.FileNotFound => break :handler_loop, + else => return err, + }; defer suite_dir.close(); - var test_case_iterator = suite_dir.iterate(); - while (try test_case_iterator.next()) |test_case_entry| { - if (test_case_entry.kind != .directory) { - continue; - } - const test_case_name = test_case_entry.name; + var suite_iter = suite_dir.iterate(); + while (try suite_iter.next()) |suite_entry| { + if (suite_entry.kind != .directory) continue; - try TestWriter(kind).writeTest(writer, fork, handler, test_case_name); + if (comptime kind.hasSuiteCase()) { + var case_dir = suite_dir.openDir(suite_entry.name, .{ .iterate = true }) catch continue; + defer case_dir.close(); + + var case_iter = case_dir.iterate(); + while (try case_iter.next()) |case_entry| { + if (case_entry.kind != .directory) continue; + try TestWriter(kind).writeTest(writer, fork, handler, suite_entry.name, case_entry.name); + } + } else { + try TestWriter(kind).writeTest(writer, fork, handler, suite_entry.name); + } } } } diff --git a/test/spec/writer/merkle_proof.zig b/test/spec/writer/merkle_proof.zig new file mode 100644 index 000000000..f2281f798 --- /dev/null +++ b/test/spec/writer/merkle_proof.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const spec_test_options = @import("spec_test_options"); +const ForkSeq = @import("config").ForkSeq; +const MerkleProof = @import("../runner/merkle_proof.zig"); + +pub const handlers = std.enums.values(MerkleProof.Handler); +pub const header = + \\// This file is generated by write_spec_tests.zig. + \\// Do not commit changes by hand. + \\ + \\const std = @import("std"); + \\const ForkSeq = @import("config").ForkSeq; + \\const active_preset = @import("preset").active_preset; + \\const spec_test_options = @import("spec_test_options"); + \\const MerkleProof = @import("../runner/merkle_proof.zig"); + \\ + \\const allocator = std.testing.allocator; + \\ + \\ +; + +const test_template = + \\test "{s} merkle_proof {s} {s} {s}" {{ + \\ const test_dir_name = try std.fs.path.join(allocator, &[_][]const u8{{ + \\ spec_test_options.spec_test_out_dir, + \\ spec_test_options.spec_test_version, + \\ @tagName(active_preset) ++ "/tests/" ++ @tagName(active_preset) ++ "/{s}/merkle_proof/{s}/{s}/{s}", + \\ }}); + \\ defer allocator.free(test_dir_name); + \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; + \\ + \\ try MerkleProof.TestCase(.{s}, .{s}).execute(allocator, test_dir); + \\}} + \\ + \\ +; + +pub fn writeHeader(writer: std.io.AnyWriter) !void { + try writer.print(header, .{}); +} + +pub fn writeTest( + writer: std.io.AnyWriter, + fork: ForkSeq, + handler: MerkleProof.Handler, + test_suite_name: []const u8, + test_case_name: []const u8, +) !void { + try writer.print(test_template, .{ + @tagName(fork), + @tagName(handler), + test_suite_name, + test_case_name, + + @tagName(fork), + @tagName(handler), + test_suite_name, + test_case_name, + + @tagName(fork), + @tagName(handler), + }); +} From 84a9408ceac680559cd2f8f83611027a2485cd63 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Wed, 12 Nov 2025 18:00:06 +0800 Subject: [PATCH 60/72] feat: init rewards spec test and fix bugs in rewards Signed-off-by: grapebaba --- .../cache/epoch_transition_cache.zig | 1 + src/state_transition/root.zig | 1 + src/state_transition/utils/finality.zig | 11 +- test/spec/root.zig | 1 + test/spec/runner/rewards.zig | 182 ++++++++++++++++++ test/spec/write_spec_tests.zig | 2 + test/spec/writer/rewards.zig | 58 ++++++ 7 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 test/spec/runner/rewards.zig create mode 100644 test/spec/writer/rewards.zig diff --git a/src/state_transition/cache/epoch_transition_cache.zig b/src/state_transition/cache/epoch_transition_cache.zig index e87283c31..f1da0d678 100644 --- a/src/state_transition/cache/epoch_transition_cache.zig +++ b/src/state_transition/cache/epoch_transition_cache.zig @@ -207,6 +207,7 @@ pub const EpochTransitionCache = struct { var indices_eligible_for_activation_queue = std.ArrayList(ValidatorIndex).init(allocator); // we will extract indices_eligible_for_activation from validator_activation_list later var validator_activation_list = ValidatorActivationList.init(allocator); + defer validator_activation_list.deinit(); var indices_to_eject = std.ArrayList(ValidatorIndex).init(allocator); var total_active_stake_by_increment: u64 = 0; diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index 4a152e1ee..d6672574f 100644 --- a/src/state_transition/root.zig +++ b/src/state_transition/root.zig @@ -25,6 +25,7 @@ pub const processInactivityUpdates = @import("./epoch/process_inactivity_updates pub const processRegistryUpdates = @import("./epoch/process_registry_updates.zig").processRegistryUpdates; pub const processSlashings = @import("./epoch/process_slashings.zig").processSlashings; pub const processRewardsAndPenalties = @import("./epoch/process_rewards_and_penalties.zig").processRewardsAndPenalties; +pub const getRewardsAndPenalties = @import("./epoch/process_rewards_and_penalties.zig").getRewardsAndPenalties; pub const processEth1DataReset = @import("./epoch/process_eth1_data_reset.zig").processEth1DataReset; pub const processPendingDeposits = @import("./epoch/process_pending_deposits.zig").processPendingDeposits; pub const processPendingConsolidations = @import("./epoch/process_pending_consolidations.zig").processPendingConsolidations; diff --git a/src/state_transition/utils/finality.zig b/src/state_transition/utils/finality.zig index 7483e9593..33fd35b52 100644 --- a/src/state_transition/utils/finality.zig +++ b/src/state_transition/utils/finality.zig @@ -1,13 +1,18 @@ const std = @import("std"); +const GENESIS_EPOCH = @import("preset").GENESIS_EPOCH; const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const preset = @import("preset").preset; const MIN_EPOCHS_TO_INACTIVITY_PENALTY = preset.MIN_EPOCHS_TO_INACTIVITY_PENALTY; pub fn getFinalityDelay(cached_state: *const CachedBeaconStateAllForks) u64 { - std.debug.assert(cached_state.getEpochCache().epoch > 0); - std.debug.assert(cached_state.getEpochCache().epoch >= cached_state.state.finalizedCheckpoint().epoch + 1); + const previous_epoch = if (cached_state.getEpochCache().epoch > GENESIS_EPOCH) + cached_state.getEpochCache().epoch - 1 + else + GENESIS_EPOCH; + std.debug.assert(previous_epoch >= cached_state.state.finalizedCheckpoint().epoch); + // previous_epoch = epoch - 1 - return cached_state.getEpochCache().epoch - 1 - cached_state.state.finalizedCheckpoint().epoch; + return previous_epoch - cached_state.state.finalizedCheckpoint().epoch; } /// If the chain has not been finalized for >4 epochs, the chain enters an "inactivity leak" mode, diff --git a/test/spec/root.zig b/test/spec/root.zig index 35a74ce06..75d20a757 100644 --- a/test/spec/root.zig +++ b/test/spec/root.zig @@ -6,5 +6,6 @@ const testing = @import("std").testing; comptime { testing.refAllDecls(@import("./test_case/merkle_proof_tests.zig")); testing.refAllDecls(@import("./test_case/operations_tests.zig")); + testing.refAllDecls(@import("./test_case/rewards_tests.zig")); testing.refAllDecls(@import("./test_case/sanity_tests.zig")); } diff --git a/test/spec/runner/rewards.zig b/test/spec/runner/rewards.zig new file mode 100644 index 000000000..a83c62a67 --- /dev/null +++ b/test/spec/runner/rewards.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const ct = @import("consensus_types"); +const ssz = @import("ssz"); +const ForkSeq = @import("config").ForkSeq; +const state_transition = @import("state_transition"); +const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; +const TestCaseUtils = @import("../test_case.zig").TestCaseUtils; +const loadSszValue = @import("../test_case.zig").loadSszSnappyValue; + +const EpochTransitionCache = state_transition.EpochTransitionCache; +const getRewardsAndPenaltiesFn = state_transition.getRewardsAndPenalties; + +const preset = @import("preset").preset; + +pub const Handler = enum { + basic, + leak, + random, + + pub inline fn suiteName(comptime self: Handler) []const u8 { + return @tagName(self) ++ "/pyspec_tests"; + } +}; + +pub fn TestCase(comptime fork: ForkSeq) type { + const Balances = ssz.FixedListType(ct.primitive.Gwei, preset.VALIDATOR_REGISTRY_LIMIT); + const DeltasType = ssz.VariableVectorType(Balances, 2); + const tc_utils = TestCaseUtils(fork); + + return struct { + pre: TestCachedBeaconStateAllForks, + source_deltas: DeltasType.Type, + target_deltas: DeltasType.Type, + head_deltas: DeltasType.Type, + inclusion_delay_deltas: DeltasType.Type, + has_inclusion_delay_deltas: bool, + inactivity_penalty_deltas: DeltasType.Type, + + const Self = @This(); + + pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { + var tc = try Self.init(allocator, dir); + defer tc.deinit(); + defer state_transition.deinitStateTransition(); + + try tc.runTest(); + } + + fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { + var tc = Self{ + .pre = undefined, + .source_deltas = DeltasType.default_value, + .target_deltas = DeltasType.default_value, + .head_deltas = DeltasType.default_value, + .inclusion_delay_deltas = DeltasType.default_value, + .has_inclusion_delay_deltas = false, + .inactivity_penalty_deltas = DeltasType.default_value, + }; + + tc.pre = try tc_utils.loadPreState(allocator, dir); + errdefer tc.pre.deinit(); + + const cache_allocator = tc.pre.allocator; + + tc.source_deltas = try Self.loadDeltas(cache_allocator, dir, "source_deltas.ssz_snappy"); + errdefer DeltasType.deinit(cache_allocator, &tc.source_deltas); + + tc.target_deltas = try Self.loadDeltas(cache_allocator, dir, "target_deltas.ssz_snappy"); + errdefer DeltasType.deinit(cache_allocator, &tc.target_deltas); + + tc.head_deltas = try Self.loadDeltas(cache_allocator, dir, "head_deltas.ssz_snappy"); + errdefer DeltasType.deinit(cache_allocator, &tc.head_deltas); + + if (try Self.loadOptionalDeltas(cache_allocator, dir, "inclusion_delay_deltas.ssz_snappy")) |deltas| { + tc.inclusion_delay_deltas = deltas; + tc.has_inclusion_delay_deltas = true; + errdefer DeltasType.deinit(cache_allocator, &tc.inclusion_delay_deltas); + } + + tc.inactivity_penalty_deltas = try Self.loadDeltas(cache_allocator, dir, "inactivity_penalty_deltas.ssz_snappy"); + errdefer DeltasType.deinit(cache_allocator, &tc.inactivity_penalty_deltas); + + return tc; + } + + fn deinit(self: *Self) void { + const allocator = self.pre.allocator; + DeltasType.deinit(allocator, &self.source_deltas); + DeltasType.deinit(allocator, &self.target_deltas); + DeltasType.deinit(allocator, &self.head_deltas); + DeltasType.deinit(allocator, &self.inclusion_delay_deltas); + DeltasType.deinit(allocator, &self.inactivity_penalty_deltas); + self.pre.deinit(); + } + + fn loadDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !DeltasType.Type { + var deltas = DeltasType.default_value; + loadSszValue(DeltasType, allocator, dir, filename, &deltas) catch |err| { + if (comptime @hasDecl(DeltasType, "deinit")) { + DeltasType.deinit(allocator, &deltas); + } + return err; + }; + return deltas; + } + + fn loadOptionalDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !?DeltasType.Type { + var deltas = DeltasType.default_value; + loadSszValue(DeltasType, allocator, dir, filename, &deltas) catch |err| switch (err) { + error.FileNotFound => { + if (comptime @hasDecl(DeltasType, "deinit")) { + DeltasType.deinit(allocator, &deltas); + } + return null; + }, + else => { + if (comptime @hasDecl(DeltasType, "deinit")) { + DeltasType.deinit(allocator, &deltas); + } + return err; + }, + }; + return deltas; + } + + fn runTest(self: *Self) !void { + const allocator = self.pre.allocator; + const cloned_state = try self.pre.cached_state.clone(allocator); + defer { + cloned_state.deinit(); + allocator.destroy(cloned_state); + } + + var epoch_cache = try EpochTransitionCache.init(allocator, cloned_state); + defer { + epoch_cache.deinit(); + allocator.destroy(epoch_cache); + } + + try getRewardsAndPenaltiesFn(allocator, cloned_state, epoch_cache, epoch_cache.rewards, epoch_cache.penalties); + + const validator_count = self.pre.cached_state.state.validators().items.len; + const rewards = epoch_cache.rewards; + const penalties = epoch_cache.penalties; + + const expected_rewards = try allocator.alloc(u64, validator_count); + defer allocator.free(expected_rewards); + const expected_penalties = try allocator.alloc(u64, validator_count); + defer allocator.free(expected_penalties); + @memset(expected_rewards, 0); + @memset(expected_penalties, 0); + + try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.source_deltas); + try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.target_deltas); + try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.head_deltas); + if (self.has_inclusion_delay_deltas) { + try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.inclusion_delay_deltas); + } + try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.inactivity_penalty_deltas); + + try std.testing.expectEqualSlices(u64, expected_rewards, rewards); + try std.testing.expectEqualSlices(u64, expected_penalties, penalties); + } + + fn accumulateDeltas(expected_rewards: []u64, expected_penalties: []u64, deltas: *const DeltasType.Type) !void { + const values = deltas.*; + const rewards = values[0].items; + const penalties = values[1].items; + + if (rewards.len != expected_rewards.len or penalties.len != expected_penalties.len) { + return error.InvalidDeltaLength; + } + + for (rewards, 0..) |value, i| { + expected_rewards[i] += value; + } + for (penalties, 0..) |value, i| { + expected_penalties[i] += value; + } + } + }; +} diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 473eb0ce9..41028a4d8 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -15,6 +15,7 @@ const supported_forks = [_]ForkSeq{ const supported_test_runners = [_]RunnerKind{ .merkle_proof, .operations, + .rewards, .sanity, }; @@ -22,6 +23,7 @@ fn TestWriter(comptime kind: RunnerKind) type { return switch (kind) { .merkle_proof => @import("./writer/merkle_proof.zig"), .operations => @import("./writer/operations.zig"), + .rewards => @import("./writer/rewards.zig"), .sanity => @import("./writer/sanity.zig"), else => @compileError("Unsupported test runner"), }; diff --git a/test/spec/writer/rewards.zig b/test/spec/writer/rewards.zig new file mode 100644 index 000000000..f79dc63c2 --- /dev/null +++ b/test/spec/writer/rewards.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const spec_test_options = @import("spec_test_options"); +const ForkSeq = @import("config").ForkSeq; +const Rewards = @import("../runner/rewards.zig"); + +pub const handlers = std.enums.values(Rewards.Handler); +pub const header = + \\// This file is generated by write_spec_tests.zig. + \\// Do not commit changes by hand. + \\ + \\const std = @import("std"); + \\const ForkSeq = @import("config").ForkSeq; + \\const active_preset = @import("preset").active_preset; + \\const spec_test_options = @import("spec_test_options"); + \\const Rewards = @import("../runner/rewards.zig"); + \\ + \\const allocator = std.testing.allocator; + \\ + \\ +; + +const test_template = + \\test "{s} rewards {s} {s}" {{ + \\ const test_dir_name = try std.fs.path.join(allocator, &[_][]const u8{{ + \\ spec_test_options.spec_test_out_dir, + \\ spec_test_options.spec_test_version, + \\ @tagName(active_preset) ++ "/tests/" ++ @tagName(active_preset) ++ "/{s}/rewards/{s}/{s}", + \\ }}); + \\ defer allocator.free(test_dir_name); + \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; + \\ + \\ try Rewards.TestCase(.{s}).execute(allocator, test_dir); + \\}} + \\ + \\ +; + +pub fn writeHeader(writer: std.io.AnyWriter) !void { + try writer.print(header, .{}); +} + +pub fn writeTest( + writer: std.io.AnyWriter, + fork: ForkSeq, + comptime handler: Rewards.Handler, + test_case_name: []const u8, +) !void { + const handler_suite = handler.suiteName(); + try writer.print(test_template, .{ + @tagName(fork), + @tagName(handler), + test_case_name, + @tagName(fork), + handler_suite, + test_case_name, + @tagName(fork), + }); +} From fd5a10840c9a85ca59ec644a7af7797184f28bec Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 13 Nov 2025 22:12:51 +0800 Subject: [PATCH 61/72] fix: fix merge issue Signed-off-by: grapebaba --- src/state_transition/block/process_attestation_altair.zig | 2 +- test/spec/write_spec_tests.zig | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/state_transition/block/process_attestation_altair.zig b/src/state_transition/block/process_attestation_altair.zig index fbcd6732e..1382828ed 100644 --- a/src/state_transition/block/process_attestation_altair.zig +++ b/src/state_transition/block/process_attestation_altair.zig @@ -46,7 +46,7 @@ pub fn processAttestationsAltair(allocator: Allocator, cached_state: *const Cach var proposer_reward: u64 = 0; for (attestations) |*attestation| { const data = attestation.data; - try validateAttestation(cached_state, attestation); + try validateAttestation(AT, cached_state, attestation); // Retrieve the validator indices from the attestation participation bitfield const attesting_indices = try if (AT == Phase0Attestation) epoch_cache.getAttestingIndicesPhase0(attestation) else epoch_cache.getAttestingIndicesElectra(attestation); diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 8fbb521f6..41028a4d8 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -21,6 +21,7 @@ const supported_test_runners = [_]RunnerKind{ fn TestWriter(comptime kind: RunnerKind) type { return switch (kind) { + .merkle_proof => @import("./writer/merkle_proof.zig"), .operations => @import("./writer/operations.zig"), .rewards => @import("./writer/rewards.zig"), .sanity => @import("./writer/sanity.zig"), From 8a9bc4af9f8f96a1876ae153cc761562bc537f60 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Fri, 14 Nov 2025 13:22:30 +0800 Subject: [PATCH 62/72] fix: add pmt dep into spec test Signed-off-by: grapebaba --- build.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/build.zig b/build.zig index b99c5f6b1..38fb3bdea 100644 --- a/build.zig +++ b/build.zig @@ -780,6 +780,7 @@ pub fn build(b: *std.Build) void { module_spec_tests.addImport("state_transition", module_state_transition); module_spec_tests.addImport("ssz", module_ssz); module_spec_tests.addImport("blst", dep_blst.module("blst")); + module_spec_tests.addImport("persistent_merkle_tree", module_persistent_merkle_tree); module_ssz_generic_spec_tests.addImport("hex", module_hex); module_ssz_generic_spec_tests.addImport("snappy", dep_snappy.module("snappy")); From 3dd89f0a7b8b2776ad40b425469fb32c4c826f44 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Fri, 14 Nov 2025 13:30:34 +0800 Subject: [PATCH 63/72] fix: sync zbuild.zon Signed-off-by: grapebaba --- zbuild.zon | 1 + 1 file changed, 1 insertion(+) diff --git a/zbuild.zon b/zbuild.zon index 35928914b..3e9dceb30 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -187,6 +187,7 @@ .state_transition, .ssz, .blst, + .persistent_merkle_tree, }, }, .filters = .{}, From f4d8c11931e03a000dcfa304e1a22f499515d775 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 10:56:29 +0800 Subject: [PATCH 64/72] fix: simplify the proof spec test Signed-off-by: grapebaba --- build.zig | 1 + src/ssz/type/byte_list.zig | 1 + test/spec/runner/merkle_proof.zig | 130 ++++++++++-------------------- 3 files changed, 43 insertions(+), 89 deletions(-) diff --git a/build.zig b/build.zig index 38fb3bdea..f837d652a 100644 --- a/build.zig +++ b/build.zig @@ -781,6 +781,7 @@ pub fn build(b: *std.Build) void { module_spec_tests.addImport("ssz", module_ssz); module_spec_tests.addImport("blst", dep_blst.module("blst")); module_spec_tests.addImport("persistent_merkle_tree", module_persistent_merkle_tree); + module_spec_tests.addImport("hex", module_hex); module_ssz_generic_spec_tests.addImport("hex", module_hex); module_ssz_generic_spec_tests.addImport("snappy", dep_snappy.module("snappy")); diff --git a/src/ssz/type/byte_list.zig b/src/ssz/type/byte_list.zig index 1d14d89ef..48643d9a9 100644 --- a/src/ssz/type/byte_list.zig +++ b/src/ssz/type/byte_list.zig @@ -147,6 +147,7 @@ pub fn ByteListType(comptime _limit: comptime_int) type { } const nodes = try allocator.alloc(Node.Id, chunk_count); + defer allocator.free(nodes); for (0..chunk_count) |i| { var leaf_buf = [_]u8{0} ** 32; const start_idx = i * 32; diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index 26b282783..d1d457113 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -4,11 +4,11 @@ const ForkSeq = @import("config").ForkSeq; const preset_mod = @import("preset"); const test_case = @import("../test_case.zig"); const loadSszValue = test_case.loadSszSnappyValue; +const hex = @import("hex"); const Root = ct.primitive.Root.Type; const pmt = @import("persistent_merkle_tree"); const Node = pmt.Node; -const NodeId = Node.Id; const Gindex = pmt.Gindex; pub const Handler = enum { @@ -20,9 +20,9 @@ pub const Handler = enum { }; const MerkleProof = struct { - leaf: Root, + leaf: [66]u8, leaf_index: u64, - branch: []Root, + branch: [][66]u8, pub fn deinit(self: *MerkleProof, allocator: std.mem.Allocator) void { allocator.free(self.branch); @@ -74,67 +74,26 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { } fn runTest(self: *Self, allocator: std.mem.Allocator) !void { - try verifyLeaf(self); - try verifyBranch(allocator, self); - } - - fn verifyLeaf(self: *Self) !void { - // TODO: handle post-Fulu forks where blob_kzg_commitments is a list root, similar to Lodestar merkleProof tests. - const leaf_gindex_value = preset_mod.KZG_COMMITMENT_GINDEX0; - const actual_leaf_index: u64 = @intCast(leaf_gindex_value); - - try std.testing.expectEqual(self.proof.leaf_index, actual_leaf_index); - - if (self.body.blob_kzg_commitments.items.len == 0) { - return error.EmptyBlobKzgCommitments; - } - - var actual_leaf: Root = undefined; + const actual_leaf_index: u64 = @intCast(preset_mod.KZG_COMMITMENT_GINDEX0); + var actual_leaf: [32]u8 = undefined; try KzgCommitment.hashTreeRoot(&self.body.blob_kzg_commitments.items[0], &actual_leaf); - - try std.testing.expect(std.mem.eql(u8, &self.proof.leaf, &actual_leaf)); - } - - fn verifyBranch(allocator: std.mem.Allocator, self: *Self) !void { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - const arena_allocator = arena.allocator(); + const actual_leaf_hex = try hex.rootToHex(&actual_leaf); var pool = try Node.Pool.init(allocator, 2048); defer pool.deinit(); - const root_node = try BeaconBlockBody.tree.fromValue(arena_allocator, &pool, &self.body); - - var actual_branch: std.ArrayListUnmanaged(Root) = .empty; - defer actual_branch.deinit(allocator); + const root_node = try BeaconBlockBody.tree.fromValue(allocator, &pool, &self.body); + const gindex = Gindex.fromUint(@as(Gindex.Uint, actual_leaf_index)); - try buildBranch(&pool, root_node, preset_mod.KZG_COMMITMENT_GINDEX0, allocator, &actual_branch); + var single_proof = try pmt.proof.createSingleProof(allocator, &pool, root_node, gindex); + defer single_proof.deinit(allocator); - try std.testing.expectEqualSlices(Root, self.proof.branch, actual_branch.items); - } - - fn buildBranch( - pool: *Node.Pool, - root_node: Node.Id, - leaf_gindex_value: usize, - allocator: std.mem.Allocator, - branch_out: *std.ArrayListUnmanaged(Root), - ) !void { - // TODO: switch to a persistent_merkle_tree helper if/when one exists (e.g. getSingleProof). - const leaf_gindex = Gindex.fromUint(@as(Gindex.Uint, @intCast(leaf_gindex_value))); - - var current = leaf_gindex; - while (@intFromEnum(current) > 1) { - const sibling = if ((@intFromEnum(current) & 1) == 0) - @as(Gindex, @enumFromInt(@intFromEnum(current) + 1)) - else - @as(Gindex, @enumFromInt(@intFromEnum(current) - 1)); - - const sibling_node = try NodeId.getNode(root_node, pool, sibling); - const sibling_root = sibling_node.getRoot(pool); - try branch_out.append(allocator, sibling_root.*); - - current = @as(Gindex, @enumFromInt(@intFromEnum(current) >> 1)); + try std.testing.expectEqual(self.proof.leaf_index, actual_leaf_index); + try std.testing.expectEqualSlices(u8, self.proof.leaf[0..66], &actual_leaf_hex); + try std.testing.expectEqual(self.proof.branch.len, single_proof.witnesses.len); + for (self.proof.branch, 0..) |expected_witness, i| { + const actual_witness_hex = try hex.rootToHex(&single_proof.witnesses[i]); + try std.testing.expectEqualSlices(u8, expected_witness[0..66], &actual_witness_hex); } } @@ -149,34 +108,32 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { } fn parseProofYaml(allocator: std.mem.Allocator, contents: []const u8) !MerkleProof { - var proof = MerkleProof{ - .leaf = undefined, - .leaf_index = 0, - .branch = &.{}, - }; - - var branch: std.ArrayListUnmanaged(Root) = .empty; + var branch: std.ArrayListUnmanaged([66]u8) = .empty; errdefer branch.deinit(allocator); + var leaf: [66]u8 = undefined; + var leaf_index: u64 = 0; var leaf_parsed = false; var index_parsed = false; var iter = std.mem.tokenizeScalar(u8, contents, '\n'); + const quote = "'\""; while (iter.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \r\t"); - if (trimmed.len == 0) continue; + if (line.len == 0) continue; - if (std.mem.startsWith(u8, trimmed, "leaf:")) { - const value_slice = std.mem.trim(u8, trimmed["leaf:".len..], " \t"); - proof.leaf = try parseHexRoot(value_slice); + if (std.mem.startsWith(u8, line, "leaf: ")) { + const value_slice = std.mem.trim(u8, line["leaf: ".len..], quote); + std.debug.assert(value_slice.len == 66); + leaf = value_slice[0..66].*; leaf_parsed = true; - } else if (std.mem.startsWith(u8, trimmed, "leaf_index:")) { - const value_slice = std.mem.trim(u8, trimmed["leaf_index:".len..], " \t"); - proof.leaf_index = try std.fmt.parseInt(u64, value_slice, 10); + } else if (std.mem.startsWith(u8, line, "leaf_index: ")) { + const value_slice = std.mem.trim(u8, line["leaf_index: ".len..], quote); + leaf_index = try std.fmt.parseInt(u64, value_slice, 10); index_parsed = true; - } else if (trimmed[0] == '-') { - const value_slice = std.mem.trim(u8, trimmed[1..], " '\t"); - const branch_value = try parseHexRoot(value_slice); + } else if (std.mem.startsWith(u8, line, "- ")) { + const value_slice = std.mem.trim(u8, line[2..], quote); + std.debug.assert(value_slice.len == 66); + const branch_value = value_slice[0..66].*; try branch.append(allocator, branch_value); } } @@ -185,22 +142,17 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { return error.InvalidProof; } - proof.branch = try branch.toOwnedSlice(allocator); - return proof; - } - - fn parseHexRoot(raw_value: []const u8) !Root { - var value = std.mem.trim(u8, raw_value, " '\t\""); - if (std.mem.startsWith(u8, value, "0x")) { - value = value[2..]; - } - if (value.len != 64) { - return error.InvalidHexLength; + const gindex = Gindex.fromUint(@as(Gindex.Uint, leaf_index)); + const expected_branch_len: usize = @intCast(gindex.pathLen()); + if (branch.items.len != expected_branch_len) { + return error.InvalidProof; } - var out: Root = undefined; - _ = try std.fmt.hexToBytes(out[0..], value); - return out; + return .{ + .leaf = leaf, + .leaf_index = leaf_index, + .branch = try branch.toOwnedSlice(allocator), + }; } }; } From e79696793721faef2c2fd7853ef87b5a703d0488 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 11:31:21 +0800 Subject: [PATCH 65/72] refactor: refactor the test structure Signed-off-by: grapebaba --- test/spec/runner/merkle_proof.zig | 57 +++++++++++++++++++++++-------- test/spec/writer/merkle_proof.zig | 5 ++- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index d1d457113..60699632d 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -6,8 +6,8 @@ const test_case = @import("../test_case.zig"); const loadSszValue = test_case.loadSszSnappyValue; const hex = @import("hex"); -const Root = ct.primitive.Root.Type; const pmt = @import("persistent_merkle_tree"); +const proof = pmt.proof; const Node = pmt.Node; const Gindex = pmt.Gindex; @@ -29,8 +29,7 @@ const MerkleProof = struct { } }; -pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { - _ = handler; +pub fn TestCase(comptime fork: ForkSeq) type { const ForkTypes = @field(ct, fork.forkName()); const BeaconBlockBody = ForkTypes.BeaconBlockBody; const KzgCommitment = ct.primitive.KZGCommitment; @@ -57,12 +56,12 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { } try loadSszValue(BeaconBlockBody, allocator, dir, "object.ssz_snappy", &body); - const proof = try loadProof(allocator, dir); - errdefer proof.deinit(allocator); + var proof_data: MerkleProof = undefined; + try loadProof(allocator, dir, &proof_data); return .{ .body = body, - .proof = proof, + .proof = proof_data, }; } @@ -77,7 +76,6 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { const actual_leaf_index: u64 = @intCast(preset_mod.KZG_COMMITMENT_GINDEX0); var actual_leaf: [32]u8 = undefined; try KzgCommitment.hashTreeRoot(&self.body.blob_kzg_commitments.items[0], &actual_leaf); - const actual_leaf_hex = try hex.rootToHex(&actual_leaf); var pool = try Node.Pool.init(allocator, 2048); defer pool.deinit(); @@ -88,23 +86,52 @@ pub fn TestCase(comptime fork: ForkSeq, comptime handler: Handler) type { var single_proof = try pmt.proof.createSingleProof(allocator, &pool, root_node, gindex); defer single_proof.deinit(allocator); - try std.testing.expectEqual(self.proof.leaf_index, actual_leaf_index); - try std.testing.expectEqualSlices(u8, self.proof.leaf[0..66], &actual_leaf_hex); - try std.testing.expectEqual(self.proof.branch.len, single_proof.witnesses.len); - for (self.proof.branch, 0..) |expected_witness, i| { - const actual_witness_hex = try hex.rootToHex(&single_proof.witnesses[i]); - try std.testing.expectEqualSlices(u8, expected_witness[0..66], &actual_witness_hex); + var actual_proof = try buildActualProof(allocator, actual_leaf_index, &actual_leaf, single_proof.witnesses); + defer actual_proof.deinit(allocator); + + try expectEqualProof(&self.proof, &actual_proof); + } + + fn buildActualProof( + allocator: std.mem.Allocator, + leaf_index: u64, + leaf_bytes: *const [32]u8, + witnesses: [][32]u8, + ) !MerkleProof { + var branch = try allocator.alloc([66]u8, witnesses.len); + errdefer allocator.free(branch); + + for (witnesses, 0..) |witness, i| { + branch[i] = try hex.rootToHex(&witness); + } + + return .{ + .leaf = try hex.rootToHex(leaf_bytes), + .leaf_index = leaf_index, + .branch = branch, + }; + } + + fn expectEqualProof( + expected: *const MerkleProof, + actual: *const MerkleProof, + ) !void { + try std.testing.expectEqual(expected.leaf_index, actual.leaf_index); + try std.testing.expectEqualSlices(u8, expected.leaf[0..66], actual.leaf[0..66]); + try std.testing.expectEqual(expected.branch.len, actual.branch.len); + for (expected.branch, 0..) |expected_witness, i| { + try std.testing.expectEqualSlices(u8, expected_witness[0..66], actual.branch[i][0..66]); } } - fn loadProof(allocator: std.mem.Allocator, dir: std.fs.Dir) !MerkleProof { + fn loadProof(allocator: std.mem.Allocator, dir: std.fs.Dir, out: *MerkleProof) !void { var file = try dir.openFile("proof.yaml", .{}); defer file.close(); const contents = try file.readToEndAlloc(allocator, 4096); defer allocator.free(contents); - return parseProofYaml(allocator, contents); + out.* = try parseProofYaml(allocator, contents); } fn parseProofYaml(allocator: std.mem.Allocator, contents: []const u8) !MerkleProof { diff --git a/test/spec/writer/merkle_proof.zig b/test/spec/writer/merkle_proof.zig index f2281f798..2fa60d424 100644 --- a/test/spec/writer/merkle_proof.zig +++ b/test/spec/writer/merkle_proof.zig @@ -29,7 +29,7 @@ const test_template = \\ defer allocator.free(test_dir_name); \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; \\ - \\ try MerkleProof.TestCase(.{s}, .{s}).execute(allocator, test_dir); + \\ try MerkleProof.TestCase(.{s}).execute(allocator, test_dir); \\}} \\ \\ @@ -48,7 +48,6 @@ pub fn writeTest( ) !void { try writer.print(test_template, .{ @tagName(fork), - @tagName(handler), test_suite_name, test_case_name, @@ -58,6 +57,6 @@ pub fn writeTest( test_case_name, @tagName(fork), - @tagName(handler), + @tagName(fork), }); } From 34e9f51feae98bd1f8e27c9147cecfb9dc58502e Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 12:27:49 +0800 Subject: [PATCH 66/72] refactor: refactor rewards spec test Signed-off-by: grapebaba --- test/spec/runner/merkle_proof.zig | 17 ++-- test/spec/runner/rewards.zig | 150 +++++++++++++++--------------- 2 files changed, 81 insertions(+), 86 deletions(-) diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index 60699632d..aa0362e63 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -137,11 +137,8 @@ pub fn TestCase(comptime fork: ForkSeq) type { fn parseProofYaml(allocator: std.mem.Allocator, contents: []const u8) !MerkleProof { var branch: std.ArrayListUnmanaged([66]u8) = .empty; errdefer branch.deinit(allocator); - var leaf: [66]u8 = undefined; - var leaf_index: u64 = 0; - - var leaf_parsed = false; - var index_parsed = false; + var leaf: ?[66]u8 = null; + var leaf_index: ?u64 = null; var iter = std.mem.tokenizeScalar(u8, contents, '\n'); const quote = "'\""; @@ -152,11 +149,9 @@ pub fn TestCase(comptime fork: ForkSeq) type { const value_slice = std.mem.trim(u8, line["leaf: ".len..], quote); std.debug.assert(value_slice.len == 66); leaf = value_slice[0..66].*; - leaf_parsed = true; } else if (std.mem.startsWith(u8, line, "leaf_index: ")) { const value_slice = std.mem.trim(u8, line["leaf_index: ".len..], quote); leaf_index = try std.fmt.parseInt(u64, value_slice, 10); - index_parsed = true; } else if (std.mem.startsWith(u8, line, "- ")) { const value_slice = std.mem.trim(u8, line[2..], quote); std.debug.assert(value_slice.len == 66); @@ -165,19 +160,19 @@ pub fn TestCase(comptime fork: ForkSeq) type { } } - if (!leaf_parsed or !index_parsed) { + if (leaf == null or leaf_index == null) { return error.InvalidProof; } - const gindex = Gindex.fromUint(@as(Gindex.Uint, leaf_index)); + const gindex = Gindex.fromUint(@as(Gindex.Uint, leaf_index.?)); const expected_branch_len: usize = @intCast(gindex.pathLen()); if (branch.items.len != expected_branch_len) { return error.InvalidProof; } return .{ - .leaf = leaf, - .leaf_index = leaf_index, + .leaf = leaf.?, + .leaf_index = leaf_index.?, .branch = try branch.toOwnedSlice(allocator), }; } diff --git a/test/spec/runner/rewards.zig b/test/spec/runner/rewards.zig index a83c62a67..070906ee0 100644 --- a/test/spec/runner/rewards.zig +++ b/test/spec/runner/rewards.zig @@ -29,96 +29,112 @@ pub fn TestCase(comptime fork: ForkSeq) type { return struct { pre: TestCachedBeaconStateAllForks, - source_deltas: DeltasType.Type, - target_deltas: DeltasType.Type, - head_deltas: DeltasType.Type, - inclusion_delay_deltas: DeltasType.Type, - has_inclusion_delay_deltas: bool, - inactivity_penalty_deltas: DeltasType.Type, + expected_rewards: []u64, + expected_penalties: []u64, const Self = @This(); pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { var tc = try Self.init(allocator, dir); - defer tc.deinit(); - defer state_transition.deinitStateTransition(); + defer { + tc.deinit(); + state_transition.deinitStateTransition(); + } try tc.runTest(); } fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { - var tc = Self{ - .pre = undefined, - .source_deltas = DeltasType.default_value, - .target_deltas = DeltasType.default_value, - .head_deltas = DeltasType.default_value, - .inclusion_delay_deltas = DeltasType.default_value, - .has_inclusion_delay_deltas = false, - .inactivity_penalty_deltas = DeltasType.default_value, - }; + var pre_state = try tc_utils.loadPreState(allocator, dir); + errdefer pre_state.deinit(); - tc.pre = try tc_utils.loadPreState(allocator, dir); - errdefer tc.pre.deinit(); + const cache_allocator = pre_state.allocator; + const validator_count = pre_state.cached_state.state.validators().items.len; + const expected = try Self.buildExpectedRewardsPenalties(cache_allocator, dir, validator_count); - const cache_allocator = tc.pre.allocator; + return .{ + .pre = pre_state, + .expected_rewards = expected.rewards, + .expected_penalties = expected.penalties, + }; + } - tc.source_deltas = try Self.loadDeltas(cache_allocator, dir, "source_deltas.ssz_snappy"); - errdefer DeltasType.deinit(cache_allocator, &tc.source_deltas); + fn deinit(self: *Self) void { + const allocator = self.pre.allocator; + allocator.free(self.expected_rewards); + allocator.free(self.expected_penalties); + self.pre.deinit(); + } - tc.target_deltas = try Self.loadDeltas(cache_allocator, dir, "target_deltas.ssz_snappy"); - errdefer DeltasType.deinit(cache_allocator, &tc.target_deltas); + fn buildExpectedRewardsPenalties( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + validator_count: usize, + ) !struct { rewards: []u64, penalties: []u64 } { + const expected_rewards = try allocator.alloc(u64, validator_count); + errdefer allocator.free(expected_rewards); + const expected_penalties = try allocator.alloc(u64, validator_count); + errdefer allocator.free(expected_penalties); - tc.head_deltas = try Self.loadDeltas(cache_allocator, dir, "head_deltas.ssz_snappy"); - errdefer DeltasType.deinit(cache_allocator, &tc.head_deltas); + @memset(expected_rewards, 0); + @memset(expected_penalties, 0); - if (try Self.loadOptionalDeltas(cache_allocator, dir, "inclusion_delay_deltas.ssz_snappy")) |deltas| { - tc.inclusion_delay_deltas = deltas; - tc.has_inclusion_delay_deltas = true; - errdefer DeltasType.deinit(cache_allocator, &tc.inclusion_delay_deltas); - } + try Self.accumulateFromFile(expected_rewards, expected_penalties, allocator, dir, "source_deltas.ssz_snappy"); + try Self.accumulateFromFile(expected_rewards, expected_penalties, allocator, dir, "target_deltas.ssz_snappy"); + try Self.accumulateFromFile(expected_rewards, expected_penalties, allocator, dir, "head_deltas.ssz_snappy"); + try Self.accumulateFromOptionalFile(expected_rewards, expected_penalties, allocator, dir, "inclusion_delay_deltas.ssz_snappy"); + try Self.accumulateFromFile(expected_rewards, expected_penalties, allocator, dir, "inactivity_penalty_deltas.ssz_snappy"); - tc.inactivity_penalty_deltas = try Self.loadDeltas(cache_allocator, dir, "inactivity_penalty_deltas.ssz_snappy"); - errdefer DeltasType.deinit(cache_allocator, &tc.inactivity_penalty_deltas); + return .{ .rewards = expected_rewards, .penalties = expected_penalties }; + } - return tc; + fn accumulateFromFile( + expected_rewards: []u64, + expected_penalties: []u64, + allocator: std.mem.Allocator, + dir: std.fs.Dir, + comptime filename: []const u8, + ) !void { + var deltas = try Self.loadDeltas(allocator, dir, filename); + defer DeltasType.deinit(allocator, &deltas); + try Self.accumulateDeltas(expected_rewards, expected_penalties, &deltas); } - fn deinit(self: *Self) void { - const allocator = self.pre.allocator; - DeltasType.deinit(allocator, &self.source_deltas); - DeltasType.deinit(allocator, &self.target_deltas); - DeltasType.deinit(allocator, &self.head_deltas); - DeltasType.deinit(allocator, &self.inclusion_delay_deltas); - DeltasType.deinit(allocator, &self.inactivity_penalty_deltas); - self.pre.deinit(); + fn accumulateFromOptionalFile( + expected_rewards: []u64, + expected_penalties: []u64, + allocator: std.mem.Allocator, + dir: std.fs.Dir, + comptime filename: []const u8, + ) !void { + if (try Self.loadOptionalDeltas(allocator, dir, filename)) |deltas_value| { + var deltas = deltas_value; + defer DeltasType.deinit(allocator, &deltas); + try Self.accumulateDeltas(expected_rewards, expected_penalties, &deltas); + } } fn loadDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !DeltasType.Type { var deltas = DeltasType.default_value; - loadSszValue(DeltasType, allocator, dir, filename, &deltas) catch |err| { + errdefer { if (comptime @hasDecl(DeltasType, "deinit")) { DeltasType.deinit(allocator, &deltas); } - return err; - }; + } + try loadSszValue(DeltasType, allocator, dir, filename, &deltas); return deltas; } fn loadOptionalDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !?DeltasType.Type { var deltas = DeltasType.default_value; + errdefer { + if (comptime @hasDecl(DeltasType, "deinit")) { + DeltasType.deinit(allocator, &deltas); + } + } loadSszValue(DeltasType, allocator, dir, filename, &deltas) catch |err| switch (err) { - error.FileNotFound => { - if (comptime @hasDecl(DeltasType, "deinit")) { - DeltasType.deinit(allocator, &deltas); - } - return null; - }, - else => { - if (comptime @hasDecl(DeltasType, "deinit")) { - DeltasType.deinit(allocator, &deltas); - } - return err; - }, + error.FileNotFound => return null, + else => return err, }; return deltas; } @@ -139,27 +155,11 @@ pub fn TestCase(comptime fork: ForkSeq) type { try getRewardsAndPenaltiesFn(allocator, cloned_state, epoch_cache, epoch_cache.rewards, epoch_cache.penalties); - const validator_count = self.pre.cached_state.state.validators().items.len; const rewards = epoch_cache.rewards; const penalties = epoch_cache.penalties; - const expected_rewards = try allocator.alloc(u64, validator_count); - defer allocator.free(expected_rewards); - const expected_penalties = try allocator.alloc(u64, validator_count); - defer allocator.free(expected_penalties); - @memset(expected_rewards, 0); - @memset(expected_penalties, 0); - - try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.source_deltas); - try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.target_deltas); - try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.head_deltas); - if (self.has_inclusion_delay_deltas) { - try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.inclusion_delay_deltas); - } - try Self.accumulateDeltas(expected_rewards, expected_penalties, &self.inactivity_penalty_deltas); - - try std.testing.expectEqualSlices(u64, expected_rewards, rewards); - try std.testing.expectEqualSlices(u64, expected_penalties, penalties); + try std.testing.expectEqualSlices(u64, self.expected_rewards, rewards); + try std.testing.expectEqualSlices(u64, self.expected_penalties, penalties); } fn accumulateDeltas(expected_rewards: []u64, expected_penalties: []u64, deltas: *const DeltasType.Type) !void { From b9c01c1876b98ed5bcd62607a5a86b3b7945d820 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 12:32:06 +0800 Subject: [PATCH 67/72] chore: format Signed-off-by: grapebaba --- test/spec/runner/rewards.zig | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/spec/runner/rewards.zig b/test/spec/runner/rewards.zig index 070906ee0..5e5fe7e73 100644 --- a/test/spec/runner/rewards.zig +++ b/test/spec/runner/rewards.zig @@ -114,7 +114,11 @@ pub fn TestCase(comptime fork: ForkSeq) type { } } - fn loadDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !DeltasType.Type { + fn loadDeltas( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + comptime filename: []const u8, + ) !DeltasType.Type { var deltas = DeltasType.default_value; errdefer { if (comptime @hasDecl(DeltasType, "deinit")) { @@ -125,7 +129,11 @@ pub fn TestCase(comptime fork: ForkSeq) type { return deltas; } - fn loadOptionalDeltas(allocator: std.mem.Allocator, dir: std.fs.Dir, comptime filename: []const u8) !?DeltasType.Type { + fn loadOptionalDeltas( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + comptime filename: []const u8, + ) !?DeltasType.Type { var deltas = DeltasType.default_value; errdefer { if (comptime @hasDecl(DeltasType, "deinit")) { @@ -162,7 +170,11 @@ pub fn TestCase(comptime fork: ForkSeq) type { try std.testing.expectEqualSlices(u64, self.expected_penalties, penalties); } - fn accumulateDeltas(expected_rewards: []u64, expected_penalties: []u64, deltas: *const DeltasType.Type) !void { + fn accumulateDeltas( + expected_rewards: []u64, + expected_penalties: []u64, + deltas: *const DeltasType.Type, + ) !void { const values = deltas.*; const rewards = values[0].items; const penalties = values[1].items; From 5307c5641600195244b86108d3131bf35c5fd67a Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 17:04:50 +0800 Subject: [PATCH 68/72] feat: add fork spec test Signed-off-by: grapebaba --- src/state_transition/cache/epoch_cache.zig | 3 +- src/state_transition/root.zig | 6 + .../slot/upgrade_state_to_altair.zig | 3 +- src/state_transition/utils/epoch.zig | 7 +- test/spec/root.zig | 1 + test/spec/runner/fork.zig | 132 ++++++++++++++++++ test/spec/runner/merkle_proof.zig | 39 +++--- test/spec/runner/rewards.zig | 17 ++- test/spec/runner_kind.zig | 1 + test/spec/write_spec_tests.zig | 2 + test/spec/writer/fork.zig | 57 ++++++++ 11 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 test/spec/runner/fork.zig create mode 100644 test/spec/writer/fork.zig diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index b75c6e6d6..a3c6f43dc 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -23,6 +23,7 @@ const BeaconStateAllForks = @import("../types/beacon_state.zig").BeaconStateAllF const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const EpochTransitionCache = @import("../cache/epoch_transition_cache.zig").EpochTransitionCache; const computeEpochAtSlot = @import("../utils/epoch.zig").computeEpochAtSlot; +const computePreviousEpoch = @import("../utils/epoch.zig").computePreviousEpoch; const computeActivationExitEpoch = @import("../utils/epoch.zig").computeActivationExitEpoch; const getEffectiveBalanceIncrementsWithLen = @import("./effective_balance_increments.zig").getEffectiveBalanceIncrementsWithLen; const getTotalSlashingsByIncrement = @import("../epoch/process_slashings.zig").getTotalSlashingsByIncrement; @@ -624,7 +625,7 @@ pub const EpochCache = struct { } pub fn getShufflingAtEpochOrNull(self: *const EpochCache, epoch: Epoch) ?*const EpochShuffling { - const previous_epoch = if (self.epoch == GENESIS_EPOCH) GENESIS_EPOCH else self.epoch - 1; + const previous_epoch = computePreviousEpoch(self.epoch); const shuffling = if (epoch == previous_epoch) self.getPreviousShuffling() else if (epoch == self.epoch) self.getCurrentShuffling() else if (epoch == self.epoch + 1) diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index 127cf9b42..f3d96bc10 100644 --- a/src/state_transition/root.zig +++ b/src/state_transition/root.zig @@ -39,6 +39,12 @@ pub const processParticipationFlagUpdates = @import("./epoch/process_participati pub const processSyncCommitteeUpdates = @import("./epoch/process_sync_committee_updates.zig").processSyncCommitteeUpdates; pub const getNextSyncCommitteeIndices = @import("./utils/sync_committee.zig").getNextSyncCommitteeIndices; +pub const upgradeStateToAltair = @import("./slot/upgrade_state_to_altair.zig").upgradeStateToAltair; +pub const upgradeStateToBellatrix = @import("./slot/upgrade_state_to_bellatrix.zig").upgradeStateToBellatrix; +pub const upgradeStateToCapella = @import("./slot/upgrade_state_to_capella.zig").upgradeStateToCapella; +pub const upgradeStateToDeneb = @import("./slot/upgrade_state_to_deneb.zig").upgradeStateToDeneb; +pub const upgradeStateToElectra = @import("./slot/upgrade_state_to_electra.zig").upgradeStateToElectra; + // Block pub const processBlockHeader = @import("./block/process_block_header.zig").processBlockHeader; pub const processWithdrawals = @import("./block/process_withdrawals.zig").processWithdrawals; diff --git a/src/state_transition/slot/upgrade_state_to_altair.zig b/src/state_transition/slot/upgrade_state_to_altair.zig index b3c247040..923c0a16f 100644 --- a/src/state_transition/slot/upgrade_state_to_altair.zig +++ b/src/state_transition/slot/upgrade_state_to_altair.zig @@ -5,6 +5,7 @@ const BeaconStateAllForks = @import("../types/beacon_state.zig").BeaconStateAllF const getNextSyncCommittee = @import("../utils/sync_committee.zig").getNextSyncCommittee; const SyncCommitteeInfo = @import("../utils/sync_committee.zig").SyncCommitteeInfo; const sumTargetUnslashedBalanceIncrements = @import("../utils/target_unslashed_balance.zig").sumTargetUnslashedBalanceIncrements; +const computePreviousEpoch = @import("../utils/epoch.zig").computePreviousEpoch; const types = @import("consensus_types"); const ValidatorIndex = types.primitive.ValidatorIndex.Type; const RootCache = @import("../utils/root_cache.zig").RootCache; @@ -52,7 +53,7 @@ pub fn upgradeStateToAltair(allocator: Allocator, cached_state: *CachedBeaconSta try cached_state.epoch_cache_ref.get().setSyncCommitteesIndexed(sync_committee_info.indices.items); try translateParticipation(allocator, cached_state, phase0_state.previous_epoch_attestations); - const previous_epoch = epoch_cache.epoch - 1; + const previous_epoch = computePreviousEpoch(epoch_cache.epoch); epoch_cache.previous_target_unslashed_balance_increments = sumTargetUnslashedBalanceIncrements(state.previousEpochParticipations().items, previous_epoch, state.validators().items); } diff --git a/src/state_transition/utils/epoch.zig b/src/state_transition/utils/epoch.zig index 79f31a459..e0f01824e 100644 --- a/src/state_transition/utils/epoch.zig +++ b/src/state_transition/utils/epoch.zig @@ -95,9 +95,12 @@ pub fn getCurrentEpoch(state: BeaconStateAllForks) Epoch { return computeEpochAtSlot(state.slot()); } +pub fn computePreviousEpoch(epoch: Epoch) Epoch { + return if (epoch == GENESIS_EPOCH) GENESIS_EPOCH else epoch - 1; +} + pub fn getPreviousEpoch(state: BeaconStateAllForks) Epoch { - const current_epoch = getCurrentEpoch(state); - return if (current_epoch == GENESIS_EPOCH) GENESIS_EPOCH else current_epoch - 1; + return computePreviousEpoch(getCurrentEpoch(state)); } pub fn computeSyncPeriodAtSlot(slot: Slot) SyncPeriod { diff --git a/test/spec/root.zig b/test/spec/root.zig index 63b9c117b..b67c420d4 100644 --- a/test/spec/root.zig +++ b/test/spec/root.zig @@ -9,5 +9,6 @@ comptime { testing.refAllDecls(@import("./test_case/rewards_tests.zig")); testing.refAllDecls(@import("./test_case/sanity_tests.zig")); testing.refAllDecls(@import("./test_case/epoch_processing_tests.zig")); + testing.refAllDecls(@import("./test_case/fork_tests.zig")); testing.refAllDecls(@import("./test_case/transition_tests.zig")); } diff --git a/test/spec/runner/fork.zig b/test/spec/runner/fork.zig new file mode 100644 index 000000000..1b0e2fa51 --- /dev/null +++ b/test/spec/runner/fork.zig @@ -0,0 +1,132 @@ +const std = @import("std"); +const ForkSeq = @import("config").ForkSeq; +const forkSeqByForkName = @import("config").forkSeqByForkName; +const state_transition = @import("state_transition"); +const upgradeStateToAltair = state_transition.upgradeStateToAltair; +const upgradeStateToBellatrix = state_transition.upgradeStateToBellatrix; +const upgradeStateToCapella = state_transition.upgradeStateToCapella; +const upgradeStateToDeneb = state_transition.upgradeStateToDeneb; +const upgradeStateToElectra = state_transition.upgradeStateToElectra; +const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; +const BeaconStateAllForks = state_transition.BeaconStateAllForks; +const test_case = @import("../test_case.zig"); +const TestCaseUtils = test_case.TestCaseUtils; +const expectEqualBeaconStates = test_case.expectEqualBeaconStates; + +pub const Handler = enum { + fork, + + pub fn suiteName(self: Handler) []const u8 { + return @tagName(self) ++ "/pyspec_tests"; + } +}; + +const Allocator = std.mem.Allocator; + +pub fn TestCase(comptime target_fork: ForkSeq) type { + comptime { + switch (target_fork) { + .altair, .bellatrix, .capella, .deneb, .electra => {}, + else => @compileError("fork tests are not defined for " ++ @tagName(target_fork)), + } + } + + const pre_fork = comptime previousFork(target_fork); + const pre_tc_utils = TestCaseUtils(pre_fork); + const post_tc_utils = TestCaseUtils(target_fork); + + return struct { + pre: TestCachedBeaconStateAllForks, + post: ?BeaconStateAllForks, + + const Self = @This(); + + pub fn execute(allocator: Allocator, dir: std.fs.Dir) !void { + var tc = try Self.init(allocator, dir); + defer { + tc.deinit(); + state_transition.deinitStateTransition(); + } + + try tc.runTest(); + } + + fn init(allocator: Allocator, dir: std.fs.Dir) !Self { + const meta_fork = try loadTargetFork(allocator, dir); + if (meta_fork != target_fork) return error.InvalidMetaFile; + + var pre_state = try pre_tc_utils.loadPreState(allocator, dir); + errdefer pre_state.deinit(); + + const post_state = try post_tc_utils.loadPostState(allocator, dir); + + return .{ + .pre = pre_state, + .post = post_state, + }; + } + + fn deinit(self: *Self) void { + self.pre.deinit(); + if (self.post) |*post_state| { + post_state.deinit(self.pre.allocator); + } + } + + fn runTest(self: *Self) !void { + if (self.post) |expected| { + try self.upgrade(); + try expectEqualBeaconStates(expected, self.pre.cached_state.state.*); + } else { + self.upgrade() catch |err| { + if (err == error.SkipZigTest) { + return err; + } + return; + }; + return error.ExpectedError; + } + } + + fn upgrade(self: *Self) !void { + const cached_state = self.pre.cached_state; + switch (target_fork) { + .altair => try upgradeStateToAltair(self.pre.allocator, cached_state), + .bellatrix => try upgradeStateToBellatrix(self.pre.allocator, cached_state), + .capella => try upgradeStateToCapella(self.pre.allocator, cached_state), + .deneb => try upgradeStateToDeneb(self.pre.allocator, cached_state), + .electra => try upgradeStateToElectra(self.pre.allocator, cached_state), + else => unreachable, + } + } + }; +} + +fn loadTargetFork(allocator: Allocator, dir: std.fs.Dir) !ForkSeq { + var meta_file = try dir.openFile("meta.yaml", .{}); + defer meta_file.close(); + const contents = try meta_file.readToEndAlloc(allocator, 256); + defer allocator.free(contents); + + const key = "fork: "; + if (std.mem.indexOf(u8, contents, key)) |start| { + const after_key = contents[start + key.len ..]; + const end = std.mem.indexOf(u8, after_key, "}") orelse return error.InvalidMetaFile; + const fork_slice = after_key[0..end]; + if (fork_slice.len == 0) return error.InvalidMetaFile; + return forkSeqByForkName(fork_slice); + } + + return error.InvalidMetaFile; +} + +fn previousFork(target: ForkSeq) ForkSeq { + return switch (target) { + .altair => .phase0, + .bellatrix => .altair, + .capella => .bellatrix, + .deneb => .capella, + .electra => .deneb, + else => @compileError("Unsupported fork transition for " ++ @tagName(target)), + }; +} diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index aa0362e63..d8ebd3a00 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -36,15 +36,17 @@ pub fn TestCase(comptime fork: ForkSeq) type { return struct { body: BeaconBlockBody.Type, - proof: MerkleProof, + expect_proof: MerkleProof, + actual_proof: MerkleProof, + allocator: std.mem.Allocator, const Self = @This(); pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) !void { var tc = try Self.init(allocator, dir); - defer tc.deinit(allocator); + defer tc.deinit(); - try tc.runTest(allocator); + try tc.runTest(); } fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { @@ -61,35 +63,38 @@ pub fn TestCase(comptime fork: ForkSeq) type { return .{ .body = body, - .proof = proof_data, + .expect_proof = proof_data, + .actual_proof = undefined, + .allocator = allocator, }; } - fn deinit(self: *Self, allocator: std.mem.Allocator) void { - self.proof.deinit(allocator); + fn deinit(self: *Self) void { + self.expect_proof.deinit(self.allocator); if (comptime @hasDecl(BeaconBlockBody, "deinit")) { - BeaconBlockBody.deinit(allocator, &self.body); + BeaconBlockBody.deinit(self.allocator, &self.body); } } - fn runTest(self: *Self, allocator: std.mem.Allocator) !void { + fn runTest(self: *Self) !void { + try self.process(); + try expectEqualProof(&self.expect_proof, &self.actual_proof); + } + + fn process(self: *Self) !void { const actual_leaf_index: u64 = @intCast(preset_mod.KZG_COMMITMENT_GINDEX0); var actual_leaf: [32]u8 = undefined; try KzgCommitment.hashTreeRoot(&self.body.blob_kzg_commitments.items[0], &actual_leaf); - var pool = try Node.Pool.init(allocator, 2048); + var pool = try Node.Pool.init(self.allocator, 2048); defer pool.deinit(); - const root_node = try BeaconBlockBody.tree.fromValue(allocator, &pool, &self.body); + const root_node = try BeaconBlockBody.tree.fromValue(self.allocator, &pool, &self.body); const gindex = Gindex.fromUint(@as(Gindex.Uint, actual_leaf_index)); - var single_proof = try pmt.proof.createSingleProof(allocator, &pool, root_node, gindex); - defer single_proof.deinit(allocator); - - var actual_proof = try buildActualProof(allocator, actual_leaf_index, &actual_leaf, single_proof.witnesses); - defer actual_proof.deinit(allocator); - - try expectEqualProof(&self.proof, &actual_proof); + var single_proof = try pmt.proof.createSingleProof(self.allocator, &pool, root_node, gindex); + defer single_proof.deinit(self.allocator); + self.actual_proof = try buildActualProof(self.allocator, actual_leaf_index, &actual_leaf, single_proof.witnesses); } fn buildActualProof( diff --git a/test/spec/runner/rewards.zig b/test/spec/runner/rewards.zig index 5e5fe7e73..fe144380d 100644 --- a/test/spec/runner/rewards.zig +++ b/test/spec/runner/rewards.zig @@ -31,6 +31,8 @@ pub fn TestCase(comptime fork: ForkSeq) type { pre: TestCachedBeaconStateAllForks, expected_rewards: []u64, expected_penalties: []u64, + actual_rewards: []u64, + actual_penalties: []u64, const Self = @This(); @@ -56,6 +58,8 @@ pub fn TestCase(comptime fork: ForkSeq) type { .pre = pre_state, .expected_rewards = expected.rewards, .expected_penalties = expected.penalties, + .actual_rewards = undefined, + .actual_penalties = undefined, }; } @@ -148,6 +152,12 @@ pub fn TestCase(comptime fork: ForkSeq) type { } fn runTest(self: *Self) !void { + try self.process(); + try std.testing.expectEqualSlices(u64, self.expected_rewards, self.actual_rewards); + try std.testing.expectEqualSlices(u64, self.expected_penalties, self.actual_penalties); + } + + fn process(self: *Self) !void { const allocator = self.pre.allocator; const cloned_state = try self.pre.cached_state.clone(allocator); defer { @@ -163,11 +173,8 @@ pub fn TestCase(comptime fork: ForkSeq) type { try getRewardsAndPenaltiesFn(allocator, cloned_state, epoch_cache, epoch_cache.rewards, epoch_cache.penalties); - const rewards = epoch_cache.rewards; - const penalties = epoch_cache.penalties; - - try std.testing.expectEqualSlices(u64, self.expected_rewards, rewards); - try std.testing.expectEqualSlices(u64, self.expected_penalties, penalties); + self.actual_rewards = epoch_cache.rewards; + self.actual_penalties = epoch_cache.penalties; } fn accumulateDeltas( diff --git a/test/spec/runner_kind.zig b/test/spec/runner_kind.zig index 1573871e3..4082c70ef 100644 --- a/test/spec/runner_kind.zig +++ b/test/spec/runner_kind.zig @@ -2,6 +2,7 @@ const std = @import("std"); pub const RunnerKind = enum { epoch_processing, + fork, finality, merkle_proof, operations, diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig index 025007349..f4f7bffc0 100644 --- a/test/spec/write_spec_tests.zig +++ b/test/spec/write_spec_tests.zig @@ -18,6 +18,7 @@ const supported_test_runners = [_]RunnerKind{ .rewards, .sanity, .epoch_processing, + .fork, .transition, }; @@ -28,6 +29,7 @@ fn TestWriter(comptime kind: RunnerKind) type { .rewards => @import("./writer/rewards.zig"), .sanity => @import("./writer/sanity.zig"), .epoch_processing => @import("./writer/epoch_processing.zig"), + .fork => @import("./writer/fork.zig"), .transition => @import("./writer/transition.zig"), else => @compileError("Unsupported test runner"), }; diff --git a/test/spec/writer/fork.zig b/test/spec/writer/fork.zig new file mode 100644 index 000000000..3e42a202c --- /dev/null +++ b/test/spec/writer/fork.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const spec_test_options = @import("spec_test_options"); +const ForkSeq = @import("config").ForkSeq; +const ForkRunner = @import("../runner/fork.zig"); + +pub const handlers = std.enums.values(ForkRunner.Handler); +pub const header = + \\// This file is generated by write_spec_tests.zig. + \\// Do not commit changes by hand. + \\ + \\const std = @import("std"); + \\const ForkSeq = @import("config").ForkSeq; + \\const active_preset = @import("preset").active_preset; + \\const spec_test_options = @import("spec_test_options"); + \\const ForkRunner = @import("../runner/fork.zig"); + \\ + \\const allocator = std.testing.allocator; + \\ + \\ +; + +const test_template = + \\test "{s} fork {s}" {{ + \\ const test_dir_name = try std.fs.path.join(allocator, &[_][]const u8{{ + \\ spec_test_options.spec_test_out_dir, + \\ spec_test_options.spec_test_version, + \\ @tagName(active_preset) ++ "/tests/" ++ @tagName(active_preset) ++ "/{s}/fork/{s}/{s}", + \\ }}); + \\ defer allocator.free(test_dir_name); + \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; + \\ + \\ try ForkRunner.TestCase(.{s}).execute(allocator, test_dir); + \\}} + \\ + \\ +; + +pub fn writeHeader(writer: std.io.AnyWriter) !void { + try writer.print(header, .{}); +} + +pub fn writeTest( + writer: std.io.AnyWriter, + fork: ForkSeq, + comptime handler: ForkRunner.Handler, + test_case_name: []const u8, +) !void { + const suite = handler.suiteName(); + try writer.print(test_template, .{ + @tagName(fork), + test_case_name, + @tagName(fork), + suite, + test_case_name, + @tagName(fork), + }); +} From f52863aea111a790d541532ccdaee2d8532ddca3 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 17:10:16 +0800 Subject: [PATCH 69/72] chore: use computePreviousEpoch helper Signed-off-by: grapebaba --- src/state_transition/utils/finality.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/state_transition/utils/finality.zig b/src/state_transition/utils/finality.zig index 33fd35b52..90f5b414e 100644 --- a/src/state_transition/utils/finality.zig +++ b/src/state_transition/utils/finality.zig @@ -1,14 +1,11 @@ const std = @import("std"); -const GENESIS_EPOCH = @import("preset").GENESIS_EPOCH; const CachedBeaconStateAllForks = @import("../cache/state_cache.zig").CachedBeaconStateAllForks; const preset = @import("preset").preset; const MIN_EPOCHS_TO_INACTIVITY_PENALTY = preset.MIN_EPOCHS_TO_INACTIVITY_PENALTY; +const computePreviousEpoch = @import("./epoch.zig").computePreviousEpoch; pub fn getFinalityDelay(cached_state: *const CachedBeaconStateAllForks) u64 { - const previous_epoch = if (cached_state.getEpochCache().epoch > GENESIS_EPOCH) - cached_state.getEpochCache().epoch - 1 - else - GENESIS_EPOCH; + const previous_epoch = computePreviousEpoch(cached_state.getEpochCache().epoch); std.debug.assert(previous_epoch >= cached_state.state.finalizedCheckpoint().epoch); // previous_epoch = epoch - 1 From 5536ea963e72642f21ca3c644623db899937c3de Mon Sep 17 00:00:00 2001 From: grapebaba Date: Thu, 20 Nov 2025 21:32:39 +0800 Subject: [PATCH 70/72] chore: sync zbuild Signed-off-by: grapebaba --- zbuild.zon | 1 + 1 file changed, 1 insertion(+) diff --git a/zbuild.zon b/zbuild.zon index f4e715d30..cbf14a3b1 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -189,6 +189,7 @@ .ssz, .blst, .persistent_merkle_tree, + .hex, }, }, .filters = .{}, From 9ff736e19ca8223d83bd92c6102563dc322b3c48 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Fri, 21 Nov 2025 12:45:20 +0800 Subject: [PATCH 71/72] fix: fix memory leak and writer wrong arg Signed-off-by: grapebaba --- test/spec/runner/merkle_proof.zig | 1 + test/spec/writer/merkle_proof.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index d8ebd3a00..6faf6b5af 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -71,6 +71,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { fn deinit(self: *Self) void { self.expect_proof.deinit(self.allocator); + self.actual_proof.deinit(self.allocator); if (comptime @hasDecl(BeaconBlockBody, "deinit")) { BeaconBlockBody.deinit(self.allocator, &self.body); } diff --git a/test/spec/writer/merkle_proof.zig b/test/spec/writer/merkle_proof.zig index 2fa60d424..a5912965e 100644 --- a/test/spec/writer/merkle_proof.zig +++ b/test/spec/writer/merkle_proof.zig @@ -48,15 +48,15 @@ pub fn writeTest( ) !void { try writer.print(test_template, .{ @tagName(fork), + @tagName(handler), test_suite_name, test_case_name, - @tagName(fork), @tagName(handler), test_suite_name, test_case_name, - @tagName(fork), + @tagName(fork), }); } From 04829f3efadac5168dda3b14e721d5e052166373 Mon Sep 17 00:00:00 2001 From: grapebaba Date: Sun, 30 Nov 2025 16:52:00 +0800 Subject: [PATCH 72/72] fix: fix merkle_proof spec test path issue and gindex bug Signed-off-by: grapebaba --- src/persistent_merkle_tree/gindex.zig | 55 +++++++++++++++++++ src/ssz/root.zig | 2 + src/ssz/type/path.zig | 78 ++++++++++++++++++++++++--- src/ssz/type/root.zig | 3 ++ test/spec/runner/merkle_proof.zig | 26 +++++---- test/spec/writer/merkle_proof.zig | 2 +- 6 files changed, 144 insertions(+), 22 deletions(-) diff --git a/src/persistent_merkle_tree/gindex.zig b/src/persistent_merkle_tree/gindex.zig index 814cb1818..b3fd3ecb9 100644 --- a/src/persistent_merkle_tree/gindex.zig +++ b/src/persistent_merkle_tree/gindex.zig @@ -102,6 +102,24 @@ pub const Gindex = enum(GindexUint) { } }.lessThan); } + + /// Concatenate multiple Generalized Indices. + /// Given generalized indices i1 for A -> B, i2 for B -> C, ..., i_n for Y -> Z, + /// returns the generalized index for A -> Z. + /// + pub fn concat(gindices: []const Gindex) Gindex { + if (gindices.len == 0) { + return Gindex.fromUint(1); // Root gindex + } + + var result = gindices[0]; + for (gindices[1..]) |gindex| { + const path_len = gindex.pathLen(); + const gindex_path = @intFromEnum(gindex) & ((@as(GindexUint, 1) << @intCast(path_len)) - 1); + result = @enumFromInt((@intFromEnum(result) << @intCast(path_len)) | gindex_path); + } + return result; + } }; test { @@ -115,3 +133,40 @@ test { try std.testing.expectEqualSlices(u1, &[_]u1{ 0, 1, 0 }, b.toPathBits(&bits)); try std.testing.expectEqual(@as(Gindex.Path, @enumFromInt(2)), b.toPath()); } + +test "concat gindices" { + // [2, 3] -> 5 + const case1: []const Gindex = &.{ Gindex.fromUint(2), Gindex.fromUint(3) }; + try std.testing.expectEqual(@as(GindexUint, 5), @intFromEnum(Gindex.concat(case1))); + + // [31, 3] -> 63 + const case2: []const Gindex = &.{ Gindex.fromUint(31), Gindex.fromUint(3) }; + try std.testing.expectEqual(@as(GindexUint, 63), @intFromEnum(Gindex.concat(case2))); + + // [31, 6] -> 126 + const case3: []const Gindex = &.{ Gindex.fromUint(31), Gindex.fromUint(6) }; + try std.testing.expectEqual(@as(GindexUint, 126), @intFromEnum(Gindex.concat(case3))); + + const empty: []const Gindex = &.{}; + try std.testing.expectEqual(@as(GindexUint, 1), @intFromEnum(Gindex.concat(empty))); + + const single: []const Gindex = &.{Gindex.fromUint(42)}; + try std.testing.expectEqual(@as(GindexUint, 42), @intFromEnum(Gindex.concat(single))); + + // [1, 5] -> 5 + const with_root: []const Gindex = &.{ Gindex.fromUint(1), Gindex.fromUint(5) }; + try std.testing.expectEqual(@as(GindexUint, 5), @intFromEnum(Gindex.concat(with_root))); + + // [5, 1] -> 5 + const root_suffix: []const Gindex = &.{ Gindex.fromUint(5), Gindex.fromUint(1) }; + try std.testing.expectEqual(@as(GindexUint, 5), @intFromEnum(Gindex.concat(root_suffix))); + + // [2, 2, 2] -> 8 (going left 3 times from root) + const three_lefts: []const Gindex = &.{ Gindex.fromUint(2), Gindex.fromUint(2), Gindex.fromUint(2) }; + try std.testing.expectEqual(@as(GindexUint, 8), @intFromEnum(Gindex.concat(three_lefts))); + + // [3, 3, 3] -> 15 (going right 3 times from root) + // concat(3, 3) = 7, concat(7, 3) = 15 + const three_rights: []const Gindex = &.{ Gindex.fromUint(3), Gindex.fromUint(3), Gindex.fromUint(3) }; + try std.testing.expectEqual(@as(GindexUint, 15), @intFromEnum(Gindex.concat(three_rights))); +} diff --git a/src/ssz/root.zig b/src/ssz/root.zig index 4da2c7f36..874cf5f50 100644 --- a/src/ssz/root.zig +++ b/src/ssz/root.zig @@ -34,6 +34,8 @@ pub const VariableVectorType = types.VariableVectorType; pub const FixedContainerType = types.FixedContainerType; pub const VariableContainerType = types.VariableContainerType; +pub const getPathGindex = types.getPathGindex; + const hasher = @import("hasher.zig"); pub const Hasher = hasher.Hasher; pub const HasherData = hasher.HasherData; diff --git a/src/ssz/type/path.zig b/src/ssz/type/path.zig index b5618dc67..08eedaa22 100644 --- a/src/ssz/type/path.zig +++ b/src/ssz/type/path.zig @@ -1,5 +1,8 @@ const std = @import("std"); const isFixedType = @import("type_kind.zig").isFixedType; +const isBasicType = @import("type_kind.zig").isBasicType; +const Gindex = @import("persistent_merkle_tree").Gindex; +const BYTES_PER_CHUNK = @import("root.zig").BYTES_PER_CHUNK; const PathItemType = union(enum) { child: struct { @@ -131,11 +134,50 @@ pub fn PathType(comptime ST: type, comptime path_str: []const u8) type { } } -const types = @import("root.zig"); +/// Get the gindex for a field/element relative to the parent type. +fn getFieldGindex(comptime item: PathItem) Gindex { + const ST = item.ST; + switch (item.item_type) { + .child => |child| { + switch (ST.kind) { + .container => { + return Gindex.fromDepth(ST.chunk_depth, child.index); + }, + .vector, .list => { + // Lists have an extra depth level for the length mixin + const depth = ST.chunk_depth + @as(u8, if (ST.kind == .list) 1 else 0); + const chunk_index = if (comptime isBasicType(ST.Element)) + child.index / (BYTES_PER_CHUNK / ST.Element.fixed_size) + else + child.index; + return Gindex.fromDepth(depth, chunk_index); + }, + else => @compileError("Cannot get field gindex for basic types"), + } + }, + .length => { + // Length node is at gindex 3 (right child of root in list structure) + return Gindex.fromDepth(1, 1); + }, + } +} -test { - // std.testing.refAllDecls(@This()); +/// Get the gindex for a path relative to the root of the type. +pub fn getPathGindex(comptime ST: type, comptime path_str: []const u8) Gindex { + const items = getPathItems(ST, path_str); + var gindices: [items.len + 1]Gindex = undefined; + gindices[0] = Gindex.fromUint(1); // root + inline for (items, 0..) |item, i| { + gindices[i + 1] = getFieldGindex(item); + } + + return Gindex.concat(&gindices); +} + +const types = @import("root.zig"); + +test "PathType" { const Root = types.ByteVectorType(32); const Checkpoint = types.FixedContainerType(struct { slot: types.UintType(64), @@ -143,8 +185,30 @@ test { }); _ = PathType(Checkpoint, "slot"); - // _ = getPath(Checkpoint, "root"); - // _ = getPath(Checkpoint, "root.31"); - // _ = getPath(Root, "0"); - // _ = getOffset(Checkpoint, "root.20"); +} + +test "getPathGindex" { + const Root = types.ByteVectorType(32); + const Checkpoint = types.FixedContainerType(struct { + epoch: types.UintType(64), + root: Root, + }); + + try std.testing.expectEqual(@as(Gindex.Uint, 2), @intFromEnum(getPathGindex(Checkpoint, "epoch"))); + try std.testing.expectEqual(@as(Gindex.Uint, 3), @intFromEnum(getPathGindex(Checkpoint, "root"))); + + const BeaconState = types.FixedContainerType(struct { + slot: types.UintType(64), + finalized_checkpoint: Checkpoint, + }); + + try std.testing.expectEqual(@as(Gindex.Uint, 7), @intFromEnum(getPathGindex(BeaconState, "finalized_checkpoint.root"))); + + const Balances = types.FixedListType(types.UintType(64), 4); + const SimpleState = types.VariableContainerType(struct { + slot: types.UintType(64), + balances: Balances, + }); + + try std.testing.expectEqual(@as(Gindex.Uint, 6), @intFromEnum(getPathGindex(SimpleState, "balances.0"))); } diff --git a/src/ssz/type/root.zig b/src/ssz/type/root.zig index d52f4ab4a..131e93e35 100644 --- a/src/ssz/type/root.zig +++ b/src/ssz/type/root.zig @@ -28,6 +28,8 @@ pub const VariableVectorType = @import("vector.zig").VariableVectorType; pub const FixedContainerType = @import("container.zig").FixedContainerType; pub const VariableContainerType = @import("container.zig").VariableContainerType; +pub const getPathGindex = @import("path.zig").getPathGindex; + pub const BYTES_PER_CHUNK: usize = 32; test { @@ -40,6 +42,7 @@ test { _ = @import("byte_vector.zig"); _ = @import("list.zig"); _ = @import("container.zig"); + _ = @import("path.zig"); } const std = @import("std"); diff --git a/test/spec/runner/merkle_proof.zig b/test/spec/runner/merkle_proof.zig index 6faf6b5af..f0d8822ae 100644 --- a/test/spec/runner/merkle_proof.zig +++ b/test/spec/runner/merkle_proof.zig @@ -1,10 +1,10 @@ const std = @import("std"); const ct = @import("consensus_types"); const ForkSeq = @import("config").ForkSeq; -const preset_mod = @import("preset"); const test_case = @import("../test_case.zig"); const loadSszValue = test_case.loadSszSnappyValue; const hex = @import("hex"); +const ssz = @import("ssz"); const pmt = @import("persistent_merkle_tree"); const proof = pmt.proof; @@ -21,7 +21,7 @@ pub const Handler = enum { const MerkleProof = struct { leaf: [66]u8, - leaf_index: u64, + leaf_gindex: Gindex, branch: [][66]u8, pub fn deinit(self: *MerkleProof, allocator: std.mem.Allocator) void { @@ -83,7 +83,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { } fn process(self: *Self) !void { - const actual_leaf_index: u64 = @intCast(preset_mod.KZG_COMMITMENT_GINDEX0); + const gindex = ssz.getPathGindex(BeaconBlockBody, "blob_kzg_commitments.0"); var actual_leaf: [32]u8 = undefined; try KzgCommitment.hashTreeRoot(&self.body.blob_kzg_commitments.items[0], &actual_leaf); @@ -91,16 +91,15 @@ pub fn TestCase(comptime fork: ForkSeq) type { defer pool.deinit(); const root_node = try BeaconBlockBody.tree.fromValue(self.allocator, &pool, &self.body); - const gindex = Gindex.fromUint(@as(Gindex.Uint, actual_leaf_index)); var single_proof = try pmt.proof.createSingleProof(self.allocator, &pool, root_node, gindex); defer single_proof.deinit(self.allocator); - self.actual_proof = try buildActualProof(self.allocator, actual_leaf_index, &actual_leaf, single_proof.witnesses); + self.actual_proof = try buildActualProof(self.allocator, gindex, &actual_leaf, single_proof.witnesses); } fn buildActualProof( allocator: std.mem.Allocator, - leaf_index: u64, + leaf_gindex: Gindex, leaf_bytes: *const [32]u8, witnesses: [][32]u8, ) !MerkleProof { @@ -113,7 +112,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { return .{ .leaf = try hex.rootToHex(leaf_bytes), - .leaf_index = leaf_index, + .leaf_gindex = leaf_gindex, .branch = branch, }; } @@ -122,7 +121,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { expected: *const MerkleProof, actual: *const MerkleProof, ) !void { - try std.testing.expectEqual(expected.leaf_index, actual.leaf_index); + try std.testing.expectEqual(expected.leaf_gindex, actual.leaf_gindex); try std.testing.expectEqualSlices(u8, expected.leaf[0..66], actual.leaf[0..66]); try std.testing.expectEqual(expected.branch.len, actual.branch.len); for (expected.branch, 0..) |expected_witness, i| { @@ -144,7 +143,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { var branch: std.ArrayListUnmanaged([66]u8) = .empty; errdefer branch.deinit(allocator); var leaf: ?[66]u8 = null; - var leaf_index: ?u64 = null; + var leaf_gindex: ?Gindex = null; var iter = std.mem.tokenizeScalar(u8, contents, '\n'); const quote = "'\""; @@ -157,7 +156,7 @@ pub fn TestCase(comptime fork: ForkSeq) type { leaf = value_slice[0..66].*; } else if (std.mem.startsWith(u8, line, "leaf_index: ")) { const value_slice = std.mem.trim(u8, line["leaf_index: ".len..], quote); - leaf_index = try std.fmt.parseInt(u64, value_slice, 10); + leaf_gindex = Gindex.fromUint(try std.fmt.parseInt(Gindex.Uint, value_slice, 10)); } else if (std.mem.startsWith(u8, line, "- ")) { const value_slice = std.mem.trim(u8, line[2..], quote); std.debug.assert(value_slice.len == 66); @@ -166,19 +165,18 @@ pub fn TestCase(comptime fork: ForkSeq) type { } } - if (leaf == null or leaf_index == null) { + if (leaf == null or leaf_gindex == null) { return error.InvalidProof; } - const gindex = Gindex.fromUint(@as(Gindex.Uint, leaf_index.?)); - const expected_branch_len: usize = @intCast(gindex.pathLen()); + const expected_branch_len: usize = @intCast(leaf_gindex.?.pathLen()); if (branch.items.len != expected_branch_len) { return error.InvalidProof; } return .{ .leaf = leaf.?, - .leaf_index = leaf_index.?, + .leaf_gindex = leaf_gindex.?, .branch = try branch.toOwnedSlice(allocator), }; } diff --git a/test/spec/writer/merkle_proof.zig b/test/spec/writer/merkle_proof.zig index a5912965e..7c32344d4 100644 --- a/test/spec/writer/merkle_proof.zig +++ b/test/spec/writer/merkle_proof.zig @@ -52,10 +52,10 @@ pub fn writeTest( test_suite_name, test_case_name, + @tagName(fork), @tagName(handler), test_suite_name, test_case_name, - @tagName(fork), @tagName(fork), });