Skip to content

cl/cltypes, cl/phase1/execution_client: dedupe signed-container SSZ and engine API call boilerplate#22176

Draft
yperbasis wants to merge 3 commits into
mainfrom
yperbasis/cl-signed-ssz
Draft

cl/cltypes, cl/phase1/execution_client: dedupe signed-container SSZ and engine API call boilerplate#22176
yperbasis wants to merge 3 commits into
mainfrom
yperbasis/cl-signed-ssz

Conversation

@yperbasis

Copy link
Copy Markdown
Member

Two consolidations in Caplin, three commits:

  1. Engine API client: 23 methods in engine_api_rpc_client.go were identical CallContext wrappers; they collapse onto generic call[T] / callValue[T] one-liners (the second needed for slice-returning methods). getAssembledBlockV4/V5/V6 fold into one version-parameterized function taking the GetPayloadVn method value; V3 keeps its extra nil-payload logic. Interface unchanged; transport-only, no consensus logic. Net −91 lines.

  2. Signed containers: eight {Message, Signature} types (SignedVoluntaryExit, SignedBeaconBlockHeader, SignedBLSToExecutionChange, SignedContributionAndProof, SignedAggregateAndProof, SignedProposerPreferences, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope) repeated the same SSZ quartet. Shared free helpers (encodeSigned/decodeSigned/sizeSigned/hashSigned) now carry it; sizeSigned states the actual rule once (96 + msgSize + 4 iff the message is dynamic), replacing the ad-hoc per-type constants (96+x, x+96, 4+x+96) — all verified equivalent. Exported structs, JSON tags, and per-type decode inits are untouched. SignedBeaconBlock/SignedBlindedBeaconBlock deliberately skipped (per-fork construction; different nil-guards).

Safety net, committed green before the refactor: signed_ssz_golden_test.go pins, per type, the full EncodeSSZ hex, EncodingSizeSSZ, HashSSZ root, and a decode round-trip on deterministically-filled instances (+299 lines). A mutation check (sizeSigned+1) flips all eight red, so the harness genuinely guards the helpers.

Pre-existing bug found while pinning, preserved as-is and worth a follow-up issue: ExecutionRequests.EncodingSizeSSZ (cl/cltypes/execution_requests.go:43) omits its three 4-byte field offsets, so SignedExecutionPayloadEnvelope.EncodingSizeSSZ under-reports by 12 (981 vs 993 encoded). Same class as the BlindedBeaconBody size bug fixed in #22165.

Verified: full go test ./cl/... green, goldens byte-identical pre/post, scoped golangci-lint clean (×2), make erigon. (cl/spectest vectors not present locally; CI covers.)

Part of a dedup series; siblings #22165#22175.

yperbasis added 3 commits July 2, 2026 21:57
Pure refactor, no behavior change; existing tests and the build are the
safety net.

All 23 EngineAPIRPCClient methods shared the same shape: allocate result,
CallContext, return. Replace with generic call/callValue helpers so each
method is a one-liner. getAssembledBlockV4/V5/V6 differed only in the
engine method and error string; fold them into getAssembledBlockFromEngine
taking the method value and name (error strings unchanged).
Pin the exact EncodeSSZ bytes, HashSSZ root, and EncodingSizeSSZ of every
{Message, Signature} container (SignedVoluntaryExit, SignedBeaconBlockHeader,
SignedBLSToExecutionChange, SignedContributionAndProof,
SignedAggregateAndProof, SignedProposerPreferences,
SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope) for
deterministically-filled instances, plus a decode round-trip through a fresh
instance. These types cross the network; the goldens guard the upcoming
SSZ-method unification byte-for-byte.

Note: SignedExecutionPayloadEnvelope's pinned size (981) is 12 bytes below
its encoded length (993) because ExecutionRequests.EncodingSizeSSZ does not
count its three field offsets - a pre-existing quirk pinned as-is, not fixed
here.
Route the SSZ quartet (encode/decode/size/hash) of the eight
{Message, Signature} container types through encodeSigned/decodeSigned/
sizeSigned/hashSigned instead of each type restating the schema.
sizeSigned derives the 96-byte signature plus, for dynamic messages, the
4-byte offset from Message.Static(), replacing the per-type constants
(96+x, x+96, 100+x, 4+x+96) that all encoded the same rule.

