Source: Tectonic Quantum Readiness Audit v1.0 — finding TEC-08
Tracking: this issue (#418)
Verified at: commit 6ad0e54 on 2026-06-11 (line numbers also match current HEAD c18124b)
Status: upstream-blocked (interop requires EIP-778 / discv5 ecosystem PQ support); vendored-scaffolding + key rotation possible now
Vulnerable code
The ENR record signing scheme is hardcoded to secp256k1 (ECDSA), and the signing happens with Keccak256-then-ECDSA in the vendored enr crate:
version-meld/enr/src/keys/k256_key.rs:22-37
/// The ENR key that stores the public key in the ENR record.
pub const ENR_KEY: &str = "secp256k1";
impl EnrKey for SigningKey {
type PublicKey = VerifyingKey;
fn sign_v4(&self, msg: &[u8]) -> Result<Vec<u8>, SigningError> {
// take a keccak256 hash then sign.
let digest = Keccak256::new().chain_update(msg);
let signature: Signature = self
.try_sign_digest_with_rng(&mut OsRng, digest)
.map_err(|_| SigningError::new("failed to sign"))?;
Ok(signature.as_bytes().to_vec())
}
version-meld/enr/src/keys/k256_key.rs:67-74
fn verify_v4(&self, msg: &[u8], sig: &[u8]) -> bool {
if let Ok(sig) = k256::ecdsa::Signature::try_from(sig) {
return self
.verify_digest(Keccak256::new().chain_update(msg), &sig)
.is_ok();
}
false
}
A second algorithm, Ed25519, is implemented but is explicitly not part of the v4 identity scheme — and as shown below it is never selected by this node:
version-meld/enr/src/keys/ed25519.rs:14-22
impl EnrKey for ed25519::Keypair {
type PublicKey = ed25519::PublicKey;
/// Performs ENR-specific signing.
///
/// Using `ed25519` keys do not currently follow the `v4` identity scheme, which dictates
/// `secp256k1` keys should be used.
fn sign_v4(&self, msg: &[u8]) -> Result<Vec<u8>, SigningError> {
Ok(self.sign(msg).to_bytes().to_vec())
}
The combined key type supports both, but is a closed enum of only two elliptic-curve algorithms:
version-meld/enr/src/keys/combined.rs:17-23
pub enum CombinedKey {
/// An `secp256k1` keypair.
Secp256k1(k256::ecdsa::SigningKey),
/// An `Ed25519` keypair.
Ed25519(ed25519::Keypair),
}
In practice the node always uses secp256k1. The discv5 ENR key is derived directly from the libp2p secp256k1 secret — the same key signs both the libp2p identity and the ENR:
node/network/src/discovery/enr_ext.rs:234-249
fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result<CombinedKey, &'static str> {
match key {
Keypair::Secp256k1(key) => {
let secret =
discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes())
.expect("libp2p key must be valid");
Ok(CombinedKey::Secp256k1(secret))
}
Keypair::Ed25519(key) => {
let ed_keypair =
discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32])
.expect("libp2p key must be valid");
Ok(CombinedKey::from(ed_keypair))
}
_ => Err("ENR: Unsupported libp2p key type"),
}
}
And the network key loaded/generated at startup is itself constrained to secp256k1:
node/network/src/service.rs:466-470
/// Loads a private key from disk. If this fails, a new key is
/// generated and is then saved to disk.
///
/// Currently only secp256k1 keys are allowed, as these are the only keys supported by discv5.
pub fn load_private_key(config: &NetworkConfig) -> Keypair {
The same CombinedKey is also what signs discv5 session handshakes (proving control of the node ID), again ECDSA-secp256k1-only:
version-meld/discv5/src/handler/crypto/mod.rs:125-143
pub(crate) fn sign_nonce(
signing_key: &CombinedKey,
challenge_data: &ChallengeData,
ephem_pubkey: &[u8],
dst_id: &NodeId,
) -> Result<Vec<u8>, Discv5Error> {
let signing_message = generate_signing_nonce(challenge_data, ephem_pubkey, dst_id);
match signing_key {
CombinedKey::Secp256k1(key) => {
let message = Sha256::new().chain_update(signing_message);
let signature: Signature = key
.try_sign_digest(message)
.map_err(|e| Discv5Error::Error(format!("Failed to sign message: {}", e)))?;
Ok(signature.as_bytes().to_vec())
}
CombinedKey::Ed25519(_) => Err(Discv5Error::KeyTypeNotSupported("Ed25519")),
}
}
The vulnerability
Primitive: ECDSA over secp256k1 (the implemented-but-unused alternative, Ed25519, is EdDSA over Curve25519). Both are discrete-log-based signatures over elliptic-curve groups.
Why quantum-vulnerable: Both are broken by Shor's algorithm. A cryptographically relevant quantum computer (CRQC) can solve the elliptic-curve discrete logarithm problem in polynomial time, recovering the private key directly from the public key. ENR public keys are published by design (they are the whole point of the record — they let peers verify the record and route to the node), so the public key is freely available to any observer. The Keccak256/Sha256 hashing in front of the signature is irrelevant to this attack; Shor targets the EC key, not the hash. (Grover does not meaningfully apply here — these are asymmetric keys, not symmetric/hash strength.)
What a CRQC attacker could actually DO: Given a target node's published ENR public key, Shor recovers its private key. Because that same key is the libp2p identity (from_libp2p) and the discv5 handshake key (sign_nonce), the attacker can:
- forge arbitrary signed ENRs for that node ID (
sign_v4),
- complete discv5 session handshakes proving "ownership" of the node ID (
sign_nonce),
- impersonate the same libp2p peer identity.
The reachable harms are discovery-layer attacks: eclipse attacks (surround a victim with attacker-controlled "identities"), Sybil amplification, and Kademlia routing-table poisoning — i.e., network isolation and DoS / partitioning.
Honest impact assessment. The audit rates this High; I assess Medium–High as more proportionate:
- No HNDL ("harvest now, decrypt later") exposure. Signatures are not confidentiality; nothing recorded today is decrypted later. Forgery requires a live CRQC at attack time, and the node keys are freely rotatable (delete the network key file → a new secp256k1 key is generated, per
load_private_key). There is no long-lived secret being harvested.
- Bounded blast radius. Stored-data integrity does not depend on this key. It is anchored on-chain via merkle roots (range-proof validation against
pora_chunks_merkle / validate_range_proof), and the miner/reward signing key is a separate key (node/miner configuration), not the p2p network key. So identity forgery yields isolation / DoS / routing manipulation — it does not enable data forgery, proof forgery, or fund/reward theft.
- This is therefore a live-attack-only, availability-class issue on a public-by-design key, not a confidentiality or value-at-risk issue.
Constraint
This cannot be fixed unilaterally in a way that interoperates:
- The EIP-778 v4 identity scheme mandates
secp256k1 (the "secp256k1" ENR key string and the keccak256-then-ECDSA verification rule are normative). A peer running upstream discv5 will reject any ENR signed with a non-standard algorithm, so a unilateral swap would partition this node from the network.
- discv5 session establishment (
generate_session_keys, sign_nonce) is likewise spec-fixed to secp256k1 ECDH/ECDSA; the module header itself notes "the specification explicitly defines a singular encryption and key-derivation algorithm."
However, the enr and discv5 crates are vendored under version-meld/ and therefore modifiable. We can add a new key variant locally; what we cannot do alone is make the rest of the network accept it. Interop requires an EIP-778 extension (a new ENR signature-algorithm identifier) plus discv5 ecosystem adoption.
Potential solution
This is upstream-blocked for interop, but there is concrete scaffolding work that can be done now:
-
Migration trigger to watch (the real unblock): an EIP-778 amendment / new ENR signature-scheme identifier that registers a PQ signature algorithm (e.g. ML-DSA / FIPS 204, or SLH-DSA / FIPS 205), accompanied by discv5 spec + reference-implementation support. Track the Ethereum devp2p/EIP-778 discussions and the sigp/discv5 + sigp/enr repos for a PQ signature-scheme issue. Do not ship a non-standard scheme before this exists, or the node will be partitioned.
-
Scaffolding doable now (vendored crates):
- Add a feature-flagged PQ variant to
CombinedKey in version-meld/enr/src/keys/combined.rs (e.g. CombinedKey::MlDsa(...) behind #[cfg(feature = "pq")]) and a matching EnrKey/EnrPublicKey impl in a new version-meld/enr/src/keys/ module, reusing the trait shape from k256_key.rs (sign_v4/verify_v4/enr_to_public). Mirror the same in version-meld/discv5/src/handler/crypto/mod.rs (sign_nonce match arm). Keep it cfg-gated and off by default so on-wire behavior is unchanged until the network agrees.
- Pull in a vetted PQ signature crate (e.g.
pqcrypto/ml-dsa) only under that feature flag.
-
Operational mitigation available today (no code change): keep network-key rotation operationally ready. Because the key is regenerated on file deletion (load_private_key) and there is no harvest exposure, rotating node keys is a sufficient response after a CRQC appears. Document the rotation runbook (stop node → remove NETWORK_KEY_FILENAME from network_dir → restart). Optionally consider rotating periodically as hygiene; note this changes the node ID / ENR.
-
No action needed on data integrity or rewards for this finding — those rely on on-chain merkle roots and the separate miner key, neither of which is this key.
Checklist
Locations verified against source at 6ad0e54; drafted by Claude Code from the third-party Tectonic PQ audit.
Source: Tectonic Quantum Readiness Audit v1.0 — finding TEC-08
Tracking: this issue (#418)
Verified at: commit
6ad0e54on 2026-06-11 (line numbers also match current HEADc18124b)Status: upstream-blocked (interop requires EIP-778 / discv5 ecosystem PQ support); vendored-scaffolding + key rotation possible now
Vulnerable code
The ENR record signing scheme is hardcoded to
secp256k1(ECDSA), and the signing happens withKeccak256-then-ECDSA in the vendoredenrcrate:version-meld/enr/src/keys/k256_key.rs:22-37
version-meld/enr/src/keys/k256_key.rs:67-74
A second algorithm, Ed25519, is implemented but is explicitly not part of the v4 identity scheme — and as shown below it is never selected by this node:
version-meld/enr/src/keys/ed25519.rs:14-22
The combined key type supports both, but is a closed enum of only two elliptic-curve algorithms:
version-meld/enr/src/keys/combined.rs:17-23
In practice the node always uses
secp256k1. The discv5 ENR key is derived directly from the libp2p secp256k1 secret — the same key signs both the libp2p identity and the ENR:node/network/src/discovery/enr_ext.rs:234-249
And the network key loaded/generated at startup is itself constrained to secp256k1:
node/network/src/service.rs:466-470
The same
CombinedKeyis also what signs discv5 session handshakes (proving control of the node ID), again ECDSA-secp256k1-only:version-meld/discv5/src/handler/crypto/mod.rs:125-143
The vulnerability
Primitive: ECDSA over secp256k1 (the implemented-but-unused alternative, Ed25519, is EdDSA over Curve25519). Both are discrete-log-based signatures over elliptic-curve groups.
Why quantum-vulnerable: Both are broken by Shor's algorithm. A cryptographically relevant quantum computer (CRQC) can solve the elliptic-curve discrete logarithm problem in polynomial time, recovering the private key directly from the public key. ENR public keys are published by design (they are the whole point of the record — they let peers verify the record and route to the node), so the public key is freely available to any observer. The
Keccak256/Sha256hashing in front of the signature is irrelevant to this attack; Shor targets the EC key, not the hash. (Grover does not meaningfully apply here — these are asymmetric keys, not symmetric/hash strength.)What a CRQC attacker could actually DO: Given a target node's published ENR public key, Shor recovers its private key. Because that same key is the libp2p identity (
from_libp2p) and the discv5 handshake key (sign_nonce), the attacker can:sign_v4),sign_nonce),The reachable harms are discovery-layer attacks: eclipse attacks (surround a victim with attacker-controlled "identities"), Sybil amplification, and Kademlia routing-table poisoning — i.e., network isolation and DoS / partitioning.
Honest impact assessment. The audit rates this High; I assess Medium–High as more proportionate:
load_private_key). There is no long-lived secret being harvested.pora_chunks_merkle/validate_range_proof), and the miner/reward signing key is a separate key (node/miner configuration), not the p2p network key. So identity forgery yields isolation / DoS / routing manipulation — it does not enable data forgery, proof forgery, or fund/reward theft.Constraint
This cannot be fixed unilaterally in a way that interoperates:
secp256k1(the"secp256k1"ENR key string and the keccak256-then-ECDSA verification rule are normative). A peer running upstream discv5 will reject any ENR signed with a non-standard algorithm, so a unilateral swap would partition this node from the network.generate_session_keys,sign_nonce) is likewise spec-fixed to secp256k1 ECDH/ECDSA; the module header itself notes "the specification explicitly defines a singular encryption and key-derivation algorithm."However, the
enranddiscv5crates are vendored underversion-meld/and therefore modifiable. We can add a new key variant locally; what we cannot do alone is make the rest of the network accept it. Interop requires an EIP-778 extension (a new ENR signature-algorithm identifier) plus discv5 ecosystem adoption.Potential solution
This is upstream-blocked for interop, but there is concrete scaffolding work that can be done now:
Migration trigger to watch (the real unblock): an EIP-778 amendment / new ENR signature-scheme identifier that registers a PQ signature algorithm (e.g. ML-DSA / FIPS 204, or SLH-DSA / FIPS 205), accompanied by discv5 spec + reference-implementation support. Track the Ethereum
devp2p/EIP-778 discussions and the sigp/discv5 + sigp/enr repos for a PQ signature-scheme issue. Do not ship a non-standard scheme before this exists, or the node will be partitioned.Scaffolding doable now (vendored crates):
CombinedKeyinversion-meld/enr/src/keys/combined.rs(e.g.CombinedKey::MlDsa(...)behind#[cfg(feature = "pq")]) and a matchingEnrKey/EnrPublicKeyimpl in a newversion-meld/enr/src/keys/module, reusing the trait shape fromk256_key.rs(sign_v4/verify_v4/enr_to_public). Mirror the same inversion-meld/discv5/src/handler/crypto/mod.rs(sign_noncematch arm). Keep itcfg-gated and off by default so on-wire behavior is unchanged until the network agrees.pqcrypto/ml-dsa) only under that feature flag.Operational mitigation available today (no code change): keep network-key rotation operationally ready. Because the key is regenerated on file deletion (
load_private_key) and there is no harvest exposure, rotating node keys is a sufficient response after a CRQC appears. Document the rotation runbook (stop node → removeNETWORK_KEY_FILENAMEfromnetwork_dir→ restart). Optionally consider rotating periodically as hygiene; note this changes the node ID / ENR.No action needed on data integrity or rewards for this finding — those rely on on-chain merkle roots and the separate miner key, neither of which is this key.
Checklist
#[cfg(feature = "pq")]PQ key variant scaffold to vendoredversion-meld/enr(CombinedKey+ newEnrKey/EnrPublicKeyimpl), default-off.cfg-gatedsign_nonce/generate_session_keysarms in vendoredversion-meld/discv5.load_private_key).Locations verified against source at 6ad0e54; drafted by Claude Code from the third-party Tectonic PQ audit.