Skip to content

chore: block proposer fixtures become bls valid#627

Closed
shane-moore wants to merge 3 commits into
ssvlabs:spec-test-cifrom
shane-moore:chore/proposer-fixtures-bls-valid
Closed

chore: block proposer fixtures become bls valid#627
shane-moore wants to merge 3 commits into
ssvlabs:spec-test-cifrom
shane-moore:chore/proposer-fixtures-bls-valid

Conversation

@shane-moore
Copy link
Copy Markdown
Collaborator

Summary

The capellaBlock and denebBlockContents fixtures in types/testingutils/beacon_node_consts.go were copied from go-eth2-client's codec tests, where BLS-typed fields are sequential-byte placeholders (0x000102…, etc.). Byte lengths are correct but the bytes are not valid compressed BLS12-381 points, so strict decoders (Lighthouse's bls crate via blst) reject them with BLST_BAD_ENCODING. This blocks cross-client consumers like Anchor's spec_tests from running real BLS validation on these fixtures.

Replace every BLS-typed value in those two literals with a canonical valid pubkey + signature derived from Testing4SharesSet().ValidatorSK. Non-BLS fields (KZG commitments/proofs, transactions, blobs, roots, etc.) are preserved byte-for-byte from upstream. Closes #622.

Changes

  • Substitute the BLS-typed values inside capellaBlock and denebBlockContents with a canonical valid pubkey + signature; everything else byte-identical to upstream.
  • Add TestProposerFixturesBLSDecodable (capella + deneb subtests) asserting every BLS field strict-decodes via herumi. Electra/Fulu use real Pectra-devnet-6 bytes and already strict-decode; not re-tested.
  • Introduce internal/proposerbls/ with a shared Visitor walker over Capella/Deneb beacon block bodies, as single source of truth for "which fields are BLS-typed", consumed by both the test and the regen script.
  • Add a //go:build ignore regen script that locates each named literal by prefix match, JSON-decodes into the typed go-eth2-client struct, walks BLS fields via the shared visitor, and substitutes values in-place inside the literal range. Length-preserving and no JSON re-marshal
  • Restructure make generate-jsons to be read-only on source: runs the BLS-decodability test as a precondition, then regenerates JSONs. Add make regen-proposer-fixtures as the explicit fix tool. Source mutation is developer-driven, never an implicit side-effect of JSON regen.

When to regenerate (after a go-eth2-client bump)

make regen-proposer-fixtures # rewrites beacon_node_consts.go in place
review the diff, commit the source fix

make generate-jsons # precondition test passes; regenerates JSONs

Limitations

  • The regen script discovers BLS positions by walking the typed go-eth2-client structs. If a future fork adds a new BLS-typed field, the walker in internal/proposerbls/walk.go needs updating. Both the script and the regression test consume it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR replaces sequential-byte BLS placeholder values in the Capella and Deneb block fixtures with canonical valid compressed BLS12-381 points, fixing BLST_BAD_ENCODING failures in strict decoders such as Lighthouse's blst crate. The inline Go byte literals are also promoted to //go:embed-loaded JSON files, improving readability and making the fixture data easier to inspect and update.

  • beacon_node_consts.go: Drops two large inline raw-string literals; replaces them with //go:embed testdata/capella_block.json and //go:embed testdata/deneb_block_contents.json plus the required blank embed import.
  • testdata/capella_block.json / testdata/deneb_block_contents.json: All BLS signature fields carry a valid 96-byte G2 compressed point (0x90faded9…) and BLS pubkey fields carry a valid 48-byte G1 compressed point (0x8e80066…); all non-BLS fields are unchanged from upstream.

Confidence Score: 5/5

Safe to merge — the fixture data change is additive and self-contained, and the //go:embed refactor is mechanical.

The changed code replaces two large inline string literals with embedded JSON files that now carry valid BLS12-381 compressed points. The substituted values have the correct byte lengths (48-byte G1 for pubkeys, 96-byte G2 for signatures) and correct compression-bit prefixes. All non-BLS fields are preserved from upstream. The embed import and directives are syntactically correct. No logic paths, exports, or interfaces are altered.

No files require special attention; the only gap is the BLS-decodability regression test described in the PR summary but absent from the diff.

Important Files Changed

Filename Overview
types/testingutils/beacon_node_consts.go Replaces inline Go byte literals for capellaBlock and denebBlockContents with //go:embed directives loading from testdata JSON files; adds the required blank embed import. Core logic is correct; the BLS-decodability regression test described in the PR is absent.
types/testingutils/testdata/capella_block.json New pretty-printed JSON fixture; all BLS signature fields replaced with a valid 96-byte G2 compressed point (0x90faded9…) and pubkey fields replaced with a valid 48-byte G1 compressed point (0x8e80066…). Non-BLS fields preserved byte-for-byte from upstream.
types/testingutils/testdata/deneb_block_contents.json New pretty-printed JSON fixture for Deneb block contents; BLS fields replaced with valid compressed points identical to those in capella_block.json. KZG commitments, proofs, blobs, and transactions are preserved from upstream.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[beacon_node_consts.go] -->|go:embed| B[testdata/capella_block.json]
    A -->|go:embed| C[testdata/deneb_block_contents.json]
    B --> D[capellaBlock]
    C --> E[denebBlockContents]
    D --> F[TestingBeaconBlockCapella]
    E --> G[TestingBlockContentsDeneb]
    F --> H[TestingBeaconBlockV]
    G --> H
    B -. BLS fields replaced .-> I[Valid G1 pubkey + G2 signature]
    C -. BLS fields replaced .-> I
Loading

Reviews (2): Last reviewed commit: "refactor: cappela and deneb block data j..." | Re-trigger Greptile

