mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
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.
183 lines
5.3 KiB
Go
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
|
|
}
|