Skip to content

feat(spectest): add SSZ roundtrip runner with comptime type dispatch#715

Open
GrapeBaBa wants to merge 9 commits intomainfrom
feat/spectest-all-81-fixtures
Open

feat(spectest): add SSZ roundtrip runner with comptime type dispatch#715
GrapeBaBa wants to merge 9 commits intomainfrom
feat/spectest-all-81-fixtures

Conversation

@GrapeBaBa
Copy link
Copy Markdown
Member

@GrapeBaBa GrapeBaBa commented Apr 10, 2026

Summary

  • Add SSZ fixture runner that tests serialization roundtrips for all 18 consensus types
  • Uses comptime inline for over a type map to generate specialized deserialize/serialize code per type
  • Includes test-mode type mirrors with 424-byte signatures for leanEnv=test fixtures
  • All 34 SSZ spectest fixtures pass (24 consensus + 5 networking + 5 xmss containers)
  • Total spec tests: 73 (14 state_transition + 25 fork_choice + 34 SSZ)

Changes

  • pkgs/spectest/src/runner/ssz_runner.zig — New SSZ roundtrip runner
  • pkgs/spectest/src/fixture_kind.zig — Add ssz variant
  • pkgs/spectest/src/generator.zig — Add ssz to empty index
  • pkgs/spectest/src/lib.zig — Export ssz_runner

Test plan

  • zig build spectest:generate discovers 34 SSZ fixtures
  • zig build spectest passes all 73 tests (exit code 0)
  • No hashsig/xmss/state-transition changes — SSZ runner is self-contained

Copilot AI review requested due to automatic review settings April 10, 2026 08:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the remaining LeanSpec fixture runners and signature-scheme dispatch needed to get the full leanSpec suite passing end-to-end (SSZ roundtrips, signature verification, and state transition/fork choice), including test-vs-prod XMSS scheme support through Rust FFI + Zig bindings.

Changes:

  • Adds Rust/Zig plumbing to verify SSZ-encoded signatures under either the test or production XMSS scheme (and updates signature sizes accordingly).
  • Introduces new spectest runners for SSZ roundtrip fixtures and verify_signatures fixtures, including test-mode type mirrors to handle 424-byte test signatures.
  • Extends spectest fixture-kind plumbing and build wiring to include the new runners and link required Rust glue.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
rust/hashsig-glue/src/lib.rs Adds scheme-aware SSZ signature verification and JSON→SSZ signature conversion helpers; updates RNG usage.
rust/hashsig-glue/Cargo.toml Pins leansig to a specific rev; adds rand_chacha + serde_json; adjusts rand version.
rust/Cargo.lock Introduces additional git-sourced dependency entries (notably a second leansig source).
pkgs/xmss/src/lib.zig Re-exports scheme IDs, signature sizes, and new helper(s) from hashsig.
pkgs/xmss/src/hashsig.zig Adds HashSigScheme dispatch, signature-size helpers, and FFI for scheme-aware verify + JSON→SSZ conversion.
pkgs/types/src/utils.zig Updates SIGSIZE to match production signature SSZ length.
pkgs/state-transition/src/transition.zig Plumbs scheme selection into signature verification and adds a scheme-aware attestation verify function.
pkgs/state-transition/src/lib.zig Re-exports verifySingleAttestationWithScheme.
pkgs/spectest/src/runner/verify_signatures_runner.zig New runner to validate proposer + attestation signatures from fixtures under test/prod schemes.
pkgs/spectest/src/runner/ssz_runner.zig New runner for SSZ roundtrip fixtures with comptime type dispatch and test-mode mirrors for signature-sized types.
pkgs/spectest/src/lib.zig Exposes the new runners.
pkgs/spectest/src/json_expect.zig Adds an expectArrayField helper used by fixture parsing.
pkgs/spectest/src/generator.zig Updates generated empty index scaffolding for new fixture kinds.
pkgs/spectest/src/fixture_kind.zig Adds ssz and verify_signatures kinds and includes them in all.
build.zig Wires @zeam/xmss and Rust glue linkage into the spectest build target.
Comments suppressed due to low confidence (1)

