From a68eeee083eec165008739737f8f86eab91b24dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:22:15 -0300 Subject: [PATCH 1/2] build(genesis)!: generate keys for leanVM's internalized XMSS 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 #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. --- README.md | 11 ++++++--- ansible/roles/ethlambda/defaults/main.yml | 2 +- ansible/roles/ethlambda/tasks/main.yml | 2 +- client-cmds/ethlambda-cmd.sh | 2 +- generate-genesis.sh | 29 +++++++++++++++++++---- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 323e56a..5e77592 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,7 @@ validators: # validator nodes specification The `spin-node.sh` triggers genesis generator (`generate-genesis.sh`) which generates the following files based on `validator-config.yaml`: 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`) 3. **validators.yaml** - Validator index assignments using round-robin distribution 4. **nodes.yaml** - ENR (Ethereum Node Records) for peer discovery 5. **genesis.json** - Genesis state in JSON format @@ -512,7 +512,7 @@ You can also run the generator standalone: #### Hash-Based Signature (Post-Quantum) Scheme Validator Keys -**Tool's Docker Image**: `HASH_SIG_CLI_IMAGE="blockblaz/hash-sig-cli:devnet4"` +**Tool's Docker Image**: `HASH_SIG_CLI_IMAGE="ghcr.io/lambdaclass/hash-sig-cli:0.5.0"` **Source**: https://github.com/blockblaz/hash-sig-cli Using the above docker tool the following files are generated (unless already generated or forced via `--forceKeyGen` flag): @@ -710,9 +710,10 @@ The manifest file (`validator-keys-manifest.yaml`) contains metadata about all g # Hash-Signature Validator Keys Manifest # Generated by hash-sig-cli -key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8 +key_scheme: XmssTargetSumLifetime32Dim42Base8 hash_function: Poseidon2 encoding: TargetSum +pubkey_bytes: 32 lifetime: 4294967296 log_num_active_epochs: 10 num_active_epochs: 1024 @@ -732,6 +733,10 @@ validators: proposer_key_privkey_file: validator_1_proposer_key_sk.ssz ``` +`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. + (See [hash-sig-cli](https://github.com/blockblaz/hash-sig-cli) for the exact manifest schema.) ## Troubleshooting diff --git a/ansible/roles/ethlambda/defaults/main.yml b/ansible/roles/ethlambda/defaults/main.yml index 87206bd..db91204 100644 --- a/ansible/roles/ethlambda/defaults/main.yml +++ b/ansible/roles/ethlambda/defaults/main.yml @@ -3,5 +3,5 @@ # Note: These are fallback defaults. Actual values are extracted from client-cmds/ethlambda-cmd.sh # in the tasks/main.yml file. These defaults are used if extraction fails. -ethlambda_docker_image: "ghcr.io/lambdaclass/ethlambda:devnet4" +ethlambda_docker_image: "ghcr.io/lambdaclass/ethlambda:devnet5-leanvm-main" deployment_mode: docker # docker or binary diff --git a/ansible/roles/ethlambda/tasks/main.yml b/ansible/roles/ethlambda/tasks/main.yml index 28e222d..e474d3a 100644 --- a/ansible/roles/ethlambda/tasks/main.yml +++ b/ansible/roles/ethlambda/tasks/main.yml @@ -26,7 +26,7 @@ - name: Set docker image and deployment mode from client-cmd.sh set_fact: - ethlambda_docker_image: "{{ ethlambda_docker_image_raw.stdout | trim | default('ghcr.io/lambdaclass/ethlambda:devnet4') }}" + ethlambda_docker_image: "{{ ethlambda_docker_image_raw.stdout | trim | default('ghcr.io/lambdaclass/ethlambda:devnet5-leanvm-main') }}" deployment_mode: "{{ ethlambda_deployment_mode_raw.stdout | trim | default('docker') }}" - name: Extract node configuration from validator-config.yaml diff --git a/client-cmds/ethlambda-cmd.sh b/client-cmds/ethlambda-cmd.sh index 1ea79ee..4b73790 100644 --- a/client-cmds/ethlambda-cmd.sh +++ b/client-cmds/ethlambda-cmd.sh @@ -51,7 +51,7 @@ node_binary="$binary_path \ $checkpoint_sync_flag" # Command when running as docker container -node_docker="ghcr.io/lambdaclass/ethlambda:devnet4 \ +node_docker="ghcr.io/lambdaclass/ethlambda:devnet5-leanvm-main \ --genesis /config/config.yaml \ --validators /config/annotated_validators.yaml \ --bootnodes /config/nodes.yaml \ diff --git a/generate-genesis.sh b/generate-genesis.sh index 85a31f9..a312ffc 100755 --- a/generate-genesis.sh +++ b/generate-genesis.sh @@ -168,7 +168,7 @@ fi echo " ✅ docker found: $(which docker)" # Hash-sig-cli Docker image (separate attester + proposer keys per validator when using dual-key manifest) -HASH_SIG_CLI_IMAGE="blockblaz/hash-sig-cli:latest" +HASH_SIG_CLI_IMAGE="ghcr.io/lambdaclass/hash-sig-cli:0.5.0" echo " ✅ Using hash-sig-cli Docker image: $HASH_SIG_CLI_IMAGE" echo "" @@ -263,14 +263,13 @@ if [ "$SHOULD_SKIP" == "true" ]; then echo "" else echo " Generating keys for $VALIDATOR_COUNT validators..." - echo " Using scheme: SIGTopLevelTargetSumLifetime32Dim64Base8" echo " Key directory: $HASH_SIG_KEYS_DIR" echo "" - # Generate hash-sig keys for all validators using Docker - # Scheme: SIGTopLevelTargetSumLifetime32Dim64Base8 + # Generate hash-sig keys for all validators using Docker. + # The signature scheme is whatever the pinned image was built against; it is + # reported back in validator-keys-manifest.yaml rather than assumed here. # Active epochs: 2^ACTIVE_EPOCH (from validator-config.yaml) - # Total lifetime: 2^32 (4,294,967,296) # Convert to absolute path for Docker volume mounting GENESIS_DIR_ABS="$(cd "$GENESIS_DIR" && pwd)" @@ -355,6 +354,26 @@ if [ "$DUAL_KEY_MODE" = true ]; then fi done echo " ✅ Manifest verified - dual-key format (attester + proposer)" + + # 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:-}" + 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 [ "$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 else FIRST_PUBKEY=$(yq eval ".validators[0].$PUBKEY_FIELD" "$MANIFEST_FILE" 2>/dev/null) if [ -z "$FIRST_PUBKEY" ]; then From 89d91fca393a6bf80721cee75407aca2277bea88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:32:14 -0300 Subject: [PATCH 2/2] fix(genesis): harden the manifest pubkey cross-check 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. --- README.md | 2 +- generate-genesis.sh | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5e77592..032b6e9 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,7 @@ validators: # validator nodes specification The `spin-node.sh` triggers genesis generator (`generate-genesis.sh`) which generates the following files based on `validator-config.yaml`: 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, 32-byte SSZ pubkeys / `hash-sig-cli:0.5.0`) +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 / `ghcr.io/lambdaclass/hash-sig-cli:0.5.0`) 3. **validators.yaml** - Validator index assignments using round-robin distribution 4. **nodes.yaml** - ENR (Ethereum Node Records) for peer discovery 5. **genesis.json** - Genesis state in JSON format diff --git a/generate-genesis.sh b/generate-genesis.sh index a312ffc..b27be3d 100755 --- a/generate-genesis.sh +++ b/generate-genesis.sh @@ -346,8 +346,9 @@ if [ "$DUAL_KEY_MODE" = true ]; then echo " ❌ Error: Could not read attester/proposer pubkeys from manifest" exit 1 fi + # Whole bytes only, so the byte count derived below is exact rather than rounded. for pk in "$ATTEST_PUB" "$PROP_PUB"; do - if [[ ! "$pk" =~ ^0x[0-9a-fA-F]+$ ]]; then + if [[ ! "$pk" =~ ^0x([0-9a-fA-F]{2})+$ ]]; then echo " ❌ Error: Manifest does not contain hex pubkeys (dual-key mode)" echo " Found: $pk" exit 1 @@ -368,7 +369,13 @@ if [ "$DUAL_KEY_MODE" = true ]; 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" + 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"