Repository navigation
Add GENERIC signing request type - #30
Open
ZayanKhan-12 wants to merge 2 commits into
Open
ZayanKhan-12 wants to merge 2 commits into
ZayanKhan-12 wants to merge 2 commits into
Conversation
Every signature this API produces is BLS.Sign(sk, hash_tree_root(SigningData( merkle_root, domain))). A GENERIC request supplies those two values directly, so a signer can serve a signing request for an object or a fork it has no schema for, and the domain no longer has to be re-derived from a slot or epoch buried in a type-specific field. Because the caller supplies the domain, the signer reads the DomainType from its first four bytes to decide whether slashing protection applies. A request for DOMAIN_BEACON_PROPOSER carries a BeaconBlockHeader and one for DOMAIN_BEACON_ATTESTER carries AttestationData, each with a Merkle proof binding it to merkle_root, so the signer is not asked to trust the caller's summary of what it is signing. A signer running without slashing protection may omit them; one running with it MUST reject a slashable request that does. The proof is empty at every fork up to and including Gloas, since an attestation signs AttestationData directly and hash_tree_root(BeaconBlock) equals hash_tree_root(BeaconBlockHeader). It is specified so that a future fork nesting these containers more deeply does not need a new request type. The three examples are cryptographically equivalent to the existing ATTESTATION, BLOCK_V2 (GLOAS) and RANDAO_REVEAL examples: same merkle root, same domain, same signing root. scripts/verify_signing_examples.py recomputes all of it and runs in CI, so the vectors cannot drift. Also extracts the fork_info shape into a reusable ForkInfo schema. The dereferenced bundle is byte-identical before and after that refactor. Closes ethereum#26 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repository guidance for coding agents: layout, the three CI checks and how to run them locally, the pre-existing publicKeys lint warning that is the expected baseline rather than something to fix, the four places a new signing request type has to be registered, and the signing root arithmetic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZayanKhan-12
force-pushed
the
feat/generic-signing-route
branch
from
September 15, 2026 22:54
12dddd6 to
1530ca7
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
GENERICsigning request type toPOST /api/v1/eth2/sign/{identifier}, implementing the proposal in #26.Every signature this API produces is
BLS.Sign(sk, hash_tree_root(SigningData(merkle_root, domain))). AGENERICrequest supplies those two values directly:{ "type": "GENERIC", "merkle_root": "0x9da60a55ce18b9b1e5875795118ab978f2ca5c0b907bdbd1b8b237a73dbeece6", "domain": "0x01000000e7a75d5a9f3f331a669f33fe87d35b0735dce47180b272ed244e3053" }A signer can then serve a request for an object or a fork it has no schema for, and no longer has to dig a slot or epoch out of a type-specific subfield to pick a fork version. As @jshufro noted in #26, this also sidesteps the per-request domain-derivation debate in #25, since the caller supplies the domain.
Slashing protection
Per the discussion between @jshufro and @eth2353, the domain is sent rather than a bare
signing_rootprecisely so the signer can still police slashable signatures. The first four bytes ofdomainare theDomainType:0x00000000DOMAIN_BEACON_PROPOSERblock_header0x01000000DOMAIN_BEACON_ATTESTERattestationThe full container is sent, not just the slashable field, so the signer is not asked to trust the caller's summary of what it is signing — this follows @jshufro's "including just the slashable field is a lot of extra trust". Each carries a
proofbinding it tomerkle_root.Following @eth2353's suggestion, the containers are optional in the schema and conditionally required by the signer: a signer with slashing protection disabled may accept a request without them, and one with slashing protection enabled MUST reject a slashable request that omits them. That is documented as
400rather than left implicit.fork_infois optional and is explicitly not used to derive the domain; signers may use it to namespace slashing-protection records.On the proof being empty today
@jshufro guessed this would be "basically 0 witnesses for the attestation case anyway" — that is confirmed, and it holds for blocks too. An attestation signs
AttestationDatadirectly, andhash_tree_root(BeaconBlock) == hash_tree_root(BeaconBlockHeader)because a block'sbodyleaf ishash_tree_root(body), which is exactly the header'sbody_root. So at every fork up to and including Gloas,generalized_indexis1andmerkle_proofis[].The proof is specified anyway, as a
generalized_indexplus a branch, so that a future fork which nests these containers more deeply does not require a new signing request type. That is the forwards-compatibility @jshufro asked for, and making the index explicit means a signer does not have to infer path parity fromfork_info.Signers are not required to implement
GENERIC; one that does not responds501, which gives a validator client a clean signal to fall back to a typed request.The examples are real
The three examples are cryptographically equivalent to examples already in the spec — same merkle root, same domain, same signing root, and therefore the same signature:
GENERIC (DOMAIN_BEACON_ATTESTER)ATTESTATION0x548c9a01…5341f0GENERIC (DOMAIN_BEACON_PROPOSER)BLOCK_V2 (GLOAS)0xaa2e0c46…de8adcGENERIC (non-slashable domain)RANDAO_REVEAL0x3d047c51…75f91escripts/verify_signing_examples.pyrecomputes all of it from first principles — SSZ merkleization,compute_domain,compute_signing_root, and proof verification — and CI runs it on every push. It reproduces the already published signing roots of theATTESTATION,BLOCK_V2 (GLOAS)andRANDAO_REVEALexamples exactly, which is what gives confidence the new vectors are right rather than merely self-consistent.I'm happy to drop the script and the CI step if you'd prefer a spec-only diff; the examples stand on their own either way.
Validation
paths-snake-casewarning on/publicKeys, unchanged frommainswagger-cli validatepasses;bundle -rdereferences cleanlyoas3-valid-media-exampledoes validate the new examples, and that each matches exactly one member of theoneOfgit diff --checkcleanNotes for reviewers
info.versionis bumpedv1.4.0→v1.5.0, matching the convention in Adding Electra changes #15, Adding Fulu changes #21 and Adding Gloas changes #28 of bumping in the feature PR.fork_infoshape is extracted into a reusableForkInfoschema soGenericSigningcan reference it. The fully dereferenced bundle is byte-identical before and after that refactor, so it is provably a no-op for consumers.bundle.yamlis added to.gitignore, since the README now tells contributors to generate it locally.GENERICrequest in Add generic, batch, and distributed signing profiles #29, which bundles it with batch and distributed signing. This PR is deliberately scoped to Proposal: Add generic signing route #26 alone; happy to reconcile in whichever direction you prefer.CLAUDE.mdwith repository guidance for coding agents, kept separate from the spec commit. It records the things that cost time to work out here and aren't discoverable from the files alone: that a clean Spectral run is0 errors, 1 warningrather than zero problems, and that the/publicKeyswarning is deployed wire format to be left alone; thatoas3-valid-media-exampleis what catches a malformed new type, via theoneOf; that a new signing type has to be registered in four places or it silently becomes unreachable; thatuint64is a JSON string; thatinfo.versionis bumped in the feature PR; and the signing root arithmetic, with thehash_tree_root(BeaconBlock) == hash_tree_root(BeaconBlockHeader)identity this PR leans on. It should save the next contributor — agent or human — the same rediscovery.Closes #26
🤖 Generated with Claude Code