Skip to content

build(genesis)!: generate keys for leanVM's internalized XMSS - #182

Open
MegaRedHand wants to merge 2 commits into
blockblaz:mainfrom
lambdaclass:build/leanvm-xmss-keygen
Open

build(genesis)!: generate keys for leanVM's internalized XMSS#182
MegaRedHand wants to merge 2 commits into
blockblaz:mainfrom
lambdaclass:build/leanvm-xmss-keygen

Conversation

@MegaRedHand

Copy link
Copy Markdown
Contributor

What

Moves genesis key generation to the XMSS implementation leanVM internalized when the standalone leanSig crate was retired, and moves the ethlambda client image in step.

Public keys are now SSZ and 32 bytes (they were 52 under leanSig) and secret keys are postcard-encoded. File names and the manifest schema are unchanged, so parse-vc.sh, the ansible roles and the client --hash-sig-keys-dir flags need no adjustment.

Changes

Before After
Keygen image blockblaz/hash-sig-cli:latest ghcr.io/lambdaclass/hash-sig-cli:0.5.0
ethlambda image ghcr.io/lambdaclass/ethlambda:devnet4 ghcr.io/lambdaclass/ethlambda:devnet5-leanvm-main
Manifest check pubkeys are hex plus: reports key_scheme and pubkey size, warns on a pre-0.5.0 key set, fails on a pubkey_bytes mismatch
Keygen log hardcoded SIGTopLevelTargetSumLifetime32Dim64Base8 scheme comes from the manifest

The keygen image is a lambdaclass GHCR build because no published blockblaz/hash-sig-cli tag generates this format: upstream's auto-release.yml fires only on a merged release PR, and hash-sig-cli#39 is still open. It is multi-arch (amd64 + arm64) and built from that branch. Once upstream tags a release this is a one-line switch back to Docker Hub.

The two image pins have to move together: a leanSig-era client cannot read these keys, and vice versa.

Why the extra manifest checks

Key file names did not change across the migration, so a hash-sig-keys/ directory left from an older image still satisfies the existing "keys already exist" check and its 52-byte pubkeys would be written into config.yaml with nothing flagging it. The generator now prints what it found and says how to regenerate:

   Key scheme: SchemeAbortingTargetSumLifetime32Dim46Base8
   Public key size: 52 bytes
   ⚠️  Manifest reports no 'pubkey_bytes' - these keys predate hash-sig-cli 0.5.0
      and are in the retired leanSig format. ...
      Regenerate: ./generate-genesis.sh <dir> --forceKeyGen

The pubkey_bytes mismatch case is a hard error: it means the manifest was edited or merged incorrectly, which has silently corrupted a key set before (a YAML 1.1 loader coerces unquoted 0x pubkeys to integers).

Verification

  • generate-genesis.sh run end to end from an empty genesis dir with --forceKeyGen against the new image: keys generated, manifest verified as XmssTargetSumLifetime32Dim42Base8 / 32 bytes, and config.yaml, validators.yaml, nodes.yaml, genesis.json, genesis.ssz, annotated_validators.yaml plus node keys all produced. All four pubkeys in config.yaml are 32 bytes.
  • Re-run against an existing 32-byte key directory: passes silently.
  • Re-run against a 52-byte key directory: warns with the regeneration command, as above.
  • bash -n clean on the touched shell scripts; the ansible files parse.

⚠️ Breaking

Existing hash-sig key sets cannot be migrated. Regenerate with --forceKeyGen, and run a leanVM-XMSS client build (devnet5-leanvm-main for ethlambda). Other clients need their own leanVM-XMSS images before they can join such a devnet.

