From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Mon, 20 Jul 2026 10:06:11 +0300 Subject: [PATCH] Aere Network: ML-KEM-768 and Falcon HashToPoint precompiles (testnet only, 0x0ae6-0x0ae7) 0x0ae6 ML-KEM-768 deterministic encapsulation FIPS 203 0x0ae7 Falcon HashToPoint (SHAKE256 sampler) NIST Falcon round 3 NOT ACTIVE ON MAINNET. These two are built and known-answer tested on an isolated single-validator QBFT testnet only. Mainnet activation is a governance decision that has not been taken. Applies on top of patch 0001. --- .../hyperledger/besu/datatypes/Address.java | 6 + .../HashToPointPrecompiledContract.java | 127 +++++++++++++++++ .../MLKEM768PrecompiledContract.java | 128 ++++++++++++++++++ .../MainnetPrecompiledContracts.java | 2 + 4 files changed, 263 insertions(+) create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/precompile/HashToPointPrecompiledContract.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/precompile/MLKEM768PrecompiledContract.java diff --git a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java index 0f486df..0ddc8ce 100644 --- a/datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java +++ b/datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java @@ -106,6 +106,12 @@ public class Address extends BytesHolder { /** AERE PQC precompile: SHAKE256 XOF (FIPS 202). */ public static final Address AERE_SHAKE256 = Address.fromHexString("0x0000000000000000000000000000000000000ae5"); + /** AERE PQC precompile: ML-KEM-768 (FIPS 203) deterministic encapsulation. */ + public static final Address AERE_MLKEM768 = Address.fromHexString("0x0000000000000000000000000000000000000ae6"); + + /** AERE PQC precompile: Falcon HashToPoint (FIPS 206) SHAKE256 rejection sampler. */ + public static final Address AERE_HASHTOPOINT = Address.fromHexString("0x0000000000000000000000000000000000000ae7"); + /** The constant ZERO. */ public static final Address ZERO = Address.fromHexString("0x0"); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/precompile/HashToPointPrecompiledContract.java b/evm/src/main/java/org/hyperledger/besu/evm/precompile/HashToPointPrecompiledContract.java new file mode 100644 index 0000000..49efff4 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/precompile/HashToPointPrecompiledContract.java @@ -0,0 +1,127 @@ +/* + * Copyright contributors to the AERE Network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.precompile; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import jakarta.validation.constraints.NotNull; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.crypto.digests.SHAKEDigest; + +/** + * AERE PQC precompile: Falcon HashToPoint (FIPS 206 / NIST Falcon round-3) at 0x0AE7. + * + *

HashToPoint is the SHAKE256-driven map from a (nonce, message) pair to a challenge polynomial + * {@code c} in Z_q[x]/(x^n+1), q = 12289. It is the single most expensive step of an on-chain + * Falcon verification: a hand-rolled Solidity Falcon-512 verify spends the bulk of its ~10.5M gas + * inside the in-EVM Keccak-f[1600] permutations that drive this rejection sampler. Exposing it + * natively lets a Solidity Falcon verifier replace that whole loop with one ~500-gas staticcall, + * collapsing per-auth Falcon cost. + * + *

Input layout: {@code logn(1) || nonce(40) || message(rest)} where {@code logn} is 9 + * (Falcon-512, n=512) or 10 (Falcon-1024, n=1024). Output: {@code n} coefficients, each a + * big-endian uint16 in [0, q), i.e. {@code 2*n} bytes. Malformed input (length < 41, or logn not + * in {9,10}) returns EMPTY (0x). + * + *

Algorithm (matches the reference {@code hash_to_point_vartime} exactly): absorb + * {@code nonce || message} into a SHAKE256 sponge, then repeatedly squeeze two bytes, interpret + * them as a big-endian 16-bit value {@code w}, and keep {@code w mod q} whenever {@code w < 5q = + * 61445}, until n coefficients are collected. Uses the audited Bouncy Castle SHAKE256 XOF. + */ +public class HashToPointPrecompiledContract extends AbstractPrecompiledContract { + + private static final int Q = 12289; + private static final int REJECT_BOUND = 5 * Q; // 61445 + private static final int NONCE_LEN = 40; + private static final int MIN_INPUT = 1 + NONCE_LEN; // logn byte + 40-byte nonce + + private static final int BASE_GAS = 60; + private static final int GAS_PER_WORD = 12; + + /** + * Instantiates a new HashToPoint precompiled contract. + * + * @param gasCalculator the gas calculator + */ + HashToPointPrecompiledContract(final GasCalculator gasCalculator) { + super("AereHashToPoint", gasCalculator); + } + + /** Ring degree n from the logn selector byte, or 0 if the selector is invalid. */ + private static int degree(final Bytes input) { + if (input.size() < MIN_INPUT) { + return 0; + } + final int logn = input.get(0) & 0xff; + if (logn == 9) { + return 512; + } + if (logn == 10) { + return 1024; + } + return 0; + } + + @Override + public long gasRequirement(final Bytes input) { + final int n = degree(input); + if (n == 0) { + // Malformed: charge only for the bytes actually presented for hashing. + final long words = ((long) input.size() + 31) / 32; + return BASE_GAS + GAS_PER_WORD * words; + } + // Absorbed bytes (everything after the logn selector) + expected squeeze. The sampler keeps a + // sample with probability 61445/65536, so it squeezes ~2*n / 0.9375 bytes on average; charge a + // conservative fixed 70/64 (~1.094x) expansion so gas is a pure function of the input. + final long absorbBytes = input.size() - 1L; + final long squeezeBytes = (2L * n * 70L) / 64L; + final long words = (absorbBytes + 31) / 32 + (squeezeBytes + 31) / 32; + return BASE_GAS + GAS_PER_WORD * words; + } + + @NotNull + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, @NotNull final MessageFrame messageFrame) { + final int n = degree(input); + if (n == 0) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + try { + // Absorb nonce || message (everything after the 1-byte logn selector). + final byte[] absorbed = input.slice(1).toArrayUnsafe(); + final SHAKEDigest shake = new SHAKEDigest(256); + shake.update(absorbed, 0, absorbed.length); + + final byte[] out = new byte[2 * n]; + final byte[] two = new byte[2]; + int filled = 0; + while (filled < n) { + shake.doOutput(two, 0, 2); // incremental squeeze, keeps the sponge in squeezing phase + final int w = ((two[0] & 0xff) << 8) | (two[1] & 0xff); + if (w < REJECT_BOUND) { + final int coeff = w % Q; + out[2 * filled] = (byte) (coeff >>> 8); + out[2 * filled + 1] = (byte) (coeff & 0xff); + filled++; + } + } + return PrecompileContractResult.success(Bytes.wrap(out)); + } catch (final Throwable t) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/precompile/MLKEM768PrecompiledContract.java b/evm/src/main/java/org/hyperledger/besu/evm/precompile/MLKEM768PrecompiledContract.java new file mode 100644 index 0000000..cf455de --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/precompile/MLKEM768PrecompiledContract.java @@ -0,0 +1,128 @@ +/* + * Copyright contributors to the AERE Network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.precompile; + +import org.hyperledger.besu.crypto.SecureRandomProvider; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import java.security.SecureRandom; + +import jakarta.validation.constraints.NotNull; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; + +/** + * AERE PQC precompile: ML-KEM-768 (FIPS 203) DETERMINISTIC encapsulation at 0x0AE6. + * + *

This is AERE's first post-quantum CONFIDENTIALITY primitive on-chain. Every other native PQC + * precompile (0x0AE1-0x0AE5) is a signature or hash and gives post-quantum AUTHENTICITY only. This + * precompile makes a Module-Lattice KEM key-agreement transcript verifiable on-chain: given an + * encapsulation key {@code ek} and the 32-byte encapsulation randomness {@code m} ("coins"), it + * recomputes the ciphertext {@code c} and shared secret {@code K} that ML-KEM.Encaps(ek, m) + * produces. A verifier compares the recomputed {@code (c, K)} against a claimed transcript; equality + * proves the KEM step was performed honestly with the stated coins. This serves UMBRA's PQXDH + * handshake settlement and the AERE PQC key-registry. + * + *

Input layout: {@code ek(1184) || m(32)} = 1216 bytes exactly. + * Output layout: {@code c(1088) || K(32)} = 1120 bytes, or EMPTY (0x) on any malformed input. + * + *

Determinism: FIPS-203 Encaps normally draws {@code m} from a CSPRNG, which cannot run inside a + * consensus-critical precompile. We take {@code m} from calldata and drive Bouncy Castle's + * ML-KEM.Encaps_internal (K-PKE.Encrypt with explicit coins), so every node computes the identical + * {@code (c, K)}. No cryptography is reimplemented here; the audited Bouncy Castle BCPQC ML-KEM + * implementation on the classpath does the work. + */ +public class MLKEM768PrecompiledContract extends AbstractPrecompiledContract { + + /** ML-KEM-768 encapsulation-key (public key) length, FIPS 203. */ + static final int EK_LEN = 1184; + + /** Encapsulation randomness ("coins" m) length. */ + static final int M_LEN = 32; + + /** Expected total calldata length. */ + static final int INPUT_LEN = EK_LEN + M_LEN; // 1216 + + /** ML-KEM-768 ciphertext length. */ + static final int CT_LEN = 1088; + + /** ML-KEM shared-secret length. */ + static final int SS_LEN = 32; + + /** + * Fixed gas. ML-KEM-768 encapsulation is dominated by one A*r matrix-vector product in the NTT + * domain (k=3) plus SHA3/SHAKE hashing; measured on the AERE Besu scratch fork it sits between + * ML-DSA-44 verify (55k) and Falcon-1024 verify (75k). Priced fixed like the other lattice + * precompiles. + */ + private static final long GAS = 60_000L; + + // The generator constructor requires a SecureRandom, but the DETERMINISTIC encapsulation path + // (internalGenerateEncapsulated with caller-supplied coins m) never draws from it: the output + // depends only on (ek, m). Uses Besu's approved provider rather than constructing one directly. + private static final SecureRandom RNG = SecureRandomProvider.publicSecureRandom(); + + /** + * Instantiates a new ML-KEM-768 precompiled contract. + * + * @param gasCalculator the gas calculator + */ + MLKEM768PrecompiledContract(final GasCalculator gasCalculator) { + super("AereMLKEM768", gasCalculator); + } + + @Override + public long gasRequirement(final Bytes input) { + return GAS; + } + + @NotNull + @Override + public PrecompileContractResult computePrecompile( + final Bytes input, @NotNull final MessageFrame messageFrame) { + if (input.size() != INPUT_LEN) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + try { + final byte[] ek = input.slice(0, EK_LEN).toArrayUnsafe(); + final byte[] m = input.slice(EK_LEN, M_LEN).toArrayUnsafe(); + + final MLKEMPublicKeyParameters pub = + new MLKEMPublicKeyParameters(MLKEMParameters.ml_kem_768, ek); + + // Deterministic Encaps: feed the caller-supplied coins m as the encapsulation randomness. + final MLKEMGenerator gen = new MLKEMGenerator(RNG); + final org.bouncycastle.crypto.SecretWithEncapsulation enc = + gen.internalGenerateEncapsulated(pub, m); + + final byte[] ss = enc.getSecret(); + final byte[] ct = enc.getEncapsulation(); + if (ct.length != CT_LEN || ss.length != SS_LEN) { + return PrecompileContractResult.success(Bytes.EMPTY); + } + + final byte[] out = new byte[CT_LEN + SS_LEN]; + System.arraycopy(ct, 0, out, 0, CT_LEN); + System.arraycopy(ss, 0, out, CT_LEN, SS_LEN); + return PrecompileContractResult.success(Bytes.wrap(out)); + } catch (final Throwable t) { + // Consensus rule for the non-signature PQC precompiles: malformed input -> EMPTY, never fault. + return PrecompileContractResult.success(Bytes.EMPTY); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java b/evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java index 43d9fb7..38baa14 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java @@ -236,5 +236,7 @@ public interface MainnetPrecompiledContracts { registry.put(Address.AERE_MLDSA44, new MLDSA44PrecompiledContract(gasCalculator)); registry.put(Address.AERE_SLHDSA128S, new SLHDSA128sPrecompiledContract(gasCalculator)); registry.put(Address.AERE_SHAKE256, new SHAKE256PrecompiledContract(gasCalculator)); + registry.put(Address.AERE_MLKEM768, new MLKEM768PrecompiledContract(gasCalculator)); + registry.put(Address.AERE_HASHTOPOINT, new HashToPointPrecompiledContract(gasCalculator)); } }