diff --git a/.gitignore b/.gitignore index 07437403..55ef5459 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ test/bun/benchmark_data/ bun/node_modules/ bun/benchmark_data/ test/spec/spec_tests/ +test/spec/test_case/ diff --git a/build.zig b/build.zig index 1cdea7e7..42eedfcc 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,8 @@ pub fn build(b: *std.Build) void { const dep_ssz = b.dependency("ssz", .{}); + const dep_snappy = b.dependency("snappy", .{}); + const options_build_options = b.addOptions(); const option_preset = b.option([]const u8, "preset", "") orelse "mainnet"; options_build_options.addOption([]const u8, "preset", option_preset); @@ -89,12 +91,35 @@ pub fn build(b: *std.Build) void { const tls_run_exe_download_spec_tests = b.step("run:download_spec_tests", "Run the download_spec_tests executable"); tls_run_exe_download_spec_tests.dependOn(&run_exe_download_spec_tests.step); + const module_write_spec_tests = b.createModule(.{ + .root_source_file = b.path("test/spec/write_spec_tests.zig"), + .target = target, + .optimize = optimize, + }); + b.modules.put(b.dupe("write_spec_tests"), module_write_spec_tests) catch @panic("OOM"); + + const exe_write_spec_tests = b.addExecutable(.{ + .name = "write_spec_tests", + .root_module = module_write_spec_tests, + }); + + const install_exe_write_spec_tests = b.addInstallArtifact(exe_write_spec_tests, .{}); + + const tls_install_exe_write_spec_tests = b.step("build-exe:write_spec_tests", "Install the write_spec_tests executable"); + tls_install_exe_write_spec_tests.dependOn(&install_exe_write_spec_tests.step); + b.getInstallStep().dependOn(&install_exe_write_spec_tests.step); + + const run_exe_write_spec_tests = b.addRunArtifact(exe_write_spec_tests); + if (b.args) |args| run_exe_write_spec_tests.addArgs(args); + const tls_run_exe_write_spec_tests = b.step("run:write_spec_tests", "Run the write_spec_tests executable"); + tls_run_exe_write_spec_tests.dependOn(&run_exe_write_spec_tests.step); + const tls_run_test = b.step("test", "Run all tests"); const test_hex = b.addTest(.{ .name = "hex", .root_module = module_hex, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "hex.filters", "hex test filters") orelse &[_][]const u8{}, }); const install_test_hex = b.addInstallArtifact(test_hex, .{}); const tls_install_test_hex = b.step("build-test:hex", "Install the hex test"); @@ -108,7 +133,7 @@ pub fn build(b: *std.Build) void { const test_constants = b.addTest(.{ .name = "constants", .root_module = module_constants, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "constants.filters", "constants test filters") orelse &[_][]const u8{}, }); const install_test_constants = b.addInstallArtifact(test_constants, .{}); const tls_install_test_constants = b.step("build-test:constants", "Install the constants test"); @@ -122,7 +147,7 @@ pub fn build(b: *std.Build) void { const test_config = b.addTest(.{ .name = "config", .root_module = module_config, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "config.filters", "config test filters") orelse &[_][]const u8{}, }); const install_test_config = b.addInstallArtifact(test_config, .{}); const tls_install_test_config = b.step("build-test:config", "Install the config test"); @@ -136,7 +161,7 @@ pub fn build(b: *std.Build) void { const test_consensus_types = b.addTest(.{ .name = "consensus_types", .root_module = module_consensus_types, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "consensus_types.filters", "consensus_types test filters") orelse &[_][]const u8{}, }); const install_test_consensus_types = b.addInstallArtifact(test_consensus_types, .{}); const tls_install_test_consensus_types = b.step("build-test:consensus_types", "Install the consensus_types test"); @@ -150,7 +175,7 @@ pub fn build(b: *std.Build) void { const test_preset = b.addTest(.{ .name = "preset", .root_module = module_preset, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "preset.filters", "preset test filters") orelse &[_][]const u8{}, }); const install_test_preset = b.addInstallArtifact(test_preset, .{}); const tls_install_test_preset = b.step("build-test:preset", "Install the preset test"); @@ -164,7 +189,7 @@ pub fn build(b: *std.Build) void { const test_state_transition = b.addTest(.{ .name = "state_transition", .root_module = module_state_transition, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "state_transition.filters", "state_transition test filters") orelse &[_][]const u8{}, }); const install_test_state_transition = b.addInstallArtifact(test_state_transition, .{}); const tls_install_test_state_transition = b.step("build-test:state_transition", "Install the state_transition test"); @@ -178,7 +203,7 @@ pub fn build(b: *std.Build) void { const test_download_spec_tests = b.addTest(.{ .name = "download_spec_tests", .root_module = module_download_spec_tests, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "download_spec_tests.filters", "download_spec_tests test filters") orelse &[_][]const u8{}, }); const install_test_download_spec_tests = b.addInstallArtifact(test_download_spec_tests, .{}); const tls_install_test_download_spec_tests = b.step("build-test:download_spec_tests", "Install the download_spec_tests test"); @@ -189,6 +214,20 @@ pub fn build(b: *std.Build) void { tls_run_test_download_spec_tests.dependOn(&run_test_download_spec_tests.step); tls_run_test.dependOn(&run_test_download_spec_tests.step); + const test_write_spec_tests = b.addTest(.{ + .name = "write_spec_tests", + .root_module = module_write_spec_tests, + .filters = b.option([][]const u8, "write_spec_tests.filters", "write_spec_tests test filters") orelse &[_][]const u8{}, + }); + const install_test_write_spec_tests = b.addInstallArtifact(test_write_spec_tests, .{}); + const tls_install_test_write_spec_tests = b.step("build-test:write_spec_tests", "Install the write_spec_tests test"); + tls_install_test_write_spec_tests.dependOn(&install_test_write_spec_tests.step); + + const run_test_write_spec_tests = b.addRunArtifact(test_write_spec_tests); + const tls_run_test_write_spec_tests = b.step("test:write_spec_tests", "Run the write_spec_tests test"); + tls_run_test_write_spec_tests.dependOn(&run_test_write_spec_tests.step); + tls_run_test.dependOn(&run_test_write_spec_tests.step); + const module_int = b.createModule(.{ .root_source_file = b.path("test/int/root.zig"), .target = target, @@ -199,7 +238,7 @@ pub fn build(b: *std.Build) void { const test_int = b.addTest(.{ .name = "int", .root_module = module_int, - .filters = &[_][]const u8{}, + .filters = b.option([][]const u8, "int.filters", "int test filters") orelse &[_][]const u8{}, }); const install_test_int = b.addInstallArtifact(test_int, .{}); const tls_install_test_int = b.step("build-test:int", "Install the int test"); @@ -210,6 +249,27 @@ pub fn build(b: *std.Build) void { tls_run_test_int.dependOn(&run_test_int.step); tls_run_test.dependOn(&run_test_int.step); + const module_spec_tests = b.createModule(.{ + .root_source_file = b.path("test/spec/root.zig"), + .target = target, + .optimize = optimize, + }); + b.modules.put(b.dupe("spec_tests"), module_spec_tests) catch @panic("OOM"); + + const test_spec_tests = b.addTest(.{ + .name = "spec_tests", + .root_module = module_spec_tests, + .filters = b.option([][]const u8, "spec_tests.filters", "spec_tests test filters") orelse &[_][]const u8{}, + }); + const install_test_spec_tests = b.addInstallArtifact(test_spec_tests, .{}); + const tls_install_test_spec_tests = b.step("build-test:spec_tests", "Install the spec_tests test"); + tls_install_test_spec_tests.dependOn(&install_test_spec_tests.step); + + const run_test_spec_tests = b.addRunArtifact(test_spec_tests); + const tls_run_test_spec_tests = b.step("test:spec_tests", "Run the spec_tests test"); + tls_run_test_spec_tests.dependOn(&run_test_spec_tests.step); + tls_run_test.dependOn(&run_test_spec_tests.step); + module_config.addImport("build_options", options_module_build_options); module_config.addImport("preset", module_preset); module_config.addImport("consensus_types", module_consensus_types); @@ -234,6 +294,12 @@ pub fn build(b: *std.Build) void { module_download_spec_tests.addImport("spec_test_options", options_module_spec_test_options); + module_write_spec_tests.addImport("spec_test_options", options_module_spec_test_options); + module_write_spec_tests.addImport("config", module_config); + module_write_spec_tests.addImport("preset", module_preset); + module_write_spec_tests.addImport("consensus_types", module_consensus_types); + module_write_spec_tests.addImport("state_transition", module_state_transition); + module_int.addImport("build_options", options_module_build_options); module_int.addImport("ssz", dep_ssz.module("ssz")); module_int.addImport("state_transition", module_state_transition); @@ -241,4 +307,13 @@ pub fn build(b: *std.Build) void { module_int.addImport("consensus_types", module_consensus_types); module_int.addImport("preset", module_preset); module_int.addImport("constants", module_constants); + + module_spec_tests.addImport("spec_test_options", options_module_spec_test_options); + module_spec_tests.addImport("consensus_types", module_consensus_types); + module_spec_tests.addImport("config", module_config); + module_spec_tests.addImport("preset", module_preset); + module_spec_tests.addImport("snappy", dep_snappy.module("snappy")); + 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")); } diff --git a/build.zig.zon b/build.zig.zon index 9cb938a0..ba827c95 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,9 +11,13 @@ .hash = "blst_z-0.0.0-td3FNDOMAACf6Apg5icx1KkzpVeIR-9oOSK0WSILFHx8", }, .ssz = .{ - .url = "git+https://github.com/chainsafe/ssz-z#76cde3c7ea23b33d2789e8e372003272abe0e335", + .url = "git+https://github.com/chainSafe/ssz-z#76cde3c7ea23b33d2789e8e372003272abe0e335", .hash = "ssz-0.1.0-yORmzN0NBQDTFE4aEPt3iwqZCa66VEPtpWTZ9_eS3cYO", }, + .snappy = .{ + .url = "git+https://github.com/chainsafe/snappy.zig#ede2ad602ac9ffa506e3724a2bf5fc14c806187f", + .hash = "snappy-0.1.0-n4AaqtMYAACgB1kHWQ2_CFI-gbtYEUCbXyYlQZ2ENyfd", + }, }, .paths = .{ "build.zig", "build.zig.zon", "src" }, } diff --git a/src/config/chain/networks/minimal.zig b/src/config/chain/networks/minimal.zig new file mode 100644 index 00000000..f3b285dc --- /dev/null +++ b/src/config/chain/networks/minimal.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const hex_utils = @import("hex"); +const Preset = @import("preset").Preset; +const ChainConfig = @import("../chain_config.zig").ChainConfig; +const BlobScheduleEntry = @import("../chain_config.zig").BlobScheduleEntry; +const b = hex_utils.hexToBytesComptime; + +pub const minimal_chain_config = ChainConfig{ + .PRESET_BASE = Preset.minimal, + .CONFIG_NAME = "minimal", + + .TERMINAL_TOTAL_DIFFICULTY = 115792089237316195423570985008687907853269984665640564039457584007913129638912, + .TERMINAL_BLOCK_HASH = b(32, "0x0000000000000000000000000000000000000000000000000000000000000000"), + .TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH = std.math.maxInt(u64), + + // Genesis + .MIN_GENESIS_ACTIVE_VALIDATOR_COUNT = 64, + .MIN_GENESIS_TIME = 1578009600, + .GENESIS_FORK_VERSION = b(4, "0x00000001"), + .GENESIS_DELAY = 300, + + // Forking + .ALTAIR_FORK_VERSION = b(4, "0x01000001"), + .ALTAIR_FORK_EPOCH = 74240, + .BELLATRIX_FORK_VERSION = b(4, "0x02000001"), + .BELLATRIX_FORK_EPOCH = std.math.maxInt(u64), + .CAPELLA_FORK_VERSION = b(4, "0x03000001"), + .CAPELLA_FORK_EPOCH = std.math.maxInt(u64), + .DENEB_FORK_VERSION = b(4, "0x04000001"), + .DENEB_FORK_EPOCH = std.math.maxInt(u64), + .ELECTRA_FORK_VERSION = b(4, "0x05000001"), + .ELECTRA_FORK_EPOCH = std.math.maxInt(u64), + .FULU_FORK_VERSION = b(4, "0x06000001"), + .FULU_FORK_EPOCH = std.math.maxInt(u64), + + // Time parameters + .SECONDS_PER_SLOT = 6, + .SECONDS_PER_ETH1_BLOCK = 14, + .MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 256, + .SHARD_COMMITTEE_PERIOD = 64, + .ETH1_FOLLOW_DISTANCE = 16, + + // Validator cycle + .INACTIVITY_SCORE_BIAS = 4, + .INACTIVITY_SCORE_RECOVERY_RATE = 16, + .EJECTION_BALANCE = 16000000000, + .MIN_PER_EPOCH_CHURN_LIMIT = 2, + .MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT = 4, + .CHURN_LIMIT_QUOTIENT = 32, + + // Fork choice + .PROPOSER_SCORE_BOOST = 40, + .REORG_HEAD_WEIGHT_THRESHOLD = 20, + .REORG_PARENT_WEIGHT_THRESHOLD = 160, + .REORG_MAX_EPOCHS_SINCE_FINALIZATION = 2, + + // Deposit contract + .DEPOSIT_CHAIN_ID = 5, + .DEPOSIT_NETWORK_ID = 5, + .DEPOSIT_CONTRACT_ADDRESS = b(20, "0x1234567890123456789012345678901234567890"), + + // Networking + .MIN_EPOCHS_FOR_BLOCK_REQUESTS = 272, + + // Deneb + .MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS = 4096, + .BLOB_SIDECAR_SUBNET_COUNT = 6, + .MAX_BLOBS_PER_BLOCK = 6, + .MAX_REQUEST_BLOB_SIDECARS = 768, + + // Electra + .MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT = 128000000000, + .MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA = 64000000000, + .BLOB_SIDECAR_SUBNET_COUNT_ELECTRA = 9, + .MAX_BLOBS_PER_BLOCK_ELECTRA = 9, + .MAX_REQUEST_BLOB_SIDECARS_ELECTRA = 1152, + + // Fulu + .MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS = 4096, + .SAMPLES_PER_SLOT = 8, + .CUSTODY_REQUIREMENT = 4, + .NODE_CUSTODY_REQUIREMENT = 1, + .VALIDATOR_CUSTODY_REQUIREMENT = 8, + .BALANCE_PER_ADDITIONAL_CUSTODY_GROUP = 32000000000, + + // Blob Scheduling + .BLOB_SCHEDULE = &[_]BlobScheduleEntry{}, +}; diff --git a/src/config/fork.zig b/src/config/fork.zig index 0fb9f4d9..a5d30a56 100644 --- a/src/config/fork.zig +++ b/src/config/fork.zig @@ -18,6 +18,22 @@ pub const ForkSeq = enum(u8) { return @tagName(self); } + pub inline fn lt(self: ForkSeq, other: ForkSeq) bool { + return @intFromEnum(self) < @intFromEnum(other); + } + + pub inline fn lte(self: ForkSeq, other: ForkSeq) bool { + return @intFromEnum(self) <= @intFromEnum(other); + } + + pub inline fn gt(self: ForkSeq, other: ForkSeq) bool { + return @intFromEnum(self) > @intFromEnum(other); + } + + pub inline fn gte(self: ForkSeq, other: ForkSeq) bool { + return @intFromEnum(self) >= @intFromEnum(other); + } + pub fn isPostAltair(self: ForkSeq) bool { return switch (self) { inline .phase0 => false, diff --git a/src/config/root.zig b/src/config/root.zig index 5508bc6a..f0e7cdd3 100644 --- a/src/config/root.zig +++ b/src/config/root.zig @@ -7,6 +7,7 @@ pub const ForkInfo = @import("./fork.zig").ForkInfo; pub const forkSeqByForkName = @import("./fork.zig").forkSeqByForkName; 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; pub const gnosis_chain_config = @import("./chain/networks/gnosis.zig").gnosis_chain_config; pub const chiado_chain_config = @import("./chain/networks/chiado.zig").chiado_chain_config; pub const sepolia_chain_config = @import("./chain/networks/sepolia.zig").sepolia_chain_config; diff --git a/src/preset/preset.zig b/src/preset/preset.zig index 19b5451d..6b2ee54d 100644 --- a/src/preset/preset.zig +++ b/src/preset/preset.zig @@ -160,4 +160,5 @@ const PresetMinimal = struct { const preset_str = @import("build_options").preset; -pub const active_preset = if (std.mem.eql(u8, preset_str, "minimal")) PresetMinimal else PresetMainnet; +pub const preset = if (std.mem.eql(u8, preset_str, "minimal")) PresetMinimal else PresetMainnet; +pub const active_preset = if (std.mem.eql(u8, preset_str, "minimal")) Preset.minimal else Preset.mainnet; diff --git a/src/preset/root.zig b/src/preset/root.zig index 208a5565..44b9ece5 100644 --- a/src/preset/root.zig +++ b/src/preset/root.zig @@ -3,7 +3,8 @@ const testing = std.testing; const preset_str = @import("build_options").preset; const c = @import("constants"); -pub const preset = @import("./preset.zig").active_preset; +pub const preset = @import("./preset.zig").preset; +pub const active_preset = @import("./preset.zig").active_preset; pub const Preset = @import("./preset.zig").Preset; // not in use for now, copied from lodestar ts params diff --git a/src/state_transition/block/process_attestation_phase0.zig b/src/state_transition/block/process_attestation_phase0.zig index 54034df4..1f58f47d 100644 --- a/src/state_transition/block/process_attestation_phase0.zig +++ b/src/state_transition/block/process_attestation_phase0.zig @@ -73,8 +73,9 @@ pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconS if (data.index != 0) { return error.InvalidAttestationNonZeroDataIndex; } - var committee_indices: []usize = undefined; - _ = try attestation.committee_bits.getTrueBitIndexes(committee_indices[0..]); + var committee_indices_buffer: [preset.MAX_COMMITTEES_PER_SLOT]usize = undefined; + const committee_indices_len = try attestation.committee_bits.getTrueBitIndexes(committee_indices_buffer[0..]); + const committee_indices = committee_indices_buffer[0..committee_indices_len]; if (committee_indices.len == 0) { return error.InvalidAttestationCommitteeBitsEmpty; } @@ -85,12 +86,17 @@ pub fn validateAttestation(comptime AT: type, cached_state: *const CachedBeaconS return error.InvalidAttestationInvalidLstCommitteeIndex; } - var aggregation_bits_array: []bool = undefined; - try attestation.aggregation_bits.toBoolSlice(&aggregation_bits_array); + var aggregation_bits_buffer: [preset.MAX_VALIDATORS_PER_COMMITTEE * preset.MAX_COMMITTEES_PER_SLOT]bool = undefined; + var aggregation_bits_slice = aggregation_bits_buffer[0..attestation.aggregation_bits.bit_len]; + try attestation.aggregation_bits.toBoolSlice(&aggregation_bits_slice); + const aggregation_bits_array = aggregation_bits_slice; // 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); + if (committee_offset + committee_validators.len > aggregation_bits_array.len) { + return error.InvalidAttestationCommitteeAggregationBitsLengthTooShort; + } const committee_aggregation_bits = aggregation_bits_array[committee_offset..(committee_offset + committee_validators.len)]; // Assert aggregation bits in this committee have at least one true bit diff --git a/src/state_transition/block/process_proposer_slashing.zig b/src/state_transition/block/process_proposer_slashing.zig index b5a63568..77069876 100644 --- a/src/state_transition/block/process_proposer_slashing.zig +++ b/src/state_transition/block/process_proposer_slashing.zig @@ -37,6 +37,10 @@ pub fn assertValidProposerSlashing( return error.InvalidProposerSlashingProposerIndexMismatch; } + if (header_1.proposer_index >= state.validators().items.len) { + return error.InvalidProposerSlashingProposerIndexOutOfRange; + } + // verify headers are different if (ssz.phase0.BeaconBlockHeader.equals(&header_1, &header_2)) { return error.InvalidProposerSlashingHeadersEqual; diff --git a/src/state_transition/block/process_voluntary_exit.zig b/src/state_transition/block/process_voluntary_exit.zig index d025430d..c136d3a6 100644 --- a/src/state_transition/block/process_voluntary_exit.zig +++ b/src/state_transition/block/process_voluntary_exit.zig @@ -23,6 +23,11 @@ pub fn isValidVoluntaryExit(cached_state: *CachedBeaconStateAllForks, signed_vol const epoch_cache = cached_state.getEpochCache(); const config = cached_state.config.chain; const voluntary_exit = signed_voluntary_exit.message; + + if (voluntary_exit.validator_index >= state.validators().items.len) { + return false; + } + const validator = state.validators().items[voluntary_exit.validator_index]; const current_epoch = epoch_cache.epoch; diff --git a/src/state_transition/block/process_withdrawals.zig b/src/state_transition/block/process_withdrawals.zig index 9e036152..0de4010b 100644 --- a/src/state_transition/block/process_withdrawals.zig +++ b/src/state_transition/block/process_withdrawals.zig @@ -9,6 +9,7 @@ const Withdrawal = ssz.capella.Withdrawal.Type; const Withdrawals = ssz.capella.Withdrawals.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; const ExecutionAddress = ssz.primitive.ExecutionAddress.Type; +const PendingPartialWithdrawal = ssz.electra.PendingPartialWithdrawal.Type; const ExecutionPayload = @import("../types/execution_payload.zig").ExecutionPayload; const hasExecutionWithdrawalCredential = @import("../utils/electra.zig").hasExecutionWithdrawalCredential; const hasEth1WithdrawalCredential = @import("../utils/capella.zig").hasEth1WithdrawalCredential; @@ -38,7 +39,10 @@ pub fn processWithdrawals( if (state.isPostElectra()) { const pending_partial_withdrawals = state.pendingPartialWithdrawals(); - @memcpy(pending_partial_withdrawals.items, state.pendingPartialWithdrawals().items[processed_partial_withdrawals_count..]); + const keep_len = pending_partial_withdrawals.items.len - processed_partial_withdrawals_count; + + std.mem.copyForwards(PendingPartialWithdrawal, pending_partial_withdrawals.items[0..keep_len], pending_partial_withdrawals.items[processed_partial_withdrawals_count..]); + pending_partial_withdrawals.shrinkRetainingCapacity(keep_len); } const next_withdrawal_index = state.nextWithdrawalIndex(); @@ -134,7 +138,7 @@ pub fn getExpectedWithdrawals( const withdraw_balance: u64 = if (withdraw_balance_gop.found_existing) withdraw_balance_gop.value_ptr.* else 0; const balance = if (state.isPostElectra()) // Deduct partially withdrawn balance already queued above - balances.items[validator_index] - withdraw_balance + if (balances.items[validator_index] > withdraw_balance) balances.items[validator_index] - withdraw_balance else 0 else balances.items[validator_index]; diff --git a/src/state_transition/cache/epoch_cache.zig b/src/state_transition/cache/epoch_cache.zig index e2708e2f..771febae 100644 --- a/src/state_transition/cache/epoch_cache.zig +++ b/src/state_transition/cache/epoch_cache.zig @@ -546,8 +546,9 @@ pub const EpochCache = struct { // In the spec it means a list of committee indices according to committeeBits // This `committeeIndices` refers to the latter // TODO Electra: resolve the naming conflicts - var committee_indices: []usize = undefined; - _ = try committee_bits.getTrueBitIndexes(committee_indices[0..]); + var committee_indices_buffer: [preset.MAX_COMMITTEES_PER_SLOT]usize = undefined; + const committee_indices_len = try committee_bits.getTrueBitIndexes(committee_indices_buffer[0..]); + const committee_indices = committee_indices_buffer[0..committee_indices_len]; var total_len: usize = 0; for (committee_indices) |committee_index| { diff --git a/src/state_transition/root.zig b/src/state_transition/root.zig index a2432529..aae5a5c0 100644 --- a/src/state_transition/root.zig +++ b/src/state_transition/root.zig @@ -3,6 +3,7 @@ const testing = std.testing; pub const computeSigningRoot = @import("./utils/signing_root.zig").computeSigningRoot; pub const BeaconBlock = @import("./types/beacon_block.zig").BeaconBlock; +pub const BeaconBlockBody = @import("./types/beacon_block.zig").BeaconBlockBody; pub const BeaconStateAllForks = @import("./types/beacon_state.zig").BeaconStateAllForks; pub const CachedBeaconStateAllForks = @import("./cache/state_cache.zig").CachedBeaconStateAllForks; @@ -37,13 +38,22 @@ pub const processSyncCommitteeUpdates = @import("./epoch/process_sync_committee_ // Block pub const processBlockHeader = @import("./block/process_block_header.zig").processBlockHeader; pub const processWithdrawals = @import("./block/process_withdrawals.zig").processWithdrawals; -pub const getExpectedWithdrawalsResult = @import("./block/process_withdrawals.zig").getExpectedWithdrawals; +pub const getExpectedWithdrawals = @import("./block/process_withdrawals.zig").getExpectedWithdrawals; pub const processExecutionPayload = @import("./block/process_execution_payload.zig").processExecutionPayload; pub const processRandao = @import("./block/process_randao.zig").processRandao; pub const processEth1Data = @import("./block/process_eth1_data.zig").processEth1Data; pub const processOperations = @import("./block/process_operations.zig").processOperations; pub const processSyncAggregate = @import("./block/process_sync_committee.zig").processSyncAggregate; pub const processBlobKzgCommitments = @import("./block/process_blob_kzg_commitments.zig").processBlobKzgCommitments; +pub const processAttestations = @import("./block/process_attestations.zig").processAttestations; +pub const processAttesterSlashing = @import("./block/process_attester_slashing.zig").processAttesterSlashing; +pub const processDeposit = @import("./block/process_deposit.zig").processDeposit; +pub const processProposerSlashing = @import("./block/process_proposer_slashing.zig").processProposerSlashing; +pub const processVoluntaryExit = @import("./block/process_voluntary_exit.zig").processVoluntaryExit; +pub const processBlsToExecutionChange = @import("./block/process_bls_to_execution_change.zig").processBlsToExecutionChange; +pub const processDepositRequest = @import("./block/process_deposit_request.zig").processDepositRequest; +pub const processWithdrawalRequest = @import("./block/process_withdrawal_request.zig").processWithdrawalRequest; +pub const processConsolidationRequest = @import("./block/process_consolidation_request.zig").processConsolidationRequest; pub const WithdrawalsResult = @import("./block/process_withdrawals.zig").WithdrawalsResult; @@ -55,6 +65,7 @@ pub const state_transition = @import("./state_transition.zig"); const EpochShuffling = @import("./utils/epoch_shuffling.zig"); pub const SignedBlock = @import("./types/signed_block.zig").SignedBlock; pub const SignedBeaconBlock = @import("./types/beacon_block.zig").SignedBeaconBlock; +pub const Attestations = @import("./types/attestation.zig").Attestations; test { testing.refAllDecls(@This()); diff --git a/src/state_transition/state_transition.zig b/src/state_transition/state_transition.zig index bf3a2d18..ed383024 100644 --- a/src/state_transition/state_transition.zig +++ b/src/state_transition/state_transition.zig @@ -49,7 +49,7 @@ pub const BlockExternalData = struct { }, }; -fn processSlotsWithTransientCache( +pub fn processSlotsWithTransientCache( allocator: std.mem.Allocator, post_state: *CachedBeaconStateAllForks, slot: Slot, diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index d997c25c..1a69b44d 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -1,11 +1,13 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const mainnet_chain_config = @import("config").mainnet_chain_config; +const minimal_chain_config = @import("config").minimal_chain_config; const ssz = @import("consensus_types"); const ElectraBeaconState = ssz.electra.BeaconState.Type; const BLSPubkey = ssz.primitive.BLSPubkey.Type; const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; const preset = @import("preset").preset; +const active_preset = @import("preset").active_preset; const BeaconConfig = @import("config").BeaconConfig; const ChainConfig = @import("config").ChainConfig; const state_transition = @import("../root.zig"); @@ -15,6 +17,7 @@ const PubkeyIndexMap = state_transition.PubkeyIndexMap(ValidatorIndex); const Index2PubkeyCache = state_transition.Index2PubkeyCache; const syncPubkeys = state_transition.syncPubkeys; const interopPubkeysCached = @import("./interop_pubkeys.zig").interopPubkeysCached; +const active_chain_config = if (active_preset == .mainnet) mainnet_chain_config else minimal_chain_config; /// generate, allocate BeaconStateAllForks /// consumer has responsibility to deinit it @@ -64,21 +67,31 @@ pub const TestCachedBeaconStateAllForks = struct { cached_state: *CachedBeaconStateAllForks, pub fn init(allocator: Allocator, validator_count: usize) !TestCachedBeaconStateAllForks { + const state = try generateElectraState(allocator, active_chain_config, validator_count); + errdefer state.deinit(allocator); + defer allocator.destroy(state); + + return initFromState(allocator, state); + } + + pub fn initFromState(allocator: Allocator, state: *BeaconStateAllForks) !TestCachedBeaconStateAllForks { + const owned_state = try allocator.create(BeaconStateAllForks); + owned_state.* = state.*; + const pubkey_index_map = try PubkeyIndexMap.init(allocator); const index_pubkey_cache = try allocator.create(Index2PubkeyCache); index_pubkey_cache.* = Index2PubkeyCache.init(allocator); - const state = try generateElectraState(allocator, mainnet_chain_config, validator_count); - const config = try BeaconConfig.init(allocator, mainnet_chain_config, state.genesisValidatorsRoot()); + const config = try BeaconConfig.init(allocator, active_chain_config, owned_state.genesisValidatorsRoot()); - try syncPubkeys(state.validators().items, pubkey_index_map, index_pubkey_cache); + try syncPubkeys(owned_state.validators().items, pubkey_index_map, index_pubkey_cache); const immutable_data = state_transition.EpochCacheImmutableData{ .config = config, .index_to_pubkey = index_pubkey_cache, .pubkey_to_index = pubkey_index_map, }; - const cached_state = try CachedBeaconStateAllForks.createCachedBeaconState(allocator, state, immutable_data, .{ - .skip_sync_committee_cache = false, + const cached_state = try CachedBeaconStateAllForks.createCachedBeaconState(allocator, owned_state, immutable_data, .{ + .skip_sync_committee_cache = owned_state.isPhase0(), .skip_sync_pubkeys = false, }); diff --git a/src/state_transition/types/beacon_state.zig b/src/state_transition/types/beacon_state.zig index 4a20ef43..1339f9a7 100644 --- a/src/state_transition/types/beacon_state.zig +++ b/src/state_transition/types/beacon_state.zig @@ -42,6 +42,45 @@ pub const BeaconStateAllForks = union(enum) { deneb: *BeaconStateDeneb, electra: *BeaconStateElectra, + pub fn init(f: ForkSeq, state_any: anytype) !@This() { + var state: @This() = undefined; + + switch (f) { + .phase0 => { + const T = ssz.phase0.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .phase0 = src }; + }, + .altair => { + const T = ssz.altair.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .altair = src }; + }, + .bellatrix => { + const T = ssz.bellatrix.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .bellatrix = src }; + }, + .capella => { + const T = ssz.capella.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .capella = src }; + }, + .deneb => { + const T = ssz.deneb.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .deneb = src }; + }, + .electra => { + const T = ssz.electra.BeaconState; + const src: *T.Type = @ptrCast(@alignCast(state_any)); + state = .{ .electra = src }; + }, + } + + return state; + } + pub fn format( self: BeaconStateAllForks, comptime fmt: []const u8, diff --git a/src/state_transition/types/execution_payload.zig b/src/state_transition/types/execution_payload.zig index 3c8c8693..c487bb86 100644 --- a/src/state_transition/types/execution_payload.zig +++ b/src/state_transition/types/execution_payload.zig @@ -22,14 +22,24 @@ pub const ExecutionPayload = union(enum) { pub fn toPayloadHeader(self: *const ExecutionPayload, allocator: Allocator) !ExecutionPayloadHeader { return switch (self.*) { .bellatrix => |payload| { - var header = toExecutionPayloadHeader(ssz.bellatrix.ExecutionPayloadHeader.Type, payload); + var header = try toExecutionPayloadHeader( + allocator, + ssz.bellatrix.ExecutionPayloadHeader.Type, + payload, + ); + errdefer header.extra_data.deinit(allocator); try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); return .{ .bellatrix = header, }; }, .capella => |payload| { - var header = toExecutionPayloadHeader(ssz.capella.ExecutionPayloadHeader.Type, payload); + var header = try toExecutionPayloadHeader( + allocator, + ssz.capella.ExecutionPayloadHeader.Type, + payload, + ); + errdefer header.extra_data.deinit(allocator); try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); try ssz.capella.Withdrawals.hashTreeRoot(allocator, &payload.withdrawals, &header.withdrawals_root); return .{ @@ -37,7 +47,12 @@ pub const ExecutionPayload = union(enum) { }; }, .deneb => |payload| { - var header = toExecutionPayloadHeader(ssz.deneb.ExecutionPayloadHeader.Type, payload); + var header = try toExecutionPayloadHeader( + allocator, + ssz.deneb.ExecutionPayloadHeader.Type, + payload, + ); + errdefer header.extra_data.deinit(allocator); try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); try ssz.capella.Withdrawals.hashTreeRoot(allocator, &payload.withdrawals, &header.withdrawals_root); header.blob_gas_used = payload.blob_gas_used; @@ -48,7 +63,12 @@ pub const ExecutionPayload = union(enum) { }, .electra => |payload| { // TODO: dedup to deneb? - var header = toExecutionPayloadHeader(ssz.electra.ExecutionPayloadHeader.Type, payload); + var header = try toExecutionPayloadHeader( + allocator, + ssz.electra.ExecutionPayloadHeader.Type, + payload, + ); + errdefer header.extra_data.deinit(allocator); try ssz.bellatrix.Transactions.hashTreeRoot(allocator, &payload.transactions, &header.transactions_root); try ssz.capella.Withdrawals.hashTreeRoot(allocator, &payload.withdrawals, &header.withdrawals_root); header.blob_gas_used = payload.blob_gas_used; @@ -286,7 +306,11 @@ pub const ExecutionPayloadHeader = union(enum) { }; /// Converts some basic fields of ExecutionPayload to ExecutionPayloadHeader. -pub fn toExecutionPayloadHeader(comptime execution_payload_header_type: type, payload: anytype) execution_payload_header_type { +pub fn toExecutionPayloadHeader( + allocator: Allocator, + comptime execution_payload_header_type: type, + payload: anytype, +) !execution_payload_header_type { var result: execution_payload_header_type = undefined; result.parent_hash = payload.parent_hash; @@ -299,7 +323,7 @@ pub fn toExecutionPayloadHeader(comptime execution_payload_header_type: type, pa result.gas_limit = payload.gas_limit; result.gas_used = payload.gas_used; result.timestamp = payload.timestamp; - result.extra_data = payload.extra_data; + result.extra_data = try payload.extra_data.clone(allocator); result.base_fee_per_gas = payload.base_fee_per_gas; result.block_hash = payload.block_hash; // remaining fields are left unset diff --git a/src/state_transition/utils/block_root.zig b/src/state_transition/utils/block_root.zig index 3c2484ec..cecf632b 100644 --- a/src/state_transition/utils/block_root.zig +++ b/src/state_transition/utils/block_root.zig @@ -13,7 +13,9 @@ pub fn getBlockRootAtSlot(state: *const BeaconStateAllForks, slot: Slot) !Root { return error.SlotTooBig; } - if (slot < state_slot - SLOTS_PER_HISTORICAL_ROOT) { + const oldestStoredSlot = if (state_slot > SLOTS_PER_HISTORICAL_ROOT) state_slot - SLOTS_PER_HISTORICAL_ROOT else 0; + + if (slot < oldestStoredSlot) { return error.SlotTooSmall; } diff --git a/test/int/process_withdrawals.zig b/test/int/process_withdrawals.zig index 404af418..fbf5ff37 100644 --- a/test/int/process_withdrawals.zig +++ b/test/int/process_withdrawals.zig @@ -13,7 +13,7 @@ test "process withdrawals - sanity" { var withdrawal_balances = std.AutoHashMap(ValidatorIndex, usize).init(allocator); defer withdrawal_balances.deinit(); - try getExpectedWithdrawalsResult(allocator, &withdrawals_result, &withdrawal_balances, test_state.cached_state); + try getExpectedWithdrawals(allocator, &withdrawals_result, &withdrawal_balances, test_state.cached_state); try processWithdrawals(test_state.cached_state, withdrawals_result); } @@ -23,7 +23,7 @@ const state_transition = @import("state_transition"); const preset = @import("preset").preset; const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; const processWithdrawals = state_transition.processWithdrawals; -const getExpectedWithdrawalsResult = state_transition.getExpectedWithdrawalsResult; +const getExpectedWithdrawals = state_transition.getExpectedWithdrawals; const WithdrawalsResult = state_transition.WithdrawalsResult; const ssz = @import("consensus_types"); const Withdrawals = ssz.capella.Withdrawals.Type; diff --git a/test/spec/README.md b/test/spec/README.md new file mode 100644 index 00000000..bd7c81ed --- /dev/null +++ b/test/spec/README.md @@ -0,0 +1,46 @@ +# Ethereum Consensus Spec Test Framework + +This framework generates and runs Zig-based tests for Ethereum consensus specifications, based on the official test formats from [ethereum/consensus-specs](https://github.com/ethereum/consensus-specs/tree/master/tests/formats). + +## Overview + +The framework automates the creation of test files for various forks and test runners, ensuring compliance with spec tests. + +Key components: +- **write_spec_tests.zig**: Main script to generate test files. +- **writer/**: Directory containing writer implementations for generating test code. +- **runner/**: Directory containing runner implementations (e.g., Operations.zig). + +Generated files: +- `test/spec/test_case/_tests.zig`: Contains test functions for each runner. +- `test/spec/root.zig`: Imports all generated tests. + +## Usage + +1. Run `zig build run:write_spec_tests` to generate test files from spec test data. + - Spec tests can be re-downloaded by running `zig build run:download_spec_tests` +2. Execute tests with `zig build test:spec_tests`. + - Specific tests can be run by adding filters + - Minimal preset can be used with `-Dpreset=minimal` + +## Supported Components + +- **Forks**: See `supported_forks` in `write_spec_tests.zig` +- **Test Runners**: See `supported_test_runners` in `write_spec_tests.zig`; see below for adding more. + +## Adding a New Test Runner + +To add support for a new test runner (e.g., `fork` or `sanity`): + +1. **Implement the Runner Module**: Create `runner/NewRunner.zig` defining the test case structure, handlers, and execution logic (similar to `Operations.zig`). +2. **Implement the Writer Module**: Create `writer/NewRunner.zig` with decls to generate test code (e.g.,`handlers`, `writeHeader` and `writeTest`). +3. **Update RunnerKind Enum**: Add the new runner to the `RunnerKind` enum in `runner_kind.zig`. +4. **Modify write_spec_tests.zig**: Add the new runner to the `supported_test_runners`, Add cases in the switches for `TestWriter` to import and use the new modules. + +Ensure the new runner follows the spec test formats and integrates with existing state transition logic. + +## Notes + +- Generated files are auto-created; do not edit manually. +- Skips unsupported or incomplete test cases (e.g., certain operations or runners). +- Relies on external spec test data in SSZ format. diff --git a/test/spec/root.zig b/test/spec/root.zig new file mode 100644 index 00000000..619e1d2f --- /dev/null +++ b/test/spec/root.zig @@ -0,0 +1,9 @@ +// This file is generated by write_spec_tests.zig. +// Do not commit changes by hand. + +const testing = @import("std").testing; + +comptime { + testing.refAllDecls(@import("./test_case/operations_tests.zig")); + testing.refAllDecls(@import("./test_case/sanity_tests.zig")); +} diff --git a/test/spec/runner/Operations.zig b/test/spec/runner/Operations.zig new file mode 100644 index 00000000..567cf8da --- /dev/null +++ b/test/spec/runner/Operations.zig @@ -0,0 +1,234 @@ +const ssz = @import("consensus_types"); +const ForkSeq = @import("config").ForkSeq; +const Preset = @import("preset").Preset; +const preset = @import("preset").preset; +const std = @import("std"); +const state_transition = @import("state_transition"); +const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; +const BeaconStateAllForks = state_transition.BeaconStateAllForks; +const Withdrawals = ssz.capella.Withdrawals.Type; +const WithdrawalsResult = state_transition.WithdrawalsResult; +const test_case = @import("../test_case.zig"); +const loadSszValue = test_case.loadSszSnappyValue; +const loadBlsSetting = test_case.loadBlsSetting; +const expectEqualBeaconStates = test_case.expectEqualBeaconStates; +const BlsSetting = test_case.BlsSetting; + +/// See https://github.com/ethereum/consensus-specs/tree/master/tests/formats/operations#operations-tests +pub const Operation = enum { + attestation, + attester_slashing, + block_header, + bls_to_execution_change, + consolidation_request, + deposit, + deposit_request, + execution_payload, + proposer_slashing, + sync_aggregate, + voluntary_exit, + withdrawal_request, + withdrawals, + + pub fn inputName(self: Operation) []const u8 { + return switch (self) { + .block_header => "block", + .bls_to_execution_change => "address_change", + .execution_payload => "body", + .withdrawals => "execution_payload", + else => @tagName(self), + }; + } + + pub fn operationObject(self: Operation) []const u8 { + return switch (self) { + .attestation => "Attestation", + .attester_slashing => "AttesterSlashing", + .block_header => "BeaconBlock", + .bls_to_execution_change => "SignedBLSToExecutionChange", + .consolidation_request => "ConsolidationRequest", + .deposit => "Deposit", + .deposit_request => "DepositRequest", + .execution_payload => "BeaconBlockBody", + .proposer_slashing => "ProposerSlashing", + .sync_aggregate => "SyncAggregate", + .voluntary_exit => "SignedVoluntaryExit", + .withdrawal_request => "WithdrawalRequest", + .withdrawals => "ExecutionPayload", + }; + } + + pub fn suiteName(self: Operation) []const u8 { + return @tagName(self) ++ "/pyspec_tests"; + } +}; + +pub const Handler = Operation; + +pub fn TestCase(comptime fork: ForkSeq, comptime operation: Operation, comptime valid: bool) type { + const ForkTypes = @field(ssz, fork.forkName()); + const OpType = @field(ForkTypes, operation.operationObject()); + + return struct { + pre: TestCachedBeaconStateAllForks, + post: if (valid) BeaconStateAllForks else void, + op: OpType.Type, + bls_setting: BlsSetting, + + 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(); + + try tc.runTest(); + } + + pub fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { + var tc = Self{ + .pre = undefined, + .post = undefined, + .op = OpType.default_value, + .bls_setting = loadBlsSetting(allocator, dir), + }; + // init the op + + try loadSszValue(OpType, allocator, dir, comptime operation.inputName() ++ ".ssz_snappy", &tc.op); + errdefer { + if (comptime @hasDecl(OpType, "deinit")) { + OpType.deinit(allocator, &tc.op); + } + } + + // init the pre state + + const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + errdefer { + 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); + + var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); + + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + + // init the post state if this is a "valid" test case + + if (valid) { + 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; + } + + pub fn deinit(self: *Self) void { + if (comptime @hasDecl(OpType, "deinit")) { + OpType.deinit(self.pre.allocator, &self.op); + } + self.pre.deinit(); + if (valid) { + self.post.deinit(self.pre.allocator); + } + } + + pub fn process(self: *Self) !void { + const verify = self.bls_setting.verify(); + + switch (operation) { + .attestation => { + const attestations_fork: ForkSeq = if (fork.gte(.electra)) .electra else .phase0; + var attestations = @field(ssz, attestations_fork.forkName()).Attestations.default_value; + defer attestations.deinit(self.pre.allocator); + const attestation: *@field(ssz, attestations_fork.forkName()).Attestation.Type = @ptrCast(@alignCast(&self.op)); + try attestations.append(self.pre.allocator, attestation.*); + const atts = attestations; + const attestations_wrapper: state_transition.Attestations = if (fork.gte(.electra)) + .{ .electra = &atts } + else + .{ .phase0 = &atts }; + + try state_transition.processAttestations(self.pre.allocator, self.pre.cached_state, attestations_wrapper, verify); + }, + .attester_slashing => { + try state_transition.processAttesterSlashing(OpType.Type, self.pre.cached_state, &self.op, verify); + }, + .block_header => { + return error.SkipZigTest; + // TODO: processBlockHeader currently takes signed block which is incorrect. Wait for it to accept unsigned block. + // try state_transition.processBlockHeader(self.pre.allocator, self.pre.cached_state, &self.op); + }, + .bls_to_execution_change => { + try state_transition.processBlsToExecutionChange(self.pre.cached_state, &self.op); + }, + .consolidation_request => { + try state_transition.processConsolidationRequest(self.pre.allocator, self.pre.cached_state, &self.op); + }, + .deposit => { + try state_transition.processDeposit(self.pre.allocator, self.pre.cached_state, &self.op); + }, + .deposit_request => { + try state_transition.processDepositRequest(self.pre.allocator, self.pre.cached_state, &self.op); + }, + .execution_payload => { + try state_transition.processExecutionPayload( + self.pre.allocator, + self.pre.cached_state, + .{ .regular = @unionInit(state_transition.BeaconBlockBody, @tagName(fork), &self.op) }, + .{ .data_availability_status = .available, .execution_payload_status = .valid }, + ); + }, + .proposer_slashing => { + try state_transition.processProposerSlashing(self.pre.cached_state, &self.op, verify); + }, + .sync_aggregate => { + return error.SkipZigTest; + }, + .voluntary_exit => { + try state_transition.processVoluntaryExit(self.pre.cached_state, &self.op, verify); + }, + .withdrawal_request => { + try state_transition.processWithdrawalRequest(self.pre.allocator, self.pre.cached_state, &self.op); + }, + .withdrawals => { + var withdrawals_result = WithdrawalsResult{ + .withdrawals = try Withdrawals.initCapacity( + self.pre.allocator, + preset.MAX_WITHDRAWALS_PER_PAYLOAD, + ), + }; + + var withdrawal_balances = std.AutoHashMap(u64, usize).init(self.pre.allocator); + defer withdrawal_balances.deinit(); + + 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); + }, + } + } + + pub fn runTest(self: *Self) !void { + if (valid) { + try self.process(); + try expectEqualBeaconStates(self.post, self.pre.cached_state.state.*); + } else { + self.process() catch |err| { + if (err == error.SkipZigTest) { + return err; + } + return; + }; + return error.ExpectedError; + } + } + }; +} diff --git a/test/spec/runner/Sanity.zig b/test/spec/runner/Sanity.zig new file mode 100644 index 00000000..4852f0c9 --- /dev/null +++ b/test/spec/runner/Sanity.zig @@ -0,0 +1,225 @@ +const std = @import("std"); +const ssz = @import("consensus_types"); +const ForkSeq = @import("config").ForkSeq; +const Preset = @import("preset").Preset; +const state_transition = @import("state_transition"); +const TestCachedBeaconStateAllForks = state_transition.test_utils.TestCachedBeaconStateAllForks; +const BeaconStateAllForks = state_transition.BeaconStateAllForks; +const test_case = @import("../test_case.zig"); +const loadSszValue = test_case.loadSszSnappyValue; +const expectEqualBeaconStates = test_case.expectEqualBeaconStates; + +/// https://github.com/ethereum/consensus-specs/blob/master/tests/formats/sanity/README.md +pub const Handler = enum { + /// https://github.com/ethereum/consensus-specs/blob/master/tests/formats/sanity/blocks.md + blocks, + /// https://github.com/ethereum/consensus-specs/blob/master/tests/formats/sanity/slots.md + slots, + + pub fn suiteName(self: Handler) []const u8 { + return @tagName(self) ++ "/pyspec_tests"; + } +}; + +pub fn SlotsTestCase(comptime fork: ForkSeq) type { + const ForkTypes = @field(ssz, fork.forkName()); + + return struct { + pre: TestCachedBeaconStateAllForks, + post: BeaconStateAllForks, + slots: 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(); + + try tc.runTest(); + } + + pub fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { + var tc = Self{ + .pre = undefined, + .post = undefined, + .slots = 0, + }; + + // Load slots + var slots_file = try dir.openFile("slots.yaml", .{}); + defer slots_file.close(); + 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; + + // Load pre state + const pre_state = try allocator.create(ForkTypes.BeaconState.Type); + errdefer { + 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); + + var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + + // 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; + } + + pub fn deinit(self: *Self) void { + self.pre.deinit(); + self.post.deinit(self.pre.allocator); + } + + pub fn process(self: *Self) !void { + try state_transition.state_transition.processSlotsWithTransientCache( + self.pre.allocator, + self.pre.cached_state, + self.pre.cached_state.state.slot() + self.slots, + undefined, + ); + } + + pub fn runTest(self: *Self) !void { + try self.process(); + try expectEqualBeaconStates(self.post, self.pre.cached_state.state.*); + } + }; +} + +pub fn BlocksTestCase(comptime fork: ForkSeq, comptime valid: bool) type { + const ForkTypes = @field(ssz, fork.forkName()); + const SignedBeaconBlock = @field(ForkTypes, "SignedBeaconBlock"); + + return struct { + pre: TestCachedBeaconStateAllForks, + post: if (valid) BeaconStateAllForks else void, + blocks: []SignedBeaconBlock.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(); + + try tc.runTest(); + } + + pub fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) !Self { + var tc = Self{ + .pre = undefined, + .post = undefined, + .blocks = undefined, + }; + + // Load meta.yaml for blocks_count + var meta_file = try dir.openFile("meta.yaml", .{}); + defer meta_file.close(); + const meta_content = try meta_file.readToEndAlloc(allocator, 1024); + defer allocator.free(meta_content); + // Parse YAML for blocks_count (simplified; assume "blocks_count: N") + const blocks_count_str = std.mem.trim(u8, meta_content, " \n{}"); + const blocks_count = if (std.mem.indexOf(u8, blocks_count_str, "blocks_count: ")) |start| blk: { + const num_str = blocks_count_str[start + "blocks_count: ".len ..]; + 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); + errdefer { + 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); + + var pre_state_all_forks = try BeaconStateAllForks.init(fork, pre_state); + tc.pre = try TestCachedBeaconStateAllForks.initFromState(allocator, &pre_state_all_forks); + + // Load blocks + tc.blocks = try allocator.alloc(SignedBeaconBlock.Type, blocks_count); + errdefer { + for (tc.blocks) |*block| { + SignedBeaconBlock.deinit(allocator, block); + } + allocator.free(tc.blocks); + } + for (tc.blocks, 0..) |*block, i| { + block.* = SignedBeaconBlock.default_value; + const block_filename = try std.fmt.allocPrint(allocator, "block_{d}.ssz_snappy", .{i}); + defer allocator.free(block_filename); + try loadSszValue(SignedBeaconBlock, allocator, dir, block_filename, block); + } + + // Load post state if valid + if (valid) { + 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; + } + + pub fn deinit(self: *Self) void { + for (self.blocks) |*block| { + if (comptime @hasDecl(SignedBeaconBlock, "deinit")) { + SignedBeaconBlock.deinit(self.pre.allocator, block); + } + } + self.pre.allocator.free(self.blocks); + self.pre.deinit(); + if (valid) { + self.post.deinit(self.pre.allocator); + } + } + + pub fn process(self: *Self) !void { + var state = self.pre.cached_state; + for (self.blocks) |*block| { + const signed_block = @unionInit(state_transition.SignedBeaconBlock, @tagName(fork), block); + state = try state_transition.state_transition.stateTransition( + self.pre.allocator, + state, + .{ + .regular = &signed_block, + }, + .{}, + ); + } + self.pre.cached_state = state; + } + + pub fn runTest(self: *Self) !void { + if (valid) { + try self.process(); + try expectEqualBeaconStates(self.post, self.pre.cached_state.state.*); + } else { + self.process() catch |err| { + if (err == error.SkipZigTest) { + return err; + } + return; + }; + return error.ExpectedError; + } + } + }; +} diff --git a/test/spec/runner_kind.zig b/test/spec/runner_kind.zig new file mode 100644 index 00000000..e3c10f2b --- /dev/null +++ b/test/spec/runner_kind.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +pub const RunnerKind = enum { + epoch_processing, + finality, + operations, + random, + rewards, + sanity, + shuffling, +}; diff --git a/test/spec/test_case.zig b/test/spec/test_case.zig new file mode 100644 index 00000000..7f7e20ed --- /dev/null +++ b/test/spec/test_case.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const snappy = @import("snappy"); +const ForkSeq = @import("config").ForkSeq; +const isFixedType = @import("ssz").isFixedType; +const BeaconStateAllForks = @import("state_transition").BeaconStateAllForks; + +const consensus_types = @import("consensus_types"); +const phase0 = consensus_types.phase0; +const altair = consensus_types.altair; +const bellatrix = consensus_types.bellatrix; +const capella = consensus_types.capella; +const deneb = consensus_types.deneb; +const electra = consensus_types.electra; + +pub const BlsSetting = enum { + default, + required, + ignored, + + pub fn verify(self: BlsSetting) bool { + return switch (self) { + .default, .required => true, + .ignored => false, + }; + } +}; + +pub fn loadBlsSetting(allocator: std.mem.Allocator, dir: std.fs.Dir) BlsSetting { + var file = dir.openFile("meta.yaml", .{}) catch return .default; + defer file.close(); + + const contents = file.readToEndAlloc(allocator, 100) catch return .default; + defer allocator.free(contents); + + if (std.mem.indexOf(u8, contents, "bls_setting: 0") != null) { + return .default; + } else if (std.mem.indexOf(u8, contents, "bls_setting: 1") != null) { + return .required; + } else if (std.mem.indexOf(u8, contents, "bls_setting: 2") != null) { + return .ignored; + } else { + return .default; + } +} + +pub fn loadSszSnappyValue(comptime ST: type, allocator: std.mem.Allocator, dir: std.fs.Dir, file_name: []const u8, out: *ST.Type) !void { + var object_file = try dir.openFile(file_name, .{}); + defer object_file.close(); + + const value_bytes = try object_file.readToEndAlloc(allocator, 100_000_000); + defer allocator.free(value_bytes); + + const serialized_buf = try allocator.alloc(u8, try snappy.uncompressedLength(value_bytes)); + defer allocator.free(serialized_buf); + const serialized_len = try snappy.uncompress(value_bytes, serialized_buf); + const serialized = serialized_buf[0..serialized_len]; + + if (comptime isFixedType(ST)) { + try ST.deserializeFromBytes(serialized, out); + } else { + try ST.deserializeFromBytes(allocator, serialized, out); + } +} + +pub fn expectEqualBeaconStates(expected: BeaconStateAllForks, actual: BeaconStateAllForks) !void { + if (expected.forkSeq() != actual.forkSeq()) return error.ForkMismatch; + + switch (expected.forkSeq()) { + .phase0 => { + if (!phase0.BeaconState.equals(expected.phase0, actual.phase0)) return error.NotEqual; + }, + .altair => { + if (!altair.BeaconState.equals(expected.altair, actual.altair)) return error.NotEqual; + }, + .bellatrix => { + if (!bellatrix.BeaconState.equals(expected.bellatrix, actual.bellatrix)) return error.NotEqual; + }, + .capella => { + if (!capella.BeaconState.equals(expected.capella, actual.capella)) return error.NotEqual; + }, + .deneb => { + if (!deneb.BeaconState.equals(expected.deneb, actual.deneb)) return error.NotEqual; + }, + .electra => { + if (!electra.BeaconState.equals(expected.electra, actual.electra)) return error.NotEqual; + }, + } +} diff --git a/test/spec/write_spec_tests.zig b/test/spec/write_spec_tests.zig new file mode 100644 index 00000000..94cf28dd --- /dev/null +++ b/test/spec/write_spec_tests.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const ForkSeq = @import("config").ForkSeq; +const spec_test_options = @import("spec_test_options"); +const RunnerKind = @import("./runner_kind.zig").RunnerKind; + +const supported_forks = [_]ForkSeq{ + .phase0, + .altair, + .bellatrix, + .capella, + .deneb, + .electra, +}; + +const supported_test_runners = [_]RunnerKind{ + .operations, + .sanity, +}; + +fn TestWriter(comptime kind: RunnerKind) type { + return switch (kind) { + .operations => @import("./writer/Operations.zig"), + .sanity => @import("./writer/Sanity.zig"), + else => @compileError("Unsupported test runner"), + }; +} + +pub fn main() !void { + const test_case_dir = "test/spec/test_case/"; + + inline for (supported_test_runners) |kind| { + const test_case_file = test_case_dir ++ @tagName(kind) ++ "_tests.zig"; + const out = try std.fs.cwd().createFile(test_case_file, .{}); + defer out.close(); + + const writer = out.writer().any(); + try writeTests(&supported_forks, kind, writer); + } + + { + const test_root_file = "test/spec/root.zig"; + const out = try std.fs.cwd().createFile(test_root_file, .{}); + defer out.close(); + const writer = out.writer().any(); + try writeTestRoot(&supported_test_runners, writer); + } +} + +pub fn writeTestRoot(comptime kinds: []const RunnerKind, writer: std.io.AnyWriter) !void { + try writer.print( + \\// This file is generated by write_spec_tests.zig. + \\// Do not commit changes by hand. + \\ + \\const testing = @import("std").testing; + \\ + \\comptime {{ + \\ + , .{}); + inline for (kinds) |kind| { + try writer.print( + \\ testing.refAllDecls(@import("./test_case/{s}_tests.zig")); + \\ + , .{@tagName(kind)}); + } + try writer.print( + \\}} + \\ + , .{}); +} + +pub fn writeTests( + comptime forks: []const ForkSeq, + comptime kind: RunnerKind, + writer: std.io.AnyWriter, +) !void { + try TestWriter(kind).writeHeader(writer); + + var root_dir = try std.fs.cwd().openDir(spec_test_options.spec_test_out_dir ++ "/" ++ spec_test_options.spec_test_version, .{}); + defer root_dir.close(); + + // minimal preset includes many more testcases and is a superset of mainnet testcases + var preset_dir = try root_dir.openDir("minimal/tests/minimal", .{}); + defer preset_dir.close(); + + inline for (forks) |fork| { + var fork_dir = try preset_dir.openDir(@tagName(fork) ++ "/" ++ @tagName(kind), .{}); + 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; + 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; + + try TestWriter(kind).writeTest(writer, fork, handler, test_case_name); + } + } + } + } +} diff --git a/test/spec/writer/Operations.zig b/test/spec/writer/Operations.zig new file mode 100644 index 00000000..e334a8e0 --- /dev/null +++ b/test/spec/writer/Operations.zig @@ -0,0 +1,64 @@ +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; + +pub const handlers = std.enums.values(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 Operations = @import("../runner/Operations.zig"); + \\ + \\const allocator = std.testing.allocator; + \\ + \\ +; + +const test_template = + \\test "{s} operations {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}/operations/{s}/pyspec_tests/{s}", + \\ }}); + \\ 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); + \\}} + \\ + \\ +; + +pub fn writeHeader(writer: std.io.AnyWriter) !void { + try writer.print(header, .{}); +} + +pub fn writeTest( + writer: std.io.AnyWriter, + fork: ForkSeq, + 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), + test_case_name, + + @tagName(fork), + @tagName(handler), + test_case_name, + + @tagName(fork), + @tagName(handler), + valid, + }); +} diff --git a/test/spec/writer/Sanity.zig b/test/spec/writer/Sanity.zig new file mode 100644 index 00000000..0bf44d70 --- /dev/null +++ b/test/spec/writer/Sanity.zig @@ -0,0 +1,67 @@ +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; + +pub const handlers = std.enums.values(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 Sanity = @import("../runner/Sanity.zig"); + \\ + \\const allocator = std.testing.allocator; + \\ + \\ +; + +const test_template = + \\test "{s} sanity {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}/sanity/{s}/pyspec_tests/{s}", + \\ }}); + \\ defer allocator.free(test_dir_name); + \\ const test_dir = std.fs.cwd().openDir(test_dir_name, .{{}}) catch return error.SkipZigTest; + \\ + \\ {s} + \\}} + \\ + \\ +; + +pub fn writeHeader(writer: std.io.AnyWriter) !void { + try writer.print(header, .{}); +} + +pub fn writeTest( + writer: std.io.AnyWriter, + fork: ForkSeq, + 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, + }; + defer std.heap.page_allocator.free(execute_call); + try writer.print(test_template, .{ + @tagName(fork), + @tagName(handler), + test_case_name, + + @tagName(fork), + @tagName(handler), + test_case_name, + + execute_call, + }); +} diff --git a/zbuild.zon b/zbuild.zon index 1ac559c4..f7d05f0f 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -13,7 +13,11 @@ .url = "git+https://github.com/chainsafe/blst-z#4e64cb54bf7e0c4a7ff8af67cebe9a31bcccaecb", }, .ssz = .{ - .url = "git+https://github.com/chainsafe/ssz-z#76cde3c7ea23b33d2789e8e372003272abe0e335", + .url = "git+https://github.com/chainSafe/ssz-z#76cde3c7ea23b33d2789e8e372003272abe0e335", + }, + .snappy = .{ + .url = "git+https://github.com/chainsafe/snappy.zig#ede2ad602ac9ffa506e3724a2bf5fc14c806187f", + .hash = "snappy-0.1.0-n4AaqtMYAACgB1kHWQ2_CFI-gbtYEUCbXyYlQZ2ENyfd", }, }, .options_modules = .{ @@ -77,6 +81,12 @@ .imports = .{.spec_test_options}, }, }, + .write_spec_tests = .{ + .root_module = .{ + .root_source_file = "test/spec/write_spec_tests.zig", + .imports = .{ .spec_test_options, .config, .preset, .consensus_types, .state_transition }, + }, + }, }, .tests = .{ .int = .{ @@ -94,5 +104,21 @@ }, .filters = .{}, }, + .spec_tests = .{ + .root_module = .{ + .root_source_file = "test/spec/root.zig", + .imports = .{ + .spec_test_options, + .consensus_types, + .config, + .preset, + .snappy, + .state_transition, + .ssz, + .blst, + }, + }, + .filters = .{}, + }, }, }