Behavior is byte-identical - pinned by the golden SSZ tests added in the
previous commit (encoding, hash tree root, and encoding size per type).
Per-type decode initialization (always-new vs nil-check, nested
Contribution init, beaconCfg-aware envelope constructor) and the
SignedProposerPreferences nil-Message size guard are kept verbatim.
SignedBeaconBlock/SignedBlindedBeaconBlock are intentionally not unified:
their Block requires per-fork construction and their decode does not
initialize the message.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces duplication in Caplin by centralizing (1) Engine API JSON-RPC boilerplate and (2) SSZ encode/decode/size/hash logic shared across multiple {Message, Signature} “signed container” types, while adding golden tests to pin byte-identical SSZ behavior during refactors.

Changes:

  • Collapses many near-identical Engine API CallContext wrappers into generic call[T] / callValue[T] helpers, and unifies getAssembledBlockV4/V5/V6 into a single parameterized helper.
  • Deduplicates SSZ encode/decode/size/hash implementations for multiple signed-container CL types via shared helpers (encodeSigned, decodeSigned, sizeSigned, hashSigned).
  • Adds SSZ golden tests to lock down exact encodings, hash roots, and decode/encode round-trips for the signed-container types.

Reviewed changes

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

Show a summary per file
File Description
cl/phase1/execution_client/execution_client_engine.go Replaces per-version assembled-block getters (V4–V6) with a single helper that takes the Engine API method value.
cl/phase1/execution_client/engine_api_rpc_client.go Introduces generic RPC wrappers to remove repetitive CallContext boilerplate across Engine API methods.
cl/cltypes/signed_ssz.go Adds shared SSZ helper functions for signed-container {Message, Signature} types.
cl/cltypes/validator.go Switches SignedVoluntaryExit SSZ methods to use shared signed-container helpers.
cl/cltypes/epbs_proposer.go Switches SignedProposerPreferences SSZ methods/size calculation to shared helpers (preserving nil guard).
cl/cltypes/epbs_payload.go Switches SignedExecutionPayloadBid / SignedExecutionPayloadEnvelope SSZ methods/size calculation to shared helpers.
cl/cltypes/contribution.go Switches SignedContributionAndProof SSZ methods/size calculation to shared helpers.
cl/cltypes/bls_to_execution_change.go Switches SignedBLSToExecutionChange SSZ methods/size calculation to shared helpers.
cl/cltypes/beacon_header.go Switches SignedBeaconBlockHeader SSZ methods/size calculation to shared helpers.
cl/cltypes/aggregate.go Switches SignedAggregateAndProof SSZ methods/size calculation to shared helpers.
cl/cltypes/signed_ssz_golden_test.go Adds golden tests pinning SSZ encoding/hash/size and round-trip decode for signed-container types.

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

Comment on lines +42 to +52
func requireGoldenSSZ(t *testing.T, obj signedContainerSSZ, wantEncHex, wantRootHex string, wantSize int) []byte {
t.Helper()
enc, err := obj.EncodeSSZ(nil)
require.NoError(t, err)
root, err := obj.HashSSZ()
require.NoError(t, err)
require.Equal(t, wantEncHex, hex.EncodeToString(enc))
require.Equal(t, wantRootHex, hex.EncodeToString(root[:]))
require.Equal(t, wantSize, obj.EncodingSizeSSZ())
return enc
}
@yperbasis

Copy link
Copy Markdown
Member Author

Filed the follow-up for the pre-existing size bug found while pinning goldens here: #22181 (ExecutionRequests.EncodingSizeSSZ omits its field offsets). The fix will need to update the one pinned EncodingSizeSSZ golden for SignedExecutionPayloadEnvelope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants