Skip to content

Add GENERIC signing request type - #30

Open
ZayanKhan-12 wants to merge 2 commits into
ethereum:mainfrom
ZayanKhan-12:feat/generic-signing-route
Open

ZayanKhan-12 wants to merge 2 commits into
ethereum:mainfrom
ZayanKhan-12:feat/generic-signing-route

Conversation

@ZayanKhan-12

@ZayanKhan-12 ZayanKhan-12 commented Sep 15, 2026 •

Copy link
Copy Markdown

Summary

Adds a GENERIC signing request type to POST /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))). A GENERIC request 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_root precisely so the signer can still police slashable signatures. The first four bytes of domain are the DomainType:

Domain type Constant Required container
0x00000000 DOMAIN_BEACON_PROPOSER block_header
0x01000000 DOMAIN_BEACON_ATTESTER attestation
anything else — none

The 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 proof binding it to merkle_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 400 rather than left implicit.

fork_info is 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 AttestationData directly, and hash_tree_root(BeaconBlock) == hash_tree_root(BeaconBlockHeader) because a block's body leaf is hash_tree_root(body), which is exactly the header's body_root. So at every fork up to and including Gloas, generalized_index is 1 and merkle_proof is [].

The proof is specified anyway, as a generalized_index plus 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 from fork_info.

Signers are not required to implement GENERIC; one that does not responds 501, 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:

New example Equivalent to Shared signing root
GENERIC (DOMAIN_BEACON_ATTESTER) ATTESTATION 0x548c9a01…5341f0
GENERIC (DOMAIN_BEACON_PROPOSER) BLOCK_V2 (GLOAS) 0xaa2e0c46…de8adc
GENERIC (non-slashable domain) RANDAO_REVEAL 0x3d047c51…75f91e

scripts/verify_signing_examples.py recomputes 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 the ATTESTATION, BLOCK_V2 (GLOAS) and RANDAO_REVEAL examples 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

  • Spectral: 0 errors, 1 warning — the pre-existing paths-snake-case warning on /publicKeys, unchanged from main
  • swagger-cli validate passes; bundle -r dereferences cleanly
  • Example vectors: 17/17 checks pass
  • Mutation-tested the verification script against 7 deliberate corruptions (tampered roots, a dropped slashing-protection container, a slashable domain with no container, a proof claiming a non-trivial index with an empty branch, a removed example, a regressed typed vector) — all 7 are caught
  • Confirmed via a deliberately invalid example that oas3-valid-media-example does validate the new examples, and that each matches exactly one member of the oneOf
  • git diff --check clean

Notes for reviewers

  • info.version is bumped v1.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.
  • The fork_info shape is extracted into a reusable ForkInfo schema so GenericSigning can reference it. The fully dereferenced bundle is byte-identical before and after that refactor, so it is provably a no-op for consumers.
  • bundle.yaml is added to .gitignore, since the README now tells contributors to generate it locally.
  • There is some overlap with the GENERIC request 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.
  • The last commit adds a CLAUDE.md with 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 is 0 errors, 1 warning rather than zero problems, and that the /publicKeys warning is deployed wire format to be left alone; that oas3-valid-media-example is what catches a malformed new type, via the oneOf; that a new signing type has to be registered in four places or it silently becomes unreachable; that uint64 is a JSON string; that info.version is bumped in the feature PR; and the signing root arithmetic, with the hash_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

zk-khan and others added 2 commits September 15, 2026 18:53
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
ZayanKhan-12 force-pushed the feat/generic-signing-route branch from 12dddd6 to 1530ca7 Compare September 15, 2026 22:54

This branch has not been deployed

No deployments
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.

Proposal: Add generic signing route

2 participants