Comment on lines +88 to +98
rewritten := bytes.Clone(literal)
for v := range pubkeys {
rewritten = bytes.ReplaceAll(rewritten, []byte(`"`+v+`"`), []byte(`"`+validPubkey+`"`))
}
for v := range sigs {
rewritten = bytes.ReplaceAll(rewritten, []byte(`"`+v+`"`), []byte(`"`+validSig+`"`))
}
if len(rewritten) != contentEnd-contentStart {
log.Fatalf("%s: substitution changed literal length", name)
}
copy(out[contentStart:contentEnd], rewritten)
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.

P2 ReplaceAll can silently corrupt non-BLS fields that share a hex value

discoverBLSValues collects the hex strings found in BLS-typed positions, then bytes.ReplaceAll swaps every quoted occurrence of those strings anywhere inside the literal. A Deneb block contains KZG commitments (48 bytes, same width as a BLS pubkey) and KZG proofs (96 bytes, same width as a BLS signature). If any KZG field happens to carry the same sequential byte pattern as a discovered BLS field, it will be silently replaced with a valid BLS point—corrupting a field the PR explicitly says should be "preserved byte-for-byte from upstream". The current fixture data is safe (verified: the 4 unique 48-byte values in denebBlockContents are all distinct), but this invariant is fragile: a go-eth2-client test-data bump that changes byte offsets could silently introduce the collision without the length-check at line 95 catching it. Consider replacing the values at the byte offsets discovered during the walk (e.g., by encoding their positions while unmarshalling) rather than doing a content-addressed search-and-replace.

Copy link
Copy Markdown
Collaborator Author

@shane-moore shane-moore May 6, 2026

Choose a reason for hiding this comment

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

Good catch, fixed in 3e2f851. discoverBLSValues now returns per-value counts, and rewriteLiteral asserts that each BLS hex appears in the literal exactly as many times as the walker found at BLS-typed positions before substituting. If a future upstream bump puts a KZG commitment/proof byte-identical to a discovered BLS placeholder, the count check fails and the script aborts before any replacement happens.

Didn't go all the way to offset-tracked replacement since the count check covers the only practical corruption mode (literal count strictly greater than walker count, i.e., a non-BLS field shares the value) without re-marshaling JSON.

@shane-moore
Copy link
Copy Markdown
Collaborator Author

shane-moore commented May 6, 2026

On the Comments Outside Diff item about Visitor nil callback fields: leaving as-is. The package is internal/proposerbls/, both current consumers always supply both callbacks, and there is no partial-visitor use case in scope. Adding if v.Pubkey != nil guards would be defensive code for a hypothetical future caller, and silently no-op-ing on a nil callback would mask programmer error rather than surface it. If a partial visitor ever becomes a real need, the right move is to make the field's optionality explicit at that point, not to pre-emptively soften the contract now.

@diegomrsantos
Copy link
Copy Markdown

Thanks for putting this together. I think the core fix is right: capellaBlock and denebBlockContents should not contain codec fixture BLS placeholders if these vectors are meant to be consumed by strict clients. Clients using stricter BLS decoding should be able to deserialize these fields without special handling.

After looking through the PR, I think we can make the shape simpler and easier to review. The current walker and regen script make sense if we expect to refresh these upstream fixtures often, but for #622 the main issue is the checked-in fixture data. I would prefer fixing the source fixtures directly and keeping the change focused.

I also think this PR is a good chance to move the giant Go raw strings into testdata/*.json. That would make the actual fixture changes reviewable, avoid GitHub's large-file rendering problem, and keep future edits in normal JSON instead of editing huge Go string literals.

With that shape, I do not think we need a separate fixture validation test or a regeneration framework in this PR. The Go tests do not currently catch invalid compressed BLS points because go-eth2-client decodes these fields as fixed-size byte arrays, but the practical fix here is to store valid fixture bytes in a format that is easy to inspect.

So my preferred version would be:

  • move the Capella and Deneb fixture JSON into testdata
  • update the BLS field values there
  • load the files from the testing helper
  • add a short comment explaining why these BLS fields should remain valid for strict decoders

@shane-moore
Copy link
Copy Markdown
Collaborator Author

shane-moore commented May 7, 2026

Good idea! Pushed the reshape in ab5d985

Mapped to your bullets:

  • Capella and Deneb fixtures moved to types/testingutils/testdata/{capella_block,deneb_block_contents}.json, multi-line jq-formatted. Actually reviewable in the GitHub diff now.
  • BLS values valid, preserved byte-for-byte from the previous commit.
  • capellaBlock and denebBlockContents declared via //go:embed; existing TestingBeaconBlockCapella / TestingBlockContentsDeneb helpers consume the same []byte vars unchanged.
  • Short comment above the embeds calling out the strict-decoder requirement.

Removed: walker, regen script, generate.go, TestProposerFixturesBLSDecodable, the regen-proposer-fixtures Makefile target, and the precondition gate on generate-jsons.

@shane-moore shane-moore marked this pull request as ready for review May 7, 2026 23:29
@diegomrsantos
Copy link
Copy Markdown

The Lint failure on this PR is coming from the golangci-lint installer URL, not from this diff.

I opened #629 to switch the repo from the retired master installer script to the supported https://golangci-lint.run/install.sh URL recommended upstream. I verified locally that the supported installer installs the current latest linter and that lint passes.

The Sync failure is separate and still needs to be handled independently.

@shane-moore
Copy link
Copy Markdown
Collaborator Author

Superseded by #630 — same branch pushed directly to ssvlabs/ssv-spec so CI runs without fork restrictions.

@shane-moore shane-moore closed this May 8, 2026
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