leanVM absorbed XMSS into its own crate and the standalone leanSig crate was
retired, so the key format clients read changed: public keys are SSZ and 32
bytes rather than 52, and secret keys are postcard-encoded. No published
hash-sig-cli tag produced that format, so the keygen image moves to
ghcr.io/lambdaclass/hash-sig-cli:0.5.0 (multi-arch, built from hash-sig-cli
PR blockblaz#39). Switch back to a blockblaz tag once upstream releases one.

The ethlambda client image moves in step, to devnet5-leanvm-main: a client
built against leanSig cannot read these keys, so the two pins have to agree.

Key file names and the manifest schema did not change across the migration, so
a leftover hash-sig-keys/ directory still satisfies the "keys already exist"
check and its old-format pubkeys would be baked into config.yaml unnoticed.
Report the scheme and public key size found in the manifest, warn when a key
set predates 0.5.0, and fail when a manifest's declared pubkey_bytes disagrees
with the pubkeys it holds.

Also drops the hardcoded scheme label from the keygen log, which named a
scheme no released image had produced for some time. The scheme now comes from
the manifest, which derives it from the constants the image was built against.

Verified by running generate-genesis.sh end to end against the new image: keys
generated, manifest verified, config.yaml carries 32-byte attestation and
proposal pubkeys, and genesis.ssz built. A 52-byte key directory warns and
names the regeneration command.

BREAKING CHANGE: existing hash-sig key sets cannot be migrated and must be
regenerated with --forceKeyGen; nodes must run a leanVM-XMSS client build.
Copilot AI review requested due to automatic review settings July 28, 2026 21:22

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 updates genesis hash-signature key generation and client/automation pins to match leanVM’s internalized XMSS key format (post-leanSig), including stronger manifest validation to prevent silently reusing incompatible legacy key directories.

Changes:

  • Pin hash-sig keygen to ghcr.io/lambdaclass/hash-sig-cli:0.5.0 and update docs to reflect 32-byte SSZ public keys.
  • Add manifest cross-checks in generate-genesis.sh to report key_scheme, validate pubkey_bytes, and warn/fail on legacy or inconsistent key sets.
  • Bump ethlambda client image pin (and ansible defaults) to devnet5-leanvm-main to stay compatible with the new key format.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Updates user-facing docs for the new keygen image and manifest fields (key_scheme, pubkey_bytes).
generate-genesis.sh Pins the new keygen image and adds manifest-based validation/reporting to detect legacy or inconsistent key directories.
client-cmds/ethlambda-cmd.sh Updates the default docker image tag for ethlambda to the leanVM-XMSS-compatible build.
ansible/roles/ethlambda/tasks/main.yml Updates fallback default ethlambda docker image to match the new client tag.
ansible/roles/ethlambda/defaults/main.yml Updates role default ethlambda docker image tag for consistency with tasks/client script.

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

Comment thread generate-genesis.sh
Comment on lines +362 to +366
MANIFEST_KEY_SCHEME=$(yq eval '.key_scheme // ""' "$MANIFEST_FILE" 2>/dev/null)
MANIFEST_PUBKEY_BYTES=$(yq eval '.pubkey_bytes // ""' "$MANIFEST_FILE" 2>/dev/null)
ATTEST_PUB_BYTES=$(( (${#ATTEST_PUB} - 2) / 2 ))
echo " Key scheme: ${MANIFEST_KEY_SCHEME:-<unreported>}"
echo " Public key size: ${ATTEST_PUB_BYTES} bytes"
Comment thread generate-genesis.sh Outdated
echo " ⚠️ Manifest reports no 'pubkey_bytes' - these keys predate hash-sig-cli 0.5.0"
echo " and are in the retired leanSig format. Clients built against leanVM's XMSS"
echo " cannot read them; the genesis below will not start such a devnet."
echo " Regenerate: ./generate-genesis.sh $GENESIS_DIR --forceKeyGen"
Comment thread README.md Outdated

1. **post-quantum secure validator keypairs** in `genesis/hash-sig-keys` unless already generated or forced with `--forceKeyGen`
2. **config.yaml** - Updated genesis time, `ATTESTATION_COMMITTEE_COUNT`, and `GENESIS_VALIDATORS` with **attestation** and **proposal** public keys per validator (dual-key layout / `hash-sig-cli:devnet4`)
2. **config.yaml** - Updated genesis time, `ATTESTATION_COMMITTEE_COUNT`, and `GENESIS_VALIDATORS` with **attestation** and **proposal** public keys per validator (dual-key layout, 32-byte SSZ pubkeys / `hash-sig-cli:0.5.0`)
Review follow-ups on the checks added in the previous commit.

Requiring whole hex bytes in the pubkey pattern makes the byte count exact
instead of rounding an odd-length string down, and it rejects a truncated
pubkey where it is already reported rather than several lines later.

A non-numeric pubkey_bytes made `[ -ne ]` fail as a *condition*, so bash took
the else path and skipped the check entirely instead of reporting anything.
Validate it before comparing.

Quote the genesis directory in the printed regeneration command so the hint
stays copy/paste-safe for paths containing spaces, and use the full image
reference in the file list so it matches the pin.
Copilot AI review requested due to automatic review settings July 28, 2026 21:32

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

generate-genesis.sh:383

  • In dual-key mode the new pubkey_bytes consistency check only derives the byte length from the attester pubkey. If the manifest is corrupted such that attester/proposer pubkeys differ in length, this won’t be detected (and the printed “Public key size” may be misleading). Consider computing and validating both lengths, and fail if they differ or if either mismatches pubkey_bytes.
    # Cross-check the pubkey length the manifest declares against the pubkeys it holds.
    # Key file names did not change when the format moved to leanVM's XMSS, so a key
    # directory left over from an older image passes the "keys already exist" check
    # above and would otherwise be baked into config.yaml unnoticed.
    MANIFEST_KEY_SCHEME=$(yq eval '.key_scheme // ""' "$MANIFEST_FILE" 2>/dev/null)
    MANIFEST_PUBKEY_BYTES=$(yq eval '.pubkey_bytes // ""' "$MANIFEST_FILE" 2>/dev/null)
    ATTEST_PUB_BYTES=$(( (${#ATTEST_PUB} - 2) / 2 ))
    echo "   Key scheme: ${MANIFEST_KEY_SCHEME:-<unreported>}"
    echo "   Public key size: ${ATTEST_PUB_BYTES} bytes"
    if [ -z "$MANIFEST_PUBKEY_BYTES" ]; then
        echo "   ⚠️  Manifest reports no 'pubkey_bytes' - these keys predate hash-sig-cli 0.5.0"
        echo "      and are in the retired leanSig format. Clients built against leanVM's XMSS"
        echo "      cannot read them; the genesis below will not start such a devnet."
        echo "      Regenerate: ./generate-genesis.sh \"$GENESIS_DIR\" --forceKeyGen"
    elif [[ ! "$MANIFEST_PUBKEY_BYTES" =~ ^[0-9]+$ ]]; then
        # Guard the arithmetic comparison below: a non-numeric value makes `[ -ne ]` fail
        # as a condition, which would skip the check instead of reporting anything.
        echo "   ❌ Error: manifest 'pubkey_bytes' is not a number: $MANIFEST_PUBKEY_BYTES"
        echo "   The manifest is inconsistent - regenerate it rather than editing it by hand"
        exit 1
    elif [ "$ATTEST_PUB_BYTES" -ne "$MANIFEST_PUBKEY_BYTES" ]; then
        echo "   ❌ Error: manifest declares pubkey_bytes=$MANIFEST_PUBKEY_BYTES but its pubkeys are $ATTEST_PUB_BYTES bytes"
        echo "   The manifest is inconsistent - regenerate it rather than editing it by hand"
        exit 1
    fi

README.md:738

  • The README now states (correctly) that key_scheme is derived from the pinned image/leanVM XMSS constants, but the earlier “Signature Scheme” section still says the system uses a fixed SIGTopLevelTargetSumLifetime32Dim64Base8 scheme. With the updated generator/image, that fixed-scheme statement is now misleading; consider updating that section to reflect that consumers should trust validator-keys-manifest.yaml:key_scheme (and potentially pubkey_bytes) rather than a hardcoded scheme name.
`key_scheme` and `pubkey_bytes` are derived from the constants of the XMSS revision the image was
built against, so they report the actual format on disk instead of a fixed label. `generate-genesis.sh`
reads `pubkey_bytes` back to detect a key directory left over from an older image.

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