mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
674 lines
21 KiB
Go
674 lines
21 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package ai implements the AI Mining precompile for EVM at address 0x0300:
|
|
// reward calculation for attested AI work, and the cryptographic verification
|
|
// that work was really done by the signer who claims it.
|
|
//
|
|
// GPU Acceleration:
|
|
// When batch size >= gpu.Threshold() and GPU is available, operations
|
|
// are dispatched to Metal/CUDA for parallel processing. This provides
|
|
// significant speedup for ML-DSA verification (NTT-based) and attestation
|
|
// verification (SHA-256/ECDSA).
|
|
package ai
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
|
|
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
|
|
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
|
|
"github.com/zeebo/blake3"
|
|
|
|
"github.com/luxfi/accel"
|
|
)
|
|
|
|
// Precompile address
|
|
const PrecompileAddress = "0x0300"
|
|
|
|
// Gas costs - optimized for high-frequency AI mining operations
|
|
const (
|
|
GasVerifyMLDSA uint64 = 3000 // ML-DSA signature verification
|
|
GasCalculateReward uint64 = 1000 // Reward calculation
|
|
GasVerifyTEE uint64 = 5000 // TEE attestation verification (platform-agnostic)
|
|
GasIsSpent uint64 = 100 // O(1) spent set lookup
|
|
GasComputeWorkId uint64 = 50 // BLAKE3 hash computation
|
|
GasMarkSpent uint64 = 5000 // State write for marking spent
|
|
)
|
|
|
|
// ML-DSA key and signature sizes
|
|
const (
|
|
MLDSA44PublicKeySize = 1312
|
|
MLDSA44SignatureSize = 2420
|
|
MLDSA65PublicKeySize = 1952
|
|
MLDSA65SignatureSize = 3309
|
|
MLDSA87PublicKeySize = 2592
|
|
MLDSA87SignatureSize = 4627
|
|
)
|
|
|
|
// Work proof layout offsets
|
|
const (
|
|
WorkProofDeviceIDOffset = 0
|
|
WorkProofNonceOffset = 32
|
|
WorkProofTimestampOffset = 64
|
|
WorkProofPrivacyOffset = 72
|
|
WorkProofComputeMinsOffset = 74
|
|
WorkProofTEEQuoteOffset = 78
|
|
WorkProofMinSize = 78 // Minimum size without TEE quote
|
|
)
|
|
|
|
// Privacy levels for compute attestation
|
|
const (
|
|
PrivacyPublic uint16 = 1 // 0.25x multiplier
|
|
PrivacyPrivate uint16 = 2 // 0.50x multiplier
|
|
PrivacyConfidential uint16 = 3 // 1.00x multiplier
|
|
PrivacySovereign uint16 = 4 // 1.50x multiplier
|
|
)
|
|
|
|
// Privacy level reward multipliers in basis points (10000 = 1.0x)
|
|
var privacyMultipliers = map[uint16]uint64{
|
|
PrivacyPublic: 2500, // 0.25x
|
|
PrivacyPrivate: 5000, // 0.50x
|
|
PrivacyConfidential: 10000, // 1.00x
|
|
PrivacySovereign: 15000, // 1.50x
|
|
}
|
|
|
|
// Base reward per compute minute (1 AI token = 1e18 wei)
|
|
var baseRewardPerMinute = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
|
|
|
|
// Errors
|
|
var (
|
|
ErrInvalidPublicKeySize = errors.New("invalid ML-DSA public key size")
|
|
ErrInvalidSignatureSize = errors.New("invalid ML-DSA signature size")
|
|
ErrInvalidWorkProof = errors.New("invalid work proof format")
|
|
ErrInvalidPrivacyLevel = errors.New("invalid privacy level")
|
|
ErrWorkAlreadySpent = errors.New("work already spent")
|
|
ErrInvalidTEEReceipt = errors.New("invalid TEE attestation receipt")
|
|
ErrTEESignatureInvalid = errors.New("TEE attestation signature verification failed")
|
|
ErrUnauthorized = errors.New("unauthorized caller")
|
|
ErrMissingAttestation = errors.New("work proof missing TEE attestation quote")
|
|
ErrDeviceKeyMismatch = errors.New("attested device identity does not match signing key")
|
|
ErrClaimTooLarge = errors.New("claim magnitude exceeds the per-claim bound")
|
|
)
|
|
|
|
// Per-claim magnitude bounds.
|
|
//
|
|
// The device binding establishes that a REAL, embedded-root-certified device
|
|
// signed the claim. It does not establish that the claim is TRUE: an attested
|
|
// device still writes its own computeMinutes and dataSize, and the reward
|
|
// schedule multiplies them by baseRewardPerMinute. Unbounded, a single
|
|
// data claim at the sovereign multiplier settles 1e18 * (2^64-1) * 1.5 / 1e4
|
|
// = 2.767e19 AI, so one compromised or lying device is an unbounded mint.
|
|
//
|
|
// These are a ceiling on how wrong one claim can be, not a substitute for the
|
|
// binding above. They are generous for honest work and fatal for a fabricated
|
|
// one.
|
|
const (
|
|
// MaxComputeMinutes bounds one compute claim at 31 days of minutes.
|
|
MaxComputeMinutes uint32 = 44640
|
|
// MaxDataSize bounds one data-contribution claim at 1 TiB.
|
|
MaxDataSize uint64 = 1 << 40
|
|
)
|
|
|
|
// DataContributionSize is the fixed size of a data-contribution descriptor:
|
|
// dataHash(32) | dataSize(uint64,8) | privacyLevel(uint16,2). A TEE attestation
|
|
// envelope binding the signing key to a real device follows these 42 bytes.
|
|
const DataContributionSize = 42
|
|
|
|
// StateDB interface for accessing and modifying state
|
|
type StateDB interface {
|
|
GetState(addr [20]byte, key [32]byte) [32]byte
|
|
SetState(addr [20]byte, key [32]byte, value [32]byte)
|
|
}
|
|
|
|
// PrecompileAddress as bytes
|
|
var precompileAddr = [20]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00}
|
|
|
|
// spentSetPrefix is the storage key prefix for spent work IDs
|
|
var spentSetPrefix = [4]byte{'s', 'p', 'n', 't'}
|
|
|
|
// sovereignSetPrefix is the storage key prefix for authorized sovereign mining signers
|
|
var sovereignSetPrefix = [4]byte{'s', 'o', 'v', 'r'}
|
|
|
|
// HanzoOrgAuthorityDeviceID is the canonical authority device measurement for Hanzo Org cloud compute.
|
|
var HanzoOrgAuthorityDeviceID = blake3.Sum256([]byte("hanzo-org-sovereign-ai-compute"))
|
|
|
|
// VerifyMLDSA verifies an ML-DSA signature (quantum-safe, FIPS 204)
|
|
// Automatically detects security level from public key size
|
|
// Gas cost: 3,000
|
|
func VerifyMLDSA(pubkey, message, signature []byte) (bool, error) {
|
|
switch len(pubkey) {
|
|
case MLDSA44PublicKeySize:
|
|
return verifyMLDSA44(pubkey, message, signature)
|
|
case MLDSA65PublicKeySize:
|
|
return verifyMLDSA65(pubkey, message, signature)
|
|
case MLDSA87PublicKeySize:
|
|
return verifyMLDSA87(pubkey, message, signature)
|
|
default:
|
|
return false, ErrInvalidPublicKeySize
|
|
}
|
|
}
|
|
|
|
func verifyMLDSA44(pubkey, message, signature []byte) (bool, error) {
|
|
if len(signature) != MLDSA44SignatureSize {
|
|
return false, ErrInvalidSignatureSize
|
|
}
|
|
var pk mldsa44.PublicKey
|
|
if err := pk.UnmarshalBinary(pubkey); err != nil {
|
|
return false, err
|
|
}
|
|
return mldsa44.Verify(&pk, message, nil, signature), nil
|
|
}
|
|
|
|
func verifyMLDSA65(pubkey, message, signature []byte) (bool, error) {
|
|
if len(signature) != MLDSA65SignatureSize {
|
|
return false, ErrInvalidSignatureSize
|
|
}
|
|
var pk mldsa65.PublicKey
|
|
if err := pk.UnmarshalBinary(pubkey); err != nil {
|
|
return false, err
|
|
}
|
|
return mldsa65.Verify(&pk, message, nil, signature), nil
|
|
}
|
|
|
|
func verifyMLDSA87(pubkey, message, signature []byte) (bool, error) {
|
|
if len(signature) != MLDSA87SignatureSize {
|
|
return false, ErrInvalidSignatureSize
|
|
}
|
|
var pk mldsa87.PublicKey
|
|
if err := pk.UnmarshalBinary(pubkey); err != nil {
|
|
return false, err
|
|
}
|
|
return mldsa87.Verify(&pk, message, nil, signature), nil
|
|
}
|
|
|
|
// CalculateReward calculates the reward for a work proof
|
|
// Work proof format:
|
|
// - [0:32] Device ID (bytes32)
|
|
// - [32:64] Nonce (bytes32)
|
|
// - [64:72] Timestamp (uint64)
|
|
// - [72:74] Privacy level (uint16)
|
|
// - [74:78] Compute minutes (uint32)
|
|
// - [78:...] TEE quote (variable, optional)
|
|
//
|
|
// Gas cost: 1,000
|
|
func CalculateReward(workProof []byte, chainId uint64) (*big.Int, error) {
|
|
if len(workProof) < WorkProofMinSize {
|
|
return nil, ErrInvalidWorkProof
|
|
}
|
|
|
|
// Extract privacy level (big-endian uint16)
|
|
privacyLevel := binary.BigEndian.Uint16(workProof[WorkProofPrivacyOffset:WorkProofComputeMinsOffset])
|
|
|
|
// Extract compute minutes (big-endian uint32)
|
|
computeMinutes := binary.BigEndian.Uint32(workProof[WorkProofComputeMinsOffset:WorkProofTEEQuoteOffset])
|
|
if computeMinutes > MaxComputeMinutes {
|
|
return nil, ErrClaimTooLarge
|
|
}
|
|
|
|
// Get multiplier for privacy level
|
|
multiplier, ok := privacyMultipliers[privacyLevel]
|
|
if !ok {
|
|
return nil, ErrInvalidPrivacyLevel
|
|
}
|
|
|
|
// Calculate reward: baseReward * computeMinutes * multiplier / 10000
|
|
reward := new(big.Int).Set(baseRewardPerMinute)
|
|
reward.Mul(reward, big.NewInt(int64(computeMinutes)))
|
|
reward.Mul(reward, big.NewInt(int64(multiplier)))
|
|
reward.Div(reward, big.NewInt(10000))
|
|
|
|
return reward, nil
|
|
}
|
|
|
|
// VerifyTEE verifies a TEE attestation receipt (supports NVIDIA NVTrust, Intel SGX, etc.)
|
|
// This validates the certificate chain and signature against platform root CA
|
|
// Gas cost: 5,000
|
|
func VerifyTEE(receipt, signature []byte) (bool, error) {
|
|
if len(receipt) == 0 {
|
|
return false, ErrInvalidTEEReceipt
|
|
}
|
|
|
|
// TEE receipt validation:
|
|
// 1. Parse receipt structure
|
|
// 2. Verify certificate chain against platform root CA
|
|
// 3. Verify signature over receipt data
|
|
// 4. Check timestamp validity
|
|
// 5. Verify device is in allowed registry
|
|
|
|
// Receipt structure (simplified):
|
|
// [0:32] Device ID (GPU/TEE identifier)
|
|
// [32:40] Timestamp
|
|
// [40:48] Nonce
|
|
// [48:...] Certificate chain
|
|
|
|
if len(receipt) < 48 {
|
|
return false, ErrInvalidTEEReceipt
|
|
}
|
|
|
|
// Basic validation - full verification via platform-specific SDKs
|
|
// Supported platforms: NVIDIA NVTrust, Intel SGX, AMD SEV, ARM TrustZone
|
|
valid, err := verifyTEESignature(receipt, signature)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return valid, nil
|
|
}
|
|
|
|
// verifyTEESignature performs real TEE attestation verification: it parses the
|
|
// X.509 certificate chain embedded in the receipt, verifies that chain to an
|
|
// embedded vendor root CA (Intel SGX today; see tee_roots.go) as of the
|
|
// report's timestamp, and verifies that `signature` is a genuine signature
|
|
// over the report header by the chain's leaf key. It fails closed on anything
|
|
// unverifiable and never returns true by default. See verifyAttestation.
|
|
func verifyTEESignature(receipt, signature []byte) (bool, error) {
|
|
return verifyAttestation(receipt, signature, teeRootPool())
|
|
}
|
|
|
|
// IsSpent checks if a work ID has been spent (O(1) state lookup)
|
|
// Gas cost: 100
|
|
func IsSpent(stateDB StateDB, workId [32]byte) bool {
|
|
key := makeSpentKey(workId)
|
|
value := stateDB.GetState(precompileAddr, key)
|
|
|
|
// Non-zero value means spent
|
|
for _, b := range value {
|
|
if b != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MarkSpent marks a work ID as spent in the state trie
|
|
// Gas cost: 5,000 (state write)
|
|
func MarkSpent(stateDB StateDB, workId [32]byte) error {
|
|
if IsSpent(stateDB, workId) {
|
|
return ErrWorkAlreadySpent
|
|
}
|
|
|
|
key := makeSpentKey(workId)
|
|
value := [32]byte{1} // Non-zero value indicates spent
|
|
stateDB.SetState(precompileAddr, key, value)
|
|
|
|
return nil
|
|
}
|
|
|
|
// makeSpentKey creates the storage key for spent set lookup
|
|
func makeSpentKey(workId [32]byte) [32]byte {
|
|
// Key = BLAKE3(spentSetPrefix || workId)
|
|
h := blake3.New()
|
|
h.Write(spentSetPrefix[:])
|
|
h.Write(workId[:])
|
|
|
|
var key [32]byte
|
|
h.Digest().Read(key[:])
|
|
return key
|
|
}
|
|
|
|
// makeSovereignKey creates the storage key for sovereign signer authorization lookup.
|
|
func makeSovereignKey(deviceId [32]byte) [32]byte {
|
|
h := blake3.New()
|
|
h.Write(sovereignSetPrefix[:])
|
|
h.Write(deviceId[:])
|
|
|
|
var key [32]byte
|
|
h.Digest().Read(key[:])
|
|
return key
|
|
}
|
|
|
|
// IsSovereignAuthorized reports whether pubkey represents an authorized Hanzo Org
|
|
// or sovereign compute signer. Initially, only the Hanzo organization is permitted
|
|
// to mine AI without confidential compute (hardware TEE quotes).
|
|
func IsSovereignAuthorized(stateDB StateDB, pubkey []byte) bool {
|
|
devId := blake3.Sum256(pubkey)
|
|
if devId == HanzoOrgAuthorityDeviceID {
|
|
return true
|
|
}
|
|
if stateDB == nil {
|
|
return false
|
|
}
|
|
key := makeSovereignKey(devId)
|
|
value := stateDB.GetState(precompileAddr, key)
|
|
for _, b := range value {
|
|
if b != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AuthorizeSovereign marks a device ID as authorized for sovereign AI mining in state.
|
|
func AuthorizeSovereign(stateDB StateDB, deviceId [32]byte) error {
|
|
key := makeSovereignKey(deviceId)
|
|
value := [32]byte{1}
|
|
stateDB.SetState(precompileAddr, key, value)
|
|
return nil
|
|
}
|
|
|
|
// DeauthorizeSovereign revokes sovereign authorization for a device ID.
|
|
func DeauthorizeSovereign(stateDB StateDB, deviceId [32]byte) error {
|
|
key := makeSovereignKey(deviceId)
|
|
stateDB.SetState(precompileAddr, key, [32]byte{})
|
|
return nil
|
|
}
|
|
|
|
// ComputeWorkId computes work ID: BLAKE3(deviceId || nonce || chainId)
|
|
// Gas cost: 50
|
|
func ComputeWorkId(deviceId, nonce [32]byte, chainId uint64) [32]byte {
|
|
h := blake3.New()
|
|
h.Write(deviceId[:])
|
|
h.Write(nonce[:])
|
|
|
|
var chainIdBytes [8]byte
|
|
binary.BigEndian.PutUint64(chainIdBytes[:], chainId)
|
|
h.Write(chainIdBytes[:])
|
|
|
|
var workId [32]byte
|
|
h.Digest().Read(workId[:])
|
|
return workId
|
|
}
|
|
|
|
// GetSecurityLevel returns the ML-DSA security level from public key size
|
|
func GetSecurityLevel(pubkey []byte) (uint8, error) {
|
|
switch len(pubkey) {
|
|
case MLDSA44PublicKeySize:
|
|
return 2, nil // NIST Level 2
|
|
case MLDSA65PublicKeySize:
|
|
return 3, nil // NIST Level 3
|
|
case MLDSA87PublicKeySize:
|
|
return 5, nil // NIST Level 5
|
|
default:
|
|
return 0, ErrInvalidPublicKeySize
|
|
}
|
|
}
|
|
|
|
// GetExpectedSignatureSize returns expected signature size for security level
|
|
func GetExpectedSignatureSize(level uint8) (int, error) {
|
|
switch level {
|
|
case 2:
|
|
return MLDSA44SignatureSize, nil
|
|
case 3:
|
|
return MLDSA65SignatureSize, nil
|
|
case 5:
|
|
return MLDSA87SignatureSize, nil
|
|
default:
|
|
return 0, ErrInvalidSignatureSize
|
|
}
|
|
}
|
|
|
|
// ParseWorkProof extracts fields from a work proof
|
|
func ParseWorkProof(workProof []byte) (deviceId, nonce [32]byte, timestamp uint64, privacy uint16, computeMins uint32, teeQuote []byte, err error) {
|
|
if len(workProof) < WorkProofMinSize {
|
|
err = ErrInvalidWorkProof
|
|
return
|
|
}
|
|
|
|
copy(deviceId[:], workProof[WorkProofDeviceIDOffset:WorkProofNonceOffset])
|
|
copy(nonce[:], workProof[WorkProofNonceOffset:WorkProofTimestampOffset])
|
|
timestamp = binary.BigEndian.Uint64(workProof[WorkProofTimestampOffset:WorkProofPrivacyOffset])
|
|
privacy = binary.BigEndian.Uint16(workProof[WorkProofPrivacyOffset:WorkProofComputeMinsOffset])
|
|
computeMins = binary.BigEndian.Uint32(workProof[WorkProofComputeMinsOffset:WorkProofTEEQuoteOffset])
|
|
|
|
if len(workProof) > WorkProofTEEQuoteOffset {
|
|
teeQuote = workProof[WorkProofTEEQuoteOffset:]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// BuildWorkProof constructs a work proof from individual fields
|
|
func BuildWorkProof(deviceId, nonce [32]byte, timestamp uint64, privacy uint16, computeMins uint32, teeQuote []byte) []byte {
|
|
size := WorkProofMinSize + len(teeQuote)
|
|
proof := make([]byte, size)
|
|
|
|
copy(proof[WorkProofDeviceIDOffset:], deviceId[:])
|
|
copy(proof[WorkProofNonceOffset:], nonce[:])
|
|
binary.BigEndian.PutUint64(proof[WorkProofTimestampOffset:], timestamp)
|
|
binary.BigEndian.PutUint16(proof[WorkProofPrivacyOffset:], privacy)
|
|
binary.BigEndian.PutUint32(proof[WorkProofComputeMinsOffset:], computeMins)
|
|
|
|
if len(teeQuote) > 0 {
|
|
copy(proof[WorkProofTEEQuoteOffset:], teeQuote)
|
|
}
|
|
|
|
return proof
|
|
}
|
|
|
|
// GetPrivacyMultiplier returns the reward multiplier for a privacy level
|
|
func GetPrivacyMultiplier(level uint16) (uint64, error) {
|
|
mult, ok := privacyMultipliers[level]
|
|
if !ok {
|
|
return 0, ErrInvalidPrivacyLevel
|
|
}
|
|
return mult, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// GPU Acceleration Support
|
|
// =============================================================================
|
|
|
|
// GPUAvailable returns true if GPU acceleration is available
|
|
func GPUAvailable() bool {
|
|
return accel.Available()
|
|
}
|
|
|
|
// GPUBackend returns the GPU backend name ("Metal", "CUDA", or "CPU")
|
|
func GPUBackend() string {
|
|
if accel.Available() {
|
|
backends := accel.Backends()
|
|
if len(backends) > 0 {
|
|
return backends[0].String()
|
|
}
|
|
}
|
|
return "CPU"
|
|
}
|
|
|
|
// GPUThreshold returns minimum batch size for GPU acceleration
|
|
func GPUThreshold() int {
|
|
return 8 // Default threshold for batch operations
|
|
}
|
|
|
|
// =============================================================================
|
|
// Batch Accumulator for GPU Dispatch
|
|
// =============================================================================
|
|
|
|
// BatchAccumulator collects verification requests for GPU batch processing.
|
|
// When the batch reaches threshold, it dispatches to GPU for parallel verification.
|
|
type BatchAccumulator struct {
|
|
mu sync.Mutex
|
|
pubkeys [][]byte
|
|
messages [][]byte
|
|
signatures [][]byte
|
|
callbacks []func(bool, error)
|
|
threshold int
|
|
}
|
|
|
|
// NewBatchAccumulator creates a new batch accumulator
|
|
func NewBatchAccumulator(threshold int) *BatchAccumulator {
|
|
if threshold < GPUThreshold() {
|
|
threshold = GPUThreshold()
|
|
}
|
|
return &BatchAccumulator{
|
|
threshold: threshold,
|
|
}
|
|
}
|
|
|
|
// Add queues a verification request. Returns true if batch was flushed.
|
|
func (ba *BatchAccumulator) Add(pubkey, message, signature []byte, callback func(bool, error)) bool {
|
|
ba.mu.Lock()
|
|
defer ba.mu.Unlock()
|
|
|
|
ba.pubkeys = append(ba.pubkeys, pubkey)
|
|
ba.messages = append(ba.messages, message)
|
|
ba.signatures = append(ba.signatures, signature)
|
|
ba.callbacks = append(ba.callbacks, callback)
|
|
|
|
if len(ba.signatures) >= ba.threshold {
|
|
ba.flushLocked()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Flush processes all pending verification requests
|
|
func (ba *BatchAccumulator) Flush() {
|
|
ba.mu.Lock()
|
|
defer ba.mu.Unlock()
|
|
ba.flushLocked()
|
|
}
|
|
|
|
// flushLocked processes the batch (must hold lock)
|
|
func (ba *BatchAccumulator) flushLocked() {
|
|
if len(ba.signatures) == 0 {
|
|
return
|
|
}
|
|
|
|
pubkeys := ba.pubkeys
|
|
messages := ba.messages
|
|
signatures := ba.signatures
|
|
callbacks := ba.callbacks
|
|
|
|
// Reset accumulator
|
|
ba.pubkeys = nil
|
|
ba.messages = nil
|
|
ba.signatures = nil
|
|
ba.callbacks = nil
|
|
|
|
// CPU is the single source of truth. The accel SigMLDSA65 batch kernel
|
|
// truncates every message to 32 bytes (accel/ops/crypto/crypto_gpu.go:
|
|
// msgSize=32) and is fixed to ML-DSA-65, so its verdict diverges from
|
|
// VerifyMLDSA (full message, mode-detected from key size) -- a CPU/GPU
|
|
// consensus split anywhere this batch result is trusted. Verify each
|
|
// element on the per-element CPU oracle.
|
|
for i := range signatures {
|
|
valid, verifyErr := VerifyMLDSA(pubkeys[i], messages[i], signatures[i])
|
|
if callbacks[i] != nil {
|
|
callbacks[i](valid, verifyErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Size returns current batch size
|
|
func (ba *BatchAccumulator) Size() int {
|
|
ba.mu.Lock()
|
|
defer ba.mu.Unlock()
|
|
return len(ba.signatures)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Batch Verification Functions
|
|
// =============================================================================
|
|
|
|
// BatchVerifyMLDSA verifies multiple ML-DSA signatures on the CPU oracle.
|
|
// Determinism over speed: the result is identical to calling VerifyMLDSA on
|
|
// each element, so it is byte-stable across the whole validator set
|
|
// regardless of build tags or hardware.
|
|
func BatchVerifyMLDSA(pubkeys, messages, signatures [][]byte) ([]bool, error) {
|
|
// The three slices are indexed in lockstep, so a caller that supplies fewer keys or
|
|
// messages than signatures used to index past the end and panic. Refuse instead:
|
|
// there is no sensible answer for a signature with no key to check it against, and
|
|
// silently verifying the shortest prefix would report "all valid" for a batch that
|
|
// was never fully checked.
|
|
if len(pubkeys) != len(signatures) || len(messages) != len(signatures) {
|
|
return nil, ErrInvalidWorkProof
|
|
}
|
|
|
|
// CPU is the single source of truth (see BatchAccumulator.flushLocked):
|
|
// the accel batch kernel truncates messages to 32 bytes and is mode-fixed,
|
|
// so it cannot match VerifyMLDSA. Verify each element on the CPU oracle.
|
|
results := make([]bool, len(signatures))
|
|
for i := range signatures {
|
|
valid, verifyErr := VerifyMLDSA(pubkeys[i], messages[i], signatures[i])
|
|
if verifyErr != nil {
|
|
results[i] = false
|
|
} else {
|
|
results[i] = valid
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// NVTrustMinQuoteSize is the minimum size for NVTrust attestation quotes
|
|
const NVTrustMinQuoteSize = 48
|
|
|
|
// BatchVerifyTEE verifies multiple TEE attestations.
|
|
// Currently uses CPU-only verification.
|
|
func BatchVerifyTEE(attestations [][]byte) ([]bool, []uint8, error) {
|
|
results := make([]bool, len(attestations))
|
|
scores := make([]uint8, len(attestations))
|
|
for i, att := range attestations {
|
|
if len(att) < NVTrustMinQuoteSize {
|
|
results[i] = false
|
|
scores[i] = 0
|
|
continue
|
|
}
|
|
// Basic attestation validation
|
|
valid, err := VerifyTEE(att, nil)
|
|
if err != nil || !valid {
|
|
results[i] = false
|
|
scores[i] = 0
|
|
} else {
|
|
results[i] = true
|
|
scores[i] = 100 // Maximum score for valid attestation
|
|
}
|
|
}
|
|
return results, scores, nil
|
|
}
|
|
|
|
// BatchCalculateReward computes rewards for multiple work proofs.
|
|
// Currently uses CPU-only calculation.
|
|
func BatchCalculateReward(workProofs [][]byte, chainId uint64) ([]*big.Int, error) {
|
|
n := len(workProofs)
|
|
if n == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// CPU calculation
|
|
results := make([]*big.Int, n)
|
|
for i := range workProofs {
|
|
reward, calcErr := CalculateReward(workProofs[i], chainId)
|
|
if calcErr != nil {
|
|
results[i] = big.NewInt(0)
|
|
} else {
|
|
results[i] = reward
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// GPU Statistics
|
|
// =============================================================================
|
|
|
|
// Stats contains GPU verification statistics
|
|
type Stats struct {
|
|
TotalVerifications uint64
|
|
GPUVerifications uint64
|
|
CPUVerifications uint64
|
|
Errors uint64
|
|
}
|
|
|
|
var (
|
|
statsMu sync.Mutex
|
|
stats Stats
|
|
)
|
|
|
|
// GPUStats returns GPU verification statistics
|
|
func GPUStats() Stats {
|
|
statsMu.Lock()
|
|
defer statsMu.Unlock()
|
|
return stats
|
|
}
|
|
|
|
// ResetGPUStats resets GPU verification statistics
|
|
func ResetGPUStats() {
|
|
statsMu.Lock()
|
|
defer statsMu.Unlock()
|
|
stats = Stats{}
|
|
}
|