Skip to content

chore: merge master into te/serialized_list - #557

Merged
wemeetagain merged 2 commits into
ChainSafe:te/serialized_listfrom
lodekeeper:chore/serialized-list-merge-master
Oct 16, 2026
Merged

wemeetagain merged 2 commits into
ChainSafe:te/serialized_listfrom
lodekeeper:chore/serialized-list-merge-master

Conversation

@lodekeeper

Copy link
Copy Markdown
Contributor

Motivation

#556 is merged and conflicts with this branch in readVariableOffsetsArrayComposite().

Description

Merges master into te/serialized_list:

  • readVariableOffsetsArrayComposite() takes master's version and stays exported. It always returns n + 1 offsets now, so withEndOffset is gone. Offsets are a Float64Array when the list is 2^32 bytes or larger, so the trailing entry doesn't wrap
  • SerializedListBaseType.value_deserializeFromBytes() drops the true argument
  • SerializedList.offsets is Uint32Array | Float64Array to match. Its doc comment no longer says that list types keep only n offsets

If you'd rather keep offsets a plain Uint32Array, the alternative is to reject serialized lists of 2^32 bytes or more in SerializedListBaseType. That would differ from ListCompositeType, which accepts them.

Tests

  • pnpm test:unit: 2405 passed
  • pnpm test:spec-generic: 5065 passed
  • pnpm test:spec-static-minimal: 13284 passed, pnpm test:spec-static-mainnet: 540 passed
  • pnpm lint clean

🤖 Generated with AI assistance

