mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
dudect verify_ct.go now imports luxfi/precompile/starkfri and exercises StarkFRIVerifyPrecompile.Run. Tracks the upstream rename of the p3q precompile to starkfri (the STARK/FRI low-degree-test verifier slot). CT property is unchanged — we measure dispatch+backend invocation, not verification outcome.
198 lines
6.3 KiB
Go
198 lines
6.3 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build p3q_verify_ct
|
|
|
|
// verify_ct.go — cgo bridge exposing the P3Q precompile structural
|
|
// gate to the C dudect harness in dudect_verify.c.
|
|
//
|
|
// CT-population framing
|
|
// ---------------------
|
|
//
|
|
// The P3Q precompile holds NO long-term secret. Wire calldata is
|
|
// PUBLIC by EVM-precompile-ABI. The CT property we test is therefore
|
|
// not confidentiality but SOUNDNESS:
|
|
//
|
|
// * A timing oracle over magic-mismatch vs length-mismatch lets an
|
|
// attacker probe the chain's structural pre-filter.
|
|
// * An attacker who can distinguish accept-cycles from reject-
|
|
// cycles on the verification dispatch can use the signal as a
|
|
// consensus-relevant oracle over time.
|
|
//
|
|
// Concretely we measure two complementary populations:
|
|
//
|
|
// * Class A (fixed): a single well-formed input passed through the
|
|
// precompile gate. Welch's t-test requires identical class-A
|
|
// samples, so we use pool[0] for every class-A draw.
|
|
//
|
|
// * Class B (varying valid): uniformly-drawn well-formed inputs from
|
|
// a precomputed pool. Each pool entry has the magic header and a
|
|
// correct backend dispatch path; the byte payload of proof and
|
|
// pub fields varies so the dispatch boundary exercises different
|
|
// calldata content.
|
|
//
|
|
// dudect compares cycle distributions of (A) vs (B). Any timing
|
|
// difference detected by dudect under this design is a REAL
|
|
// content-dependent path in the precompile structural gate (i.e.,
|
|
// in `precompile/p3q/contract.go::Run` lines 119..190) or its cgo
|
|
// dispatch to the backend verifier.
|
|
//
|
|
// Build:
|
|
// GOWORK=off go build -buildmode=c-shared \
|
|
// -tags p3q_verify_ct \
|
|
// -o libp3q_verify.dylib ./verify_ct.go
|
|
//
|
|
// On Linux the output extension is .so; the Makefile selects.
|
|
|
|
package main
|
|
|
|
// Force-include the ARM compat shim on AArch64 hosts so that
|
|
// dudect/src/dudect.h's unconditional <emmintrin.h>/<x86intrin.h>
|
|
// includes resolve via the shim's blocking #defines. Matches the
|
|
// Makefile behavior so `go build` and `make` agree on arm64.
|
|
|
|
/*
|
|
#cgo arm64 CFLAGS: -include ${SRCDIR}/dudect_compat.h
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"unsafe"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
starkfri "github.com/luxfi/precompile/starkfri"
|
|
)
|
|
|
|
// Pool size: 64 valid inputs is enough to expose any per-content
|
|
// timing variation without making startup pathologically slow.
|
|
const kValidPool = 64
|
|
|
|
// Per-pool-entry input length. We use the canonical Pulsar-65
|
|
// signature byte-length-equivalent (3293 bytes for ML-DSA-65) wrapped
|
|
// in the P3Q magic header, plus a 96-byte representative public-input
|
|
// payload. This is the production hot-path width.
|
|
const (
|
|
kProofBodyLen = 3293
|
|
kPubLen = 96
|
|
)
|
|
|
|
// Fixed magic header "P3Q1".
|
|
var magicHeader = []byte{0x50, 0x33, 0x51, 0x31}
|
|
|
|
// Per-input total wire size.
|
|
const kInputLen = 1 + 4 + (len("P3Q1") + kProofBodyLen) + 4 + kPubLen
|
|
|
|
var validPool [kValidPool][]byte
|
|
|
|
// buildValidInput constructs a structurally well-formed input whose
|
|
// proof field begins with the magic header. The remaining bytes are
|
|
// drawn from `randSrc` so each pool entry's content differs.
|
|
func buildValidInput(randSrc *[kProofBodyLen + kPubLen]byte) []byte {
|
|
out := make([]byte, 0, kInputLen)
|
|
// Version byte.
|
|
out = append(out, 0x01)
|
|
// proof_len (BE32): 4 (magic) + kProofBodyLen.
|
|
plen := uint32(len(magicHeader) + kProofBodyLen)
|
|
var b4 [4]byte
|
|
binary.BigEndian.PutUint32(b4[:], plen)
|
|
out = append(out, b4[:]...)
|
|
// Magic header + body.
|
|
out = append(out, magicHeader...)
|
|
out = append(out, randSrc[:kProofBodyLen]...)
|
|
// pub_len (BE32).
|
|
binary.BigEndian.PutUint32(b4[:], uint32(kPubLen))
|
|
out = append(out, b4[:]...)
|
|
// pub body.
|
|
out = append(out, randSrc[kProofBodyLen:kProofBodyLen+kPubLen]...)
|
|
return out
|
|
}
|
|
|
|
// Initialise the per-startup pool of valid inputs. Returns 0 on
|
|
// success, non-zero on failure.
|
|
//
|
|
//export p3q_verify_ct_setup
|
|
func p3q_verify_ct_setup() C.int {
|
|
// Register a backend verifier that always accepts. This pins the
|
|
// CT measurement on the precompile dispatch + backend invocation
|
|
// itself, not on a particular verification outcome. (The CT
|
|
// property we want must hold regardless of backend acceptance.)
|
|
starkfri.RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
|
|
return true, nil
|
|
})
|
|
|
|
for i := 0; i < kValidPool; i++ {
|
|
var src [kProofBodyLen + kPubLen]byte
|
|
if _, err := rand.Read(src[:]); err != nil {
|
|
return 1
|
|
}
|
|
validPool[i] = buildValidInput(&src)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Returns the per-sample input size for the C harness scratch buffer.
|
|
//
|
|
//export p3q_verify_ct_input_size
|
|
func p3q_verify_ct_input_size() C.size_t {
|
|
return C.size_t(kInputLen)
|
|
}
|
|
|
|
// Returns the number of valid inputs in the per-startup pool.
|
|
//
|
|
//export p3q_verify_ct_pool_size
|
|
func p3q_verify_ct_pool_size() C.size_t {
|
|
return C.size_t(kValidPool)
|
|
}
|
|
|
|
// Copies validPool[idx] into the caller-supplied dst buffer
|
|
// (kInputLen bytes). Returns 0 on success, non-zero on bounds violation.
|
|
//
|
|
//export p3q_verify_ct_copy_pool
|
|
func p3q_verify_ct_copy_pool(idx C.size_t, dst *C.uint8_t) C.int {
|
|
i := int(idx)
|
|
if i < 0 || i >= kValidPool || validPool[i] == nil {
|
|
return 1
|
|
}
|
|
dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), kInputLen)
|
|
copy(dstSlice, validPool[i])
|
|
return 0
|
|
}
|
|
|
|
// Static singleton — RequiredGas is a pure function of input length.
|
|
var (
|
|
dummyCaller = common.Address{}
|
|
supplied = uint64(1 << 30) // far above required for kInputLen
|
|
)
|
|
|
|
// One dudect measurement sample.
|
|
//
|
|
// data points to kInputLen bytes of caller-controlled wire calldata.
|
|
// The cgo bridge constructs an input slice over those bytes and calls
|
|
// the P3Q precompile Run() dispatch; the return value is ignored
|
|
// (dudect measures cycles, not result).
|
|
//
|
|
// The function MUST be branchless on `data` — any data-dependent
|
|
// branch we introduce here pollutes the measurement before Run()
|
|
// even starts. We only copy data into a fresh slice and dispatch.
|
|
//
|
|
//export p3q_verify_ct
|
|
func p3q_verify_ct(data *C.uint8_t) {
|
|
inputSlice := unsafe.Slice((*byte)(unsafe.Pointer(data)), kInputLen)
|
|
input := append([]byte{}, inputSlice...)
|
|
_, _, _ = starkfri.StarkFRIVerifyPrecompile.Run(
|
|
nil,
|
|
dummyCaller,
|
|
starkfri.ContractStarkFRIVerifyAddress,
|
|
input,
|
|
supplied,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// main is required for `go build -buildmode=c-shared`.
|
|
func main() {}
|