aere-node/patches/0002-aere-pqc-precompiles-testnet.patch
Aere Network 48416dfe73 Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 10:25:45 +03:00

318 lines
15 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aere Network <node@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.
+ *
+ * <p>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.
+ *
+ * <p>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 &lt; 41, or logn not
+ * in {9,10}) returns EMPTY (0x).
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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));
}
}