lodekeeper and others added 2 commits October 16, 2026 12:08
…lements (ChainSafe#556)

**Motivation**

`readOffsetsArrayComposite()` returned one offset per element, so both
deserialize loops in `arrayComposite.ts` had to special case the last
element (`i === length - 1 ? end : start + offsets[i + 1]`). The
comments next to them ("Capture length before pushing end offset",
"offests include the last element end") describe an n + 1 layout that
the code no longer had.

The fixed size branch also built an offsets array of `i * fixedSize`
just so both element kinds could share one loop, while serialization
already writes fixed size elements at `offset + i * fixedSize` directly.

This also lets ChainSafe#555 drop the `withEndOffset` flag it adds to
`readVariableOffsetsArrayComposite()`, since that is the layout
`SerializedList` needs.

**Description**

- Fixed size elements are read at `start + i * fixedSize` in
`value_deserializeFromBytesArrayComposite()`,
`tree_deserializeFromBytesArrayComposite()` and
`ProgressiveListCompositeType.tree_deserializeFromBytes()`, with no
offsets array. The length checks (`size % fixedSize`,
`assertValidArrayLength()`) and their errors are unchanged
- `readVariableOffsetsArrayComposite()` now returns n + 1 offsets, with
a trailing entry equal to `end - start`. An empty array returns `[0]`
- `assertValidArrayLength()` still gets the element count
(`offsets.length - 1`)
- The variable size loops use `endEl = start + offsets[i + 1]` with no
branch

The offset readers are module private and the deserialize functions
above are their only callers. ProgressiveList keeps its own variable
size offset reader. Validation and error order are unchanged. The
trailing write happens after all offset checks.

Fixed size elements use plain number math, so they don't wrap at any
size. Variable size offsets stay in a `Uint32Array`, except when the
array is 2^32 bytes or larger. Then they go in a `Float64Array` so the
trailing entry doesn't wrap, and the last element still ends at `end` as
before. The SSZ spec caps the serialized size below 2^32, but the
library accepted these inputs before this change.

**Tests**

- `pnpm test:unit`: 1814 passed
- `pnpm test:spec-generic`: 5065 passed
- `pnpm test:spec-static-minimal`: 13284 passed, `pnpm
test:spec-static-mainnet`: 540 passed
- `pnpm lint` clean

**Benchmarks**

Local, node 24 (V8 13.6), at 08422c6. Medians of 6 interleaved
master/branch rounds per case, with the side that runs first alternating
each round. `deserialize()` = value, `deserializeToView()` = tree. Cases
up to 128 elements come from the small file (3 to 6 s per case), the
rest from the large file (1.5 to 4 s per case).

| case | value master | value branch | tree master | tree branch |
| --- | --- | --- | --- | --- |
| List[ByteList] 1 x 100B | 0.91 us | 0.90 us | 2.83 us | 2.73 us |
| List[ByteList] 4 x 100B | 3.08 us | 2.99 us | 7.07 us | 6.84 us |
| List[ByteList] 1000 x 1000B | 1.10 ms | 1.06 ms | 3.71 ms | 3.79 ms |
| List[ByteList] 10000 x 100B | 9.14 ms | 8.51 ms | 24.66 ms | 20.82 ms
|
| List[ByteList] 100000 x 0B | 8.46 ms | 8.99 ms | 34.71 ms | 27.17 ms |
| ProgressiveList[ProgressiveByteList] 10000 x 100B | 8.98 ms | 8.72 ms
| - | - |
| List[Container(fixed 40B)] 1 | 0.52 us | 0.48 us | 2.69 us | 2.54 us |
| List[Container(fixed 40B)] 16 | 2.20 us | 2.11 us | 6.73 us | 6.23 us
|
| List[Container(fixed 40B)] 128 | 14.15 us | 13.01 us | 37.04 us |
35.58 us |
| List[Container(fixed 40B)] 100000 | 23.81 ms | 26.08 ms | 50.51 ms |
47.99 ms |
| ProgressiveList[Container(fixed 40B)] 16 | 2.50 us | 2.19 us | 5.85 us
| 5.19 us |
| ProgressiveList[Container(fixed 40B)] 100000 | 23.39 ms | 22.52 ms |
51.03 ms | 45.75 ms |
| Vector[Bytes32, 4] | 0.63 us | 0.57 us | 0.98 us | 0.91 us |
| Vector[Bytes32, 8192] | 453.48 us | 413.21 us | 1.15 ms | 960.27 us |

The fixed size cases are a few percent faster than master because
nothing is allocated for offsets anymore. This also removes the n = 16
regression the first commit had (`List[Container(fixed 40B)] 16` value:
2.16 us on master vs 2.89 us, from `Uint32Array(17)` crossing V8's 64
byte on heap limit). It's now 2.20 us on master vs 2.11 us. Variable
size cases are unchanged within noise. On this machine the large cases
vary by about 20% between rounds of the same code, so differences below
that only show up in the small cases.

<details><summary>Bench file (not committed). The small cases used the
same file with counts 1/4 (ByteList), 1/16/128 (Container) and
Vector[Bytes32, 4], without the ProgressiveList cases</summary>

```ts
import {bench, describe} from "@chainsafe/benchmark";
import {
  ByteListType,
  ByteVectorType,
  CompositeType,
  ContainerType,
  ListCompositeType,
  ProgressiveByteListType,
  ProgressiveListCompositeType,
  UintNumberType,
  VectorCompositeType,
} from "../../../src/index.ts";

const bytes32 = new ByteVectorType(32);
const uint64 = new UintNumberType(8);
const byteList = new ByteListType(2 ** 30);
const fixedContainer = new ContainerType({root: bytes32, value: uint64}, {typeName: "Container(fixed 40B)"});

function filledBytes(len: number, seed: number): Uint8Array {
  const b = new Uint8Array(len);
  for (let i = 0; i < len; i++) b[i] = (seed + i) & 0xff;
  return b;
}

function byteLists(count: number, size: number): Uint8Array[] {
  return Array.from({length: count}, (_, i) => filledBytes(size, i));
}

describe("arrayComposite deserialize", () => {
  const cases: {id: string; type: CompositeType<unknown, unknown, unknown>; value: unknown; tree: boolean}[] = [];

  // variable size elements, readVariableOffsetsArrayComposite()
  for (const [count, size] of [
    [4, 100],
    [1_000, 1_000],
    [10_000, 100],
    [100_000, 0],
  ] as const) {
    cases.push({
      id: `List[ByteList] ${count} x ${size}B`,
      type: new ListCompositeType(byteList, 2 ** 20),
      value: byteLists(count, size),
      tree: true,
    });
  }
  cases.push({
    id: "ProgressiveList[ProgressiveByteList] 10000 x 100B",
    type: new ProgressiveListCompositeType(new ProgressiveByteListType()),
    value: byteLists(10_000, 100),
    tree: false,
  });

  // fixed size elements, start + i * fixedSize
  for (const count of [16, 100_000]) {
    cases.push({
      id: `List[Container(fixed 40B)] ${count}`,
      type: new ListCompositeType(fixedContainer, 2 ** 40),
      value: Array.from({length: count}, (_, i) => ({root: filledBytes(32, i), value: i})),
      tree: true,
    });
  }
  for (const count of [16, 100_000]) {
    cases.push({
      id: `ProgressiveList[Container(fixed 40B)] ${count}`,
      type: new ProgressiveListCompositeType(fixedContainer),
      value: Array.from({length: count}, (_, i) => ({root: filledBytes(32, i), value: i})),
      tree: true,
    });
  }
  cases.push({
    id: "Vector[Bytes32, 8192]",
    type: new VectorCompositeType(bytes32, 8192),
    value: Array.from({length: 8192}, (_, i) => filledBytes(32, i)),
    tree: true,
  });

  for (const {id, type, value, tree} of cases) {
    const bytes = type.serialize(value);

    bench({
      id: `${id} - value`,
      fn: () => {
        type.deserialize(bytes);
      },
    });

    if (tree) {
      bench({
        id: `${id} - tree`,
        fn: () => {
          type.deserializeToView(bytes);
        },
      });
    }
  }
});
```

</details>

🤖 Generated with AI assistance

---------

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Resolve the conflict with ChainSafe#556 in readVariableOffsetsArrayComposite(): keep
master's version, which always returns n + 1 offsets (Float64Array at 2^32
bytes and above), and keep it exported. Drop the withEndOffset argument in
SerializedListBaseType and widen SerializedList.offsets to
Uint32Array | Float64Array.

🤖 Generated with AI assistance
@lodekeeper
lodekeeper requested a review from a team as a code owner October 16, 2026 16:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T16:28:04.864800Z dc94d55 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@wemeetagain
wemeetagain merged commit ca973a6 into ChainSafe:te/serialized_list Oct 16, 2026
11 checks passed
wemeetagain added a commit that referenced this pull request Oct 16, 2026
PR #557 brought master's changes into this branch with a squash commit, so the PR comparison still includes the changes from #556. Record the actual merge with master to correct the comparison base.

The resulting tree is identical to ca973a6; preserve the exported readVariableOffsetsArrayComposite helper required by SerializedListType.
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