rust/hashsig-glue/src/lib.rs:112

  • PrivateKey::generate now accepts any R: Rng, which allows non-cryptographic RNGs to be used for key generation. To avoid accidental insecure usage, keep the stronger bound (e.g., R: Rng + CryptoRng / RngCore + CryptoRng) matching what XmssScheme::key_gen expects.
    pub fn generate<R: Rng>(
        rng: &mut R,
        activation_epoch: u32,
        num_active_epochs: u32,
    ) -> (PublicKey, Self) {
        let (pk, sk) =
            XmssScheme::key_gen(rng, activation_epoch as usize, num_active_epochs as usize);


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

signature_scheme: xmss.HashSigScheme,
) !void {
const signature_ssz_len = xmss.signatureSszLenForScheme(signature_scheme);
if (signature_ssz_len > signatureBytes.len) {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signatureBytes is a *const types.SIGBYTES (pointer to a fixed-size array), so signatureBytes.len is not valid in Zig and will not compile. Compare against the array length via signatureBytes.*.len (or types.SIGSIZE) instead.

Suggested change
if (signature_ssz_len > signatureBytes.len) {
if (signature_ssz_len > signatureBytes.*.len) {

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +328
const pubkey = xmss.PublicKey.fromBytes(pubkey_bytes) catch return false;
pub_keys.append(allocator, pubkey.handle) catch return false;
pk_wrappers.append(allocator, pubkey) catch return false;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runner builds xmss.PublicKey wrappers and appends them into pub_keys / pk_wrappers, but on allocation failure the locally created pubkey isn't deinitialized before returning. Use errdefer pubkey.deinit() (and/or structure the append sequence) so failures don't leak FFI handles.

Suggested change
const pubkey = xmss.PublicKey.fromBytes(pubkey_bytes) catch return false;
pub_keys.append(allocator, pubkey.handle) catch return false;
pk_wrappers.append(allocator, pubkey) catch return false;
var pubkey: ?xmss.PublicKey = xmss.PublicKey.fromBytes(pubkey_bytes) catch return false;
errdefer if (pubkey) |*wrapper| wrapper.deinit();
pk_wrappers.append(allocator, pubkey.?) catch return false;
pubkey = null;
pub_keys.append(allocator, pk_wrappers.items[pk_wrappers.items.len - 1].handle) catch {
var wrapper = pk_wrappers.pop();
wrapper.deinit();
return false;
};

Copilot uses AI. Check for mistakes.
return false;
};

const epoch: u32 = @intCast(block.slot);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block.slot is a u64 but is cast to u32 with @intCast, which will truncate on large slots and can make signature verification incorrect. Use a checked cast (e.g., std.math.cast(u32, block.slot)) and treat out-of-range values as an invalid fixture.

Suggested change
const epoch: u32 = @intCast(block.slot);
const epoch = std.math.cast(u32, block.slot) orelse {
std.debug.print("fixture {s} case {s}: invalid block slot\n", .{ ctx.fixture_label, ctx.case_name });
return false;
};

Copilot uses AI. Check for mistakes.
Add SSZ fixture runner that tests serialization roundtrips for all 18
consensus types. Uses test-mode type mirrors with 424-byte signatures
for leanEnv=test fixtures, enabling all 34 SSZ tests to pass without
requiring hashsig infrastructure changes.
@GrapeBaBa GrapeBaBa force-pushed the feat/spectest-all-81-fixtures branch from 078b369 to 273dad7 Compare April 10, 2026 09:47
@GrapeBaBa GrapeBaBa changed the title feat: implement all 81 leanSpec spec tests feat(spectest): add SSZ roundtrip runner with comptime type dispatch Apr 10, 2026
Update leanSpec to 1177800 which includes the latest spectest fixture
vectors for SSZ, verify_signatures, and other test categories.
Copilot AI review requested due to automatic review settings April 10, 2026 10:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +12
const fixture_kind = @import("../fixture_kind.zig");
const skip = @import("../skip.zig");

const Fork = forks.Fork;
const FixtureKind = fixture_kind.FixtureKind;
const types = @import("@zeam/types");
const params = @import("@zeam/params");
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file declares several top-level constants that are never referenced (fixture_kind/FixtureKind, skip, and params). Zig treats unused top-level declarations as compile errors, so this will fail to build. Remove the unused imports/aliases (or wire them into the runner if they’re intended to be used).

Suggested change
const fixture_kind = @import("../fixture_kind.zig");
const skip = @import("../skip.zig");
const Fork = forks.Fork;
const FixtureKind = fixture_kind.FixtureKind;
const types = @import("@zeam/types");
const params = @import("@zeam/params");
const Fork = forks.Fork;
const types = @import("@zeam/types");

Copilot uses AI. Check for mistakes.
… type

The updated leanSpec generates new fork_choice fixtures with
gossipAggregatedAttestation steps. Mark this step type as unsupported
(graceful skip) alongside the existing attestation step type, so these
tests don't fail with InvalidFixture.
… fork choice steps

Add full attestation step support to the fork choice runner instead of
skipping them as unsupported. This resolves 15 previously failing fork
choice spec tests.

Changes:
- Add processAttestationStep with validator bounds check, attestation
  data validation, and signature error detection
- Add processGossipAggregatedAttestationStep with aggregation proof
  handling and store integration
- Add validateAttestationDataForGossip implementing leanSpec validation
  rules (block existence, slot relationships, future slot tolerance)
- Align forkchoice.zig future attestation check with leanSpec (allow
  +1 slot tolerance)
- Consolidate KeyManager error variants into ValidatorKeyNotFound
- Move MAX_ATTESTATIONS_DATA to constants module
Copilot AI review requested due to automatic review settings April 10, 2026 13:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkgs/node/src/chain.zig Outdated
Comment on lines +912 to +917
if (att_data_set.count() > constants.MAX_ATTESTATIONS_DATA) {
self.logger.err(
"block contains {d} distinct AttestationData entries (max {d}) for block root=0x{x}",
.{ att_data_set.count(), self.config.spec.max_attestations_data, &freshFcBlock.blockRoot },
.{ att_data_set.count(), constants.MAX_ATTESTATIONS_DATA, &freshFcBlock.blockRoot },
);
return BlockProcessingError.TooManyAttestationData;
return BlockProcessingError.InvalidSignatureGroups;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constants.MAX_ATTESTATIONS_DATA hard-codes the max distinct AttestationData per block to 8, but the rest of the codebase (e.g. ChainSpec.max_attestations_data defaults to 16 and forkchoice block-building respects that) still treats this as configurable. This introduces inconsistent limits between block production/selection and block processing, potentially rejecting otherwise-valid blocks. Prefer using self.config.spec.max_attestations_data here (or update the spec/defaults and forkchoice to use the same source of truth).

Copilot uses AI. Check for mistakes.
Comment thread pkgs/node/src/chain.zig
Comment on lines 898 to 910
// Each unique AttestationData must appear at most once per block.
{
var att_data_set = std.AutoHashMap(types.AttestationData, void).init(self.allocator);
defer att_data_set.deinit();
for (aggregated_attestations) |agg_att| {
const result = try att_data_set.getOrPut(agg_att.data);
if (result.found_existing) {
self.logger.err(
"block contains duplicate AttestationData entries for block root=0x{x}",
.{&freshFcBlock.blockRoot},
);
return BlockProcessingError.DuplicateAttestationData;
return BlockProcessingError.InvalidSignatureGroups;
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes two distinct block-processing conditions (duplicate AttestationData and too many distinct AttestationData) to return InvalidSignatureGroups. That error name no longer matches the failure mode and makes it harder for callers/tests to distinguish data-shape issues from signature-group problems. Consider reintroducing a dedicated error (or renaming/generalizing the error) so the returned error communicates the actual validation failure.

Copilot uses AI. Check for mistakes.
// assert data.slot <= current_slot + Slot(1)
// In production, chain.validateAttestationData enforces the stricter gossip
// check (data.slot <= current_slot) upstream.
if (attestation_slot > self.fcStore.slot_clock.timeSlots.load(.monotonic) + 1) {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file already imports constants.zig and defines MAX_FUTURE_SLOT_TOLERANCE = 1, but the future-slot check uses a literal + 1. Use constants.MAX_FUTURE_SLOT_TOLERANCE (and consider @addWithOverflow if Slot can reach its max) to keep the tolerance consistent and avoid duplicating the value.

Suggested change
if (attestation_slot > self.fcStore.slot_clock.timeSlots.load(.monotonic) + 1) {
const current_slot = self.fcStore.slot_clock.timeSlots.load(.monotonic);
const max_future_slot_result = @addWithOverflow(current_slot, constants.MAX_FUTURE_SLOT_TOLERANCE);
const max_future_slot = if (max_future_slot_result[1] != 0)
std.math.maxInt(@TypeOf(current_slot))
else
max_future_slot_result[0];
if (attestation_slot > max_future_slot) {

Copilot uses AI. Check for mistakes.
Comment thread pkgs/node/src/chain.zig
Comment on lines 906 to 913
"block contains duplicate AttestationData entries for block root=0x{x}",
.{&freshFcBlock.blockRoot},
);
return BlockProcessingError.DuplicateAttestationData;
return BlockProcessingError.InvalidSignatureGroups;
}
}
if (att_data_set.count() > self.config.spec.max_attestations_data) {
if (att_data_set.count() > constants.MAX_ATTESTATIONS_DATA) {
self.logger.err(
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says there are “No hashsig/xmss/state-transition changes” and that the SSZ runner is self-contained, but this diff also changes production node behavior (pkgs/node/src/chain.zig, pkgs/node/src/forkchoice.zig) and key-manager error semantics (pkgs/key-manager/src/lib.zig). Please update the PR description/scope (or split into separate PRs) so reviewers know these runtime changes are intentional and reviewed.

Copilot uses AI. Check for mistakes.
…st commit

These changes (KeyManager error consolidation, MAX_ATTESTATIONS_DATA
constant move, chain.zig error renaming) are out of scope for this PR
and caused CI failures due to missing max_attestations_data struct field
in test initializers.
Copilot AI review requested due to automatic review settings April 11, 2026 14:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// Parse participants aggregation bits.
var aggregation_bits = try parseAggregationBitsValue(ctx.allocator, participants_value, fixture_path, case_name, step_index, "proof.participants");
errdefer aggregation_bits.deinit();
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregation_bits is initialized and used to derive indices / copy into proof, but it's never deinitialized on the success path (only errdefer is present). Even though the runner uses an arena per fixture, this still causes unnecessary per-step allocations to accumulate and makes the code easy to misuse if the allocator changes.

Add a defer aggregation_bits.deinit(); after parseAggregationBitsValue(...) succeeds (and keep the existing errdefer inside parseAggregationBitsValue).

Suggested change
errdefer aggregation_bits.deinit();
defer aggregation_bits.deinit();

Copilot uses AI. Check for mistakes.
…te_transition cases

Bump leanSpec submodule to 9b89651 (50 commits ahead) and regenerate
fixtures — 468 total, up from 123. All fork_choice and state_transition
tests now run without skips.

production (forkchoice):
  * forkchoice.onBlock now enforces leanSpec store.process_block rules:
    reject blocks with duplicate AttestationData or more than
    MAX_ATTESTATIONS_DATA (16) distinct entries. Previously this lived
    only in chain.zig and was skipped by the spectest path.
  * Remove the NotFinalizedDesendant rejection from onBlockUnlocked —
    leanSpec store.on_block accepts every block whose parent is known,
    and get_head naturally ignores forks off pre-finalization ancestors
    because it walks best descendants from latest_justified.
  * Promote isFinalizedDescendant to pub and move the DoS defense to
    chain.validateBlock (gossip attack surface) as a new
    BlockValidationError.NotFinalizedDescendant. Production continues
    to reject mal-parented blocks at the chain layer; forkchoice stays
    spec-pure so spectests can exercise the leanSpec semantics directly.

spectest runner — state_transition:
  * buildBlock now parses full aggregated attestations
    (aggregationBits.data + AttestationData) and hands them to
    apply_transition. The prior "attestations unsupported" early-return
    silently skipped 30 justification/finalization cases.

spectest runner — fork_choice:
  * Tick steps accept either `interval` or `time` plus `hasProposal`
    (new leanSpec schema).
  * blockAttestations check treats attestationSlot/targetSlot as
    optional (both were made optional in leanSpec test_types).
  * attestationChecks mirrors leanSpec
    extract_attestations_from_aggregated_payloads: iterate
    latest_new/known_aggregated_payloads and pick the largest-slot
    entry that includes the validator (previous code read the wrong
    AttestationTracker source).
  * Implement nine previously-UnsupportedFixture checks so 10 cases
    stop silently skipping:
      - safeTargetSlot / safeTargetRootLabel
      - latestFinalizedRootLabel
      - filledBlockRootLabel (tracks the block root just processed)
      - labelsInStore (asserts labelled roots still live in protoArray)
      - reorgDepth (walks new-head ancestry, then counts from old head)
      - attestationSignatureTargetSlots
      - latestNewAggregatedTargetSlots
      - latestKnownAggregatedTargetSlots
  * processAttestationStep now writes into attestation_signatures with
    ZERO_SIGBYTES placeholder. leanSpec's store.on_attestation inserts
    here on the gossip path; the runner was only calling onAttestation
    (validator tracker only), leaving attestation_signatures perpetually
    empty and the new *TargetSlots checks unobservable.
  * processBlockStep calls pruneStaleAttestationData when
    latest_finalized advances, matching leanSpec store.on_block's
    tail behaviour. Without this, attestation/payload maps kept stale
    entries that failed the *TargetSlots checks after finalization.

results:
  * 159/230 generated tests pass; remaining 71 failures are all SSZ
    basic-type fixtures (new leanSpec types: Uint16/Uint32/Uint64/Fp/
    Boolean/Bytes4/Bytes64/Bitvector/Bitlist/List/Vector/HashTreeOpening)
    — intentionally out of scope.
  * 0 fork_choice skips, 0 state_transition skips
    (down from 10 fork_choice + 30 state_transition silently skipped).
  * zig build test still exits 0.
Copilot AI review requested due to automatic review settings April 20, 2026 12:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

pkgs/node/src/forkchoice.zig:520

  • isFinalizedDescendant is now pub and is called from chain.validateBlock, but it reads protoArray/fcStore without taking self.mutex. This introduces a potential data race with concurrent forkchoice updates. Make it thread-safe (e.g., acquire mutex.lockShared() inside), or provide a separate isFinalizedDescendantUnlocked for internal callers and keep the public API locked.
    pub fn isFinalizedDescendant(self: *Self, blockRoot: types.Root) bool {
        const finalized_slot = self.fcStore.latest_finalized.slot;
        const finalized_root = self.fcStore.latest_finalized.root;

        var searched_idx_or_null = self.protoArray.indices.get(blockRoot);

        while (searched_idx_or_null) |searched_idx| {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.{ fixture_path, case_name, formatStep(step_index) },
);
return FixtureError.InvalidFixture;
};
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StepContext comment says applyChecks will read/clear last_pre_block_head_root/last_block_root, but verifyReorgDepth only reads them and never clears. Either clear these fields after consuming them (e.g., in verifyReorgDepth / filledBlockRootLabel) or update the comment to match the actual lifecycle to avoid stale state being reused across steps.

Suggested change
};
};
ctx.last_pre_block_head_root = null;

Copilot uses AI. Check for mistakes.
Comment on lines +922 to +925
const num_validators = ctx.fork_choice.anchorState.validators.constSlice().len;
if (validator_id >= num_validators) {
return error.UnknownValidator;
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validator_id is parsed as u64, but num_validators is a slice length (usize). The comparison validator_id >= num_validators will not compile due to mismatched integer types. Cast one side (e.g., cast validator_id to usize after bounds checking, or cast num_validators to u64) and keep the type consistent with types.ValidatorIndex downstream.

Copilot uses AI. Check for mistakes.
Comment on lines +1886 to +1887
if (validator >= bits.len()) continue;
const has_bit = bits.get(@intCast(validator)) catch false;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validator is a u64, but bits.len() returns usize, so if (validator >= bits.len()) will not compile. Convert validator to usize (with an explicit bounds check/cast) before comparing/indexing into the bitlist, and keep the same usize value for the subsequent bits.get(...) call.

Suggested change
if (validator >= bits.len()) continue;
const has_bit = bits.get(@intCast(validator)) catch false;
if (validator > std.math.maxInt(usize)) continue;
const validator_index: usize = @intCast(validator);
if (validator_index >= bits.len()) continue;
const has_bit = bits.get(validator_index) catch false;

Copilot uses AI. Check for mistakes.
Adds SSZ roundtrip coverage for the scalar, byte-array, bitvector,
integer-vector, bitlist and list types introduced alongside the
new test_basic_types / test_xmss_containers fixtures:

  Uint8, Uint16, Uint32, Uint64, Fp, Boolean
  Bytes4, Bytes32, Bytes52, Bytes64
  SampleBitvector8/64, AttestationSubnets, SyncCommitteeSubnets
  SampleUint16Vector3, SampleUint64Vector4
  SampleBitlist16, SampleBytes32List8, SampleUint32List16
  ByteListMiB, HashTreeOpening (empty), SignedAggregatedAttestation

Wires `@zeam/xmss` into the zeam_spectests module so ByteListMiB and
HashDigest types are reachable from the runner.

Known-skipped via explicit skip list (previously silently unsupported):

  - SampleUnionNone / SampleUnionTypes — zeam's SSZ library has no
    `union(enum)` path yet; leanSpec introduced SSZUnion in this
    release.
  - SampleUint16Vector3 / SampleUint64Vector4 / HashTreeLayer /
    HashTreeOpening (typical) — blockblaz/ssz.zig's deserialize for
    `.array` with element size > 1 conflates byte offset with element
    index, so only out[0], out[2], ... get written and the roundtrip
    diverges. Upstream bug; filed for separate fix.

Result: 230/230 spectests pass, 0 skips in the runner path (40
previously silent SkippedFixture cases now run).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants