Files
zeekay e41cd6d4ab crypto precompiles: unvalidated points, a price that could not be paid, and free calls
Across the curve and hash precompiles: decode refuses a point that is not on
its curve or not canonically encoded, rather than handing an off-curve point to
an arithmetic routine; pasta's inversion no longer treats a multiple of p as
invertible, where ModInverse returns nil and leaves the divisor untouched;
compute refuses a quoted price wider than 256 bits instead of returning it
modulo, and enforces readOnly; poseidon's sponge fee grows with its input; hqc
reports an unwired backend as itself rather than as a verification failure; and
sr25519 pins that the cgo and non-cgo verifiers name the same backend and agree,
which is a consensus split when they do not. Every path deducts gas before it
works, so no call is free. KAT vectors accompany each.
2026-08-28 15:39:27 -07:00

183 lines
5.3 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package hqc is the EVM precompile for HQC (Hamming Quasi-Cyclic), the
// NIST PQC4-round selected backup KEM. Code-based, family-disjoint
// from Module-LWE ML-KEM: a structural break in lattice cryptography
// would not compromise HQC, and vice-versa.
//
// Address: 0x0000000000000000000000000000000000012208 (LP-4200 unified
// PQCrypto block, slot following Magnetar 0x012207).
//
// Operations:
//
// 0x01 Encapsulate — generate ciphertext + shared secret from
// public key. Deterministic from a caller-
// provided 32-byte seed (the same discipline
// as the ML-KEM precompile: on-chain
// determinism via seed, never crypto/rand).
//
// Decapsulate is intentionally excluded: it requires the private key
// in calldata, which is public on-chain. Decapsulation MUST be
// performed off-chain.
//
// LP-0120 §4 (family-disjoint KEM) pins this slot.
package hqc
import (
"crypto/sha256"
"errors"
"github.com/luxfi/crypto/hqc"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
)
// ContractAddress is the canonical LP-4200 slot for HQC.
var ContractAddress = common.HexToAddress("0x0000000000000000000000000000000000012208")
// HQCPrecompile is the singleton instance.
var HQCPrecompile = &hqcPrecompile{}
var _ contract.StatefulPrecompiledContract = &hqcPrecompile{}
// SeedSize fixed at 32 bytes — same on-chain-determinism contract as
// the ML-KEM precompile.
const SeedSize = 32
// OpEncapsulate is the only operation. Decapsulate would need the private
// key in calldata, which every validator can read.
const OpEncapsulate uint8 = 0x01
// Mode bytes select the parameter set, mirroring the ML-KEM precompile's
// ordering (128 / 192 / 256 as 0x00 / 0x01 / 0x02).
const (
ModeHQC128 uint8 = 0x00
ModeHQC192 uint8 = 0x01
ModeHQC256 uint8 = 0x02
)
// Gas schedule. HQC encapsulation is dominated by polynomial multiplication
// in F_2[X]/(X^n - 1) with n ≈ 17-58 K bits depending on mode. Charge a
// flat base plus a per-byte cost.
const (
BaseEncapsulateGas uint64 = 25_000
PerByteGas uint64 = 10
)
// Error sentinels. A backend that is not wired reports itself: the
// precompile passes luxfi/crypto/hqc.ErrBackendNotWired straight through
// rather than restating it under a second name.
var (
ErrInvalidInputLength = errors.New("hqc: invalid input length")
ErrInvalidMode = errors.New("hqc: invalid mode byte")
ErrInvalidOperation = errors.New("hqc: invalid operation byte")
)
// deterministicReader — same construction as mlkem's. SHA-256 over
// the caller-supplied 32-byte seed, output stream consumed by the
// underlying HQC encapsulation. Ensures every validator produces
// bit-identical ciphertext (no state divergence).
type deterministicReader struct {
state [SeedSize]byte
pos int
}
func newDeterministicReader(seed []byte) *deterministicReader {
r := &deterministicReader{}
copy(r.state[:], seed)
return r
}
func (r *deterministicReader) Read(p []byte) (int, error) {
for n := 0; n < len(p); {
if r.pos == 0 || r.pos >= 32 {
h := sha256.Sum256(r.state[:])
r.state = h
r.pos = 0
}
c := copy(p[n:], r.state[r.pos:])
r.pos += c
n += c
}
return len(p), nil
}
type hqcPrecompile struct{}
// RequiredGas — base + per-byte over the entire input. The
// per-byte term covers seed (32 B), op byte (1 B), mode byte (1 B),
// and the public key (2,249 / 4,522 / 7,245 B by mode).
func (*hqcPrecompile) RequiredGas(input []byte) uint64 {
return BaseEncapsulateGas + uint64(len(input))*PerByteGas
}
// Wire format
//
// [1 byte op] OpEncapsulate
// [1 byte mode] ModeHQC128 / ModeHQC192 / ModeHQC256
// [32 bytes seed] deterministic-rng seed
// [N bytes pk] HQC public key, N = params.PublicKeySize for mode
//
// Returns the ciphertext concatenated with the 64-byte shared secret
// (CT || SS). The 64-byte shared secret is FIPS-style; truncate caller-
// side if a shorter symmetric key is needed.
func (p *hqcPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
_ common.Address,
input []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
remaining, err := contract.DeductGas(suppliedGas, p.RequiredGas(input))
if err != nil {
return nil, 0, err
}
const minLen = 1 + 1 + SeedSize
if len(input) < minLen {
return nil, remaining, ErrInvalidInputLength
}
op := input[0]
modeByte := input[1]
seed := input[2 : 2+SeedSize]
pkBytes := input[2+SeedSize:]
if op != OpEncapsulate {
return nil, remaining, ErrInvalidOperation
}
var mode hqc.Mode
switch modeByte {
case ModeHQC128:
mode = hqc.HQC128
case ModeHQC192:
mode = hqc.HQC192
case ModeHQC256:
mode = hqc.HQC256
default:
return nil, remaining, ErrInvalidMode
}
params := hqc.MustParamsFor(mode)
if len(pkBytes) != params.PublicKeySize {
return nil, remaining, ErrInvalidInputLength
}
pk := &hqc.PublicKey{Mode: mode, Bytes: pkBytes}
rng := newDeterministicReader(seed)
ct, ss, err := hqc.Encapsulate(pk, rng)
if err != nil {
// Includes the ErrBackendNotWired stub-path failure on hosts
// without an HQC backend compiled in.
return nil, remaining, err
}
out := make([]byte, 0, len(ct.Bytes)+len(ss))
out = append(out, ct.Bytes...)
out = append(out, ss...)
return out, remaining, nil
}