Files
precompile/ai/cross_chain_mining.go
2026-09-08 02:41:14 -07:00

355 lines
12 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// cross_chain_mining.go — atomic AI-mining claims for the 'AI'/'BLG' economy. Any L1/L2
// produces AI work (TEE-attested compute) or a PII-free data contribution off-chain,
// signs it with a post-quantum ML-DSA key, and submits it (directly or relayed via Warp).
// These methods ATOMICALLY verify the attestation, prevent double-spend, compute the
// reward, and mark the work spent — in one call, so a contract mints in a single step
// without the check-then-act race a multi-call (isSpent → calculateReward → markSpent)
// orchestration would expose to front-running/replay.
//
// They compose only already-verified primitives (VerifyMLDSA, CalculateReward,
// ComputeWorkId, IsSpent/MarkSpent, privacyMultipliers) — no new crypto. The chainId is
// part of the work id, so the same work can't be minted on two chains (the WorkContext
// is bound at attestation time). The PII-free guarantee is asserted by the attesting
// signer (a recognized scrub service / TEE); a future variant verifies a ZK/FHE proof
// (zk 0x0900 / fhe 0x0700) inline for trustless anonymization.
package ai
import (
"crypto/x509"
"encoding/binary"
"fmt"
"math/big"
"github.com/zeebo/blake3"
"github.com/luxfi/precompile/contract"
)
// Gas: the fused cost of the primitives each method composes. A mint now also
// verifies a TEE attestation certificate chain (GasVerifyTEE) — the X.509 work
// that proves the signing key belongs to a real device — so the cost is metered.
const (
GasVerifyAndMintWork uint64 = GasVerifyMLDSA + GasVerifyTEE + GasCalculateReward + GasMarkSpent
GasVerifyAndMintData uint64 = GasVerifyMLDSA + GasVerifyTEE + GasCalculateReward + GasMarkSpent
)
// ErrInputTooShort is returned when calldata is truncated.
var ErrInputTooShort = fmt.Errorf("input too short")
// blobs decodes count length-prefixed blobs from the front of input and reports the offset
// just past the last one:
//
// [4]len0 | blob0 | [4]len1 | blob1 | ...
//
// Every length is widened to int before it is used. Computing the bound in uint32 — as
// `uint32(len(input)) < 4+n+4` — wraps for n near 2^32, so the check passes and the slice
// expression that follows panics with "slice bounds out of range". A panic inside a
// precompile is not recovered anywhere on the execution path, so it takes down every
// validator that processed the transaction; eight bytes of calldata were enough. int is
// wide enough on 64-bit, and the n < 0 test covers a 32-bit build where int(uint32) can go
// negative.
func blobs(input []byte, count int) ([][]byte, int, error) {
out := make([][]byte, 0, count)
off := 0
for range count {
if off+4 > len(input) {
return nil, 0, ErrInputTooShort
}
n := int(binary.BigEndian.Uint32(input[off : off+4]))
off += 4
if n < 0 || off+n > len(input) {
return nil, 0, ErrInputTooShort
}
out = append(out, input[off:off+n])
off += n
}
return out, off, nil
}
// parseTriple decodes three length-prefixed blobs followed by a uint64 chainId:
//
// [4]lenA | A | [4]lenB | B | [4]lenC | C | [8]chainId
func parseTriple(input []byte) (a, b, c []byte, chainId uint64, err error) {
parts, off, err := blobs(input, 3)
if err != nil {
return
}
if off+8 > len(input) {
err = ErrInputTooShort
return
}
return parts[0], parts[1], parts[2], binary.BigEndian.Uint64(input[off : off+8]), nil
}
// mintWork atomically settles an attested compute work-proof: workId is bound to
// (deviceId, nonce, chainId); if unspent, the reward is computed and the work is marked
// spent. Returns the reward; the caller mints it. (The ML-DSA attestation is verified by
// the Run wrapper before this is called.)
func mintWork(stateDB StateDB, workProof []byte, chainId uint64) (*big.Int, error) {
if len(workProof) < WorkProofMinSize {
return nil, ErrInvalidWorkProof
}
var deviceId, nonce [32]byte
copy(deviceId[:], workProof[0:32])
copy(nonce[:], workProof[32:64])
workId := ComputeWorkId(deviceId, nonce, chainId)
if IsSpent(stateDB, workId) {
return nil, ErrWorkAlreadySpent
}
reward, err := CalculateReward(workProof, chainId)
if err != nil {
return nil, err
}
if err := MarkSpent(stateDB, workId); err != nil {
return nil, err
}
return reward, nil
}
// mintData atomically settles a PII-free data contribution. descriptor =
// dataHash(32) | dataSize(uint64,8) | privacyLevel(uint16,2). The contribution id binds
// all of these to the chain, so the same dataset can't be double-claimed. Reward scales
// with data size and the privacy multiplier (the same schedule as compute mining).
func mintData(stateDB StateDB, descriptor []byte, chainId uint64) (*big.Int, error) {
if len(descriptor) < DataContributionSize {
return nil, ErrInvalidWorkProof
}
var dataHash [32]byte
copy(dataHash[:], descriptor[0:32])
dataSize := binary.BigEndian.Uint64(descriptor[32:40])
privacyLevel := binary.BigEndian.Uint16(descriptor[40:42])
// dataSize is written by the attested device and multiplies the reward, so
// it is bounded before it is used. Attestation proves the device is real,
// not that its self-reported size is honest.
if dataSize > MaxDataSize {
return nil, ErrClaimTooLarge
}
multiplier, ok := privacyMultipliers[privacyLevel]
if !ok {
return nil, ErrInvalidPrivacyLevel
}
// contributionId = ComputeWorkId(dataHash, size||privacy, chainId) — unique per
// (dataset, size, privacy, chain), preventing double-claim of the same data.
var nonce [32]byte
binary.BigEndian.PutUint64(nonce[0:8], dataSize)
binary.BigEndian.PutUint16(nonce[8:10], privacyLevel)
contributionId := ComputeWorkId(dataHash, nonce, chainId)
if IsSpent(stateDB, contributionId) {
return nil, ErrWorkAlreadySpent
}
reward := new(big.Int).Set(baseRewardPerMinute)
reward.Mul(reward, new(big.Int).SetUint64(dataSize))
reward.Mul(reward, big.NewInt(int64(multiplier)))
reward.Div(reward, big.NewInt(10000))
if err := MarkSpent(stateDB, contributionId); err != nil {
return nil, err
}
return reward, nil
}
// verifyDeviceBinding is the trust gate that makes a mint impossible without a
// chain-trusted signature. The attestation envelope appended to a work proof /
// data descriptor is:
//
// envelope = [4]sigLen | teeSig | receipt
// receipt = report(48) | certChainDER (report = deviceID(32)|ts(8)|nonce(8))
//
// It establishes, in order: (1) the certificate chain in the receipt terminates
// at an embedded vendor root (a genuine TEE device), checked at the report's own
// timestamp so every validator agrees; (2) teeSig is the device leaf's signature
// over the 48-byte report; and (3) the attested deviceID equals BLAKE3(pubkey) —
// so the trust runs embedded-root → device leaf → this exact ML-DSA key. A miner
// that presents a self-generated key with no embedded-root-anchored quote is
// rejected: the signing key is vouched for on chain, never asserted in calldata.
//
// roots is threaded explicitly (production passes teeRootPool(); tests pass a
// generated PKI) so the gate carries no hidden global state.
func verifyDeviceBinding(pubkey, envelope []byte, roots *x509.CertPool) error {
if len(envelope) < 4 {
return ErrInvalidTEEReceipt
}
sigLen := uint64(binary.BigEndian.Uint32(envelope[0:4]))
if sigLen == 0 || 4+sigLen > uint64(len(envelope)) {
return ErrInvalidTEEReceipt
}
teeSig := envelope[4 : 4+sigLen]
receipt := envelope[4+sigLen:]
ok, err := verifyAttestation(receipt, teeSig, roots)
if err != nil {
return err
}
if !ok {
return ErrTEESignatureInvalid
}
// receipt is guaranteed >= teeReportLen here (verifyAttestation enforced it),
// so receipt[0:32] (the attested deviceID) is safe to read.
var attestedDevice [32]byte
copy(attestedDevice[:], receipt[:32])
if attestedDevice != blake3.Sum256(pubkey) {
return ErrDeviceKeyMismatch
}
return nil
}
func (c *AIMiningContract) runVerifyAndMintWork(
accessibleState contract.AccessibleState,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
return c.verifyAndMintWork(accessibleState, input, suppliedGas, readOnly, teeRootPool())
}
// verifyAndMintWork is runVerifyAndMintWork with the trust-anchor pool injected.
func (c *AIMiningContract) verifyAndMintWork(
accessibleState contract.AccessibleState,
input []byte,
suppliedGas uint64,
readOnly bool,
roots *x509.CertPool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot mint in read-only mode")
}
if suppliedGas < GasVerifyAndMintWork {
return nil, 0, fmt.Errorf("out of gas")
}
remaining := suppliedGas - GasVerifyAndMintWork
workProof, pubkey, signature, chainId, err := parseTriple(input)
if err != nil {
return nil, remaining, err
}
// 1. The post-quantum key signed THIS exact work claim.
valid, err := VerifyMLDSA(pubkey, workProof, signature)
if err != nil {
return nil, remaining, err
}
if !valid {
return nil, remaining, fmt.Errorf("ml-dsa attestation invalid")
}
if len(workProof) < WorkProofMinSize {
return nil, remaining, ErrInvalidWorkProof
}
privacyLevel := binary.BigEndian.Uint16(workProof[WorkProofPrivacyOffset:WorkProofComputeMinsOffset])
stateDB := accessibleState.GetStateDB()
dbAdapter := &stateDBAdapter{stateDB, ContractAddress}
// 2. Hardware TEE / Sovereign Attestation Trust Gate:
// Initially, only the Hanzo organization (PrivacySovereign with authorized key)
// is permitted to mine AI without confidential compute (hardware TEE quote).
// Untrusted third-party miners MUST provide a valid hardware TEE attestation quote.
if privacyLevel == PrivacySovereign {
if !IsSovereignAuthorized(dbAdapter, pubkey) {
return nil, remaining, ErrUnauthorized
}
} else {
if len(workProof) <= WorkProofTEEQuoteOffset {
return nil, remaining, ErrMissingAttestation
}
if err := verifyDeviceBinding(pubkey, workProof[WorkProofTEEQuoteOffset:], roots); err != nil {
return nil, remaining, err
}
}
// 3. The work proof's own deviceID must equal the attested key identity, so
// the settled workId is scoped to the chain-trusted device, not a value
// the attacker grinds freely.
if [32]byte(workProof[:32]) != blake3.Sum256(pubkey) {
return nil, remaining, ErrDeviceKeyMismatch
}
reward, err := mintWork(dbAdapter, workProof, chainId)
if err != nil {
return nil, remaining, err
}
result := make([]byte, 32)
reward.FillBytes(result)
return result, remaining, nil
}
func (c *AIMiningContract) runVerifyAndMintData(
accessibleState contract.AccessibleState,
input []byte,
suppliedGas uint64,
readOnly bool,
) ([]byte, uint64, error) {
return c.verifyAndMintData(accessibleState, input, suppliedGas, readOnly, teeRootPool())
}
// verifyAndMintData is runVerifyAndMintData with the trust-anchor pool injected.
func (c *AIMiningContract) verifyAndMintData(
accessibleState contract.AccessibleState,
input []byte,
suppliedGas uint64,
readOnly bool,
roots *x509.CertPool,
) ([]byte, uint64, error) {
if readOnly {
return nil, suppliedGas, fmt.Errorf("cannot mint in read-only mode")
}
if suppliedGas < GasVerifyAndMintData {
return nil, 0, fmt.Errorf("out of gas")
}
remaining := suppliedGas - GasVerifyAndMintData
descriptor, pubkey, signature, chainId, err := parseTriple(input)
if err != nil {
return nil, remaining, err
}
// 1. The post-quantum key signed THIS exact data descriptor.
valid, err := VerifyMLDSA(pubkey, descriptor, signature)
if err != nil {
return nil, remaining, err
}
if !valid {
return nil, remaining, fmt.Errorf("ml-dsa attestation invalid")
}
if len(descriptor) < DataContributionSize {
return nil, remaining, ErrInvalidWorkProof
}
privacyLevel := binary.BigEndian.Uint16(descriptor[40:42])
stateDB := accessibleState.GetStateDB()
dbAdapter := &stateDBAdapter{stateDB, ContractAddress}
// 2. Hardware TEE / Sovereign Attestation Trust Gate:
// Initially, only the Hanzo organization is permitted to contribute data without
// a hardware TEE enclave quote.
if privacyLevel == PrivacySovereign {
if !IsSovereignAuthorized(dbAdapter, pubkey) {
return nil, remaining, ErrUnauthorized
}
} else {
// A TEE quote (appended after the 42-byte descriptor) binds that key to a
// real, embedded-root-certified device (the scrub service's enclave).
if len(descriptor) <= DataContributionSize {
return nil, remaining, ErrMissingAttestation
}
if err := verifyDeviceBinding(pubkey, descriptor[DataContributionSize:], roots); err != nil {
return nil, remaining, err
}
}
reward, err := mintData(dbAdapter, descriptor, chainId)
if err != nil {
return nil, remaining, err
}
result := make([]byte, 32)
reward.FillBytes(result)
return result, remaining, nil
}