mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
markSpent was an unauthenticated public selector on the double-spend guard. workId = BLAKE3(deviceId‖nonce‖chainId) is computable offline, so any address could mark any claim spent before its owner presented it — denying the reward, and, where a missing claim reads as a missing attestation, forcing a slash. The Solidity interface has always documented "only callable by authorized contracts"; no check existed and ErrUnauthorized sat declared and unused. It now fails closed: the atomic paths mark spent internally as the last step of admitting a claim, which is the only safe ordering, so nothing legitimate is lost and there is no authority to guess at. The mint paths already require a TEE quote and bind it to the signing key (ErrMissingAttestation / ErrDeviceKeyMismatch), which establishes that a real 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. Unbounded, one sovereign-privacy data claim settles 1e18 * (2^64-1) * 1.5 / 1e4 = 2.767e19 AI. Both magnitudes are now bounded per claim — a ceiling on how wrong one claim can be, not a substitute for the binding above. Co-authored-by: Hanzo Dev <[email protected]>
339 lines
9.6 KiB
Go
339 lines
9.6 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ai
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/precompile/modules"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
)
|
|
|
|
var _ contract.Configurator = (*configurator)(nil)
|
|
var _ contract.StatefulPrecompiledContract = (*AIMiningContract)(nil)
|
|
|
|
// ConfigKey is the key used in json config files to specify this precompile config.
|
|
const ConfigKey = "aiMiningConfig"
|
|
|
|
// ContractAddress is the address of the AI Mining precompile
|
|
// AI Mining range: 0x0300-0x03FF (significant bytes at START of 20-byte address)
|
|
var ContractAddress = common.HexToAddress("0x0300000000000000000000000000000000000000")
|
|
|
|
// AIMiningPrecompile is the singleton instance
|
|
var AIMiningPrecompile = &AIMiningContract{}
|
|
|
|
// Module is the precompile module
|
|
var Module = modules.Module{
|
|
ConfigKey: ConfigKey,
|
|
Address: ContractAddress,
|
|
Contract: AIMiningPrecompile,
|
|
Configurator: &configurator{},
|
|
}
|
|
|
|
// Method selectors (first 4 bytes of keccak256 of function signature)
|
|
const (
|
|
SelectorVerifyMLDSA uint32 = 0x01000000 // verifyMLDSA(bytes,bytes,bytes)
|
|
SelectorCalculateReward uint32 = 0x02000000 // calculateReward(bytes,uint64)
|
|
SelectorVerifyTEE uint32 = 0x03000000 // verifyTEE(bytes,bytes)
|
|
SelectorIsSpent uint32 = 0x04000000 // isSpent(bytes32)
|
|
SelectorMarkSpent uint32 = 0x05000000 // markSpent(bytes32)
|
|
SelectorComputeWorkId uint32 = 0x06000000 // computeWorkId(bytes32,bytes32,uint64)
|
|
// Atomic cross-chain mining claims (verify attestation -> reward -> mark spent).
|
|
SelectorVerifyAndMintWork uint32 = 0x07000000 // verifyAndMintWork(workProof,pubkey,sig,chainId) -> reward
|
|
SelectorVerifyAndMintData uint32 = 0x08000000 // verifyAndMintData(descriptor,pubkey,sig,chainId) -> reward
|
|
)
|
|
|
|
type configurator struct{}
|
|
|
|
func init() {
|
|
if err := modules.RegisterModule(Module); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (*configurator) MakeConfig() precompileconfig.Config {
|
|
return new(Config)
|
|
}
|
|
|
|
func (*configurator) Configure(
|
|
chainConfig precompileconfig.ChainConfig,
|
|
cfg precompileconfig.Config,
|
|
state contract.StateDB,
|
|
blockContext contract.ConfigurationBlockContext,
|
|
) error {
|
|
// No state initialization required
|
|
return nil
|
|
}
|
|
|
|
// Config implements the precompileconfig.Config interface
|
|
type Config struct {
|
|
Upgrade precompileconfig.Upgrade `json:"upgrade"`
|
|
}
|
|
|
|
func (c *Config) Key() string {
|
|
return ConfigKey
|
|
}
|
|
|
|
func (c *Config) Timestamp() *uint64 {
|
|
return c.Upgrade.Timestamp()
|
|
}
|
|
|
|
func (c *Config) IsDisabled() bool {
|
|
return c.Upgrade.Disable
|
|
}
|
|
|
|
func (c *Config) Equal(cfg precompileconfig.Config) bool {
|
|
other, ok := cfg.(*Config)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return c.Upgrade.Equal(&other.Upgrade)
|
|
}
|
|
|
|
func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error {
|
|
return nil
|
|
}
|
|
|
|
// AIMiningContract implements the AI Mining precompile
|
|
type AIMiningContract struct{}
|
|
|
|
// Run executes the precompile
|
|
func (c *AIMiningContract) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) (ret []byte, remainingGas uint64, err error) {
|
|
if len(input) < 4 {
|
|
return nil, suppliedGas, fmt.Errorf("input too short")
|
|
}
|
|
|
|
selector := binary.BigEndian.Uint32(input[:4])
|
|
data := input[4:]
|
|
|
|
switch selector {
|
|
case SelectorVerifyMLDSA:
|
|
return c.runVerifyMLDSA(accessibleState, data, suppliedGas, readOnly)
|
|
case SelectorCalculateReward:
|
|
return c.runCalculateReward(accessibleState, data, suppliedGas)
|
|
case SelectorVerifyTEE:
|
|
return c.runVerifyTEE(accessibleState, data, suppliedGas)
|
|
case SelectorIsSpent:
|
|
return c.runIsSpent(accessibleState, data, suppliedGas)
|
|
case SelectorMarkSpent:
|
|
return c.runMarkSpent(accessibleState, data, suppliedGas, readOnly)
|
|
case SelectorComputeWorkId:
|
|
return c.runComputeWorkId(data, suppliedGas)
|
|
case SelectorVerifyAndMintWork:
|
|
return c.runVerifyAndMintWork(accessibleState, data, suppliedGas, readOnly)
|
|
case SelectorVerifyAndMintData:
|
|
return c.runVerifyAndMintData(accessibleState, data, suppliedGas, readOnly)
|
|
default:
|
|
return nil, suppliedGas, fmt.Errorf("unknown method selector: %x", selector)
|
|
}
|
|
}
|
|
|
|
func (c *AIMiningContract) runVerifyMLDSA(
|
|
state contract.AccessibleState,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasVerifyMLDSA {
|
|
return nil, 0, fmt.Errorf("out of gas")
|
|
}
|
|
|
|
// pubkey | message | signature, each length-prefixed. blobs is the one decoder for
|
|
// this framing; see its comment for why the lengths may not be handled in uint32.
|
|
parts, _, err := blobs(input, 3)
|
|
if err != nil {
|
|
return nil, suppliedGas - GasVerifyMLDSA, err
|
|
}
|
|
|
|
valid, err := VerifyMLDSA(parts[0], parts[1], parts[2])
|
|
if err != nil {
|
|
return nil, suppliedGas - GasVerifyMLDSA, err
|
|
}
|
|
|
|
result := make([]byte, 32)
|
|
if valid {
|
|
result[31] = 1
|
|
}
|
|
return result, suppliedGas - GasVerifyMLDSA, nil
|
|
}
|
|
|
|
func (c *AIMiningContract) runCalculateReward(
|
|
state contract.AccessibleState,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasCalculateReward {
|
|
return nil, 0, fmt.Errorf("out of gas")
|
|
}
|
|
|
|
if len(input) < 86 { // WorkProofMinSize + 8 bytes for chainId
|
|
return nil, suppliedGas - GasCalculateReward, fmt.Errorf("input too short")
|
|
}
|
|
|
|
workProof := input[:len(input)-8]
|
|
chainId := binary.BigEndian.Uint64(input[len(input)-8:])
|
|
|
|
reward, err := CalculateReward(workProof, chainId)
|
|
if err != nil {
|
|
return nil, suppliedGas - GasCalculateReward, err
|
|
}
|
|
|
|
result := make([]byte, 32)
|
|
reward.FillBytes(result)
|
|
return result, suppliedGas - GasCalculateReward, nil
|
|
}
|
|
|
|
func (c *AIMiningContract) runVerifyTEE(
|
|
state contract.AccessibleState,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasVerifyTEE {
|
|
return nil, 0, fmt.Errorf("out of gas")
|
|
}
|
|
|
|
// receipt | signature, each length-prefixed.
|
|
parts, _, err := blobs(input, 2)
|
|
if err != nil {
|
|
return nil, suppliedGas - GasVerifyTEE, err
|
|
}
|
|
|
|
valid, err := VerifyTEE(parts[0], parts[1])
|
|
if err != nil {
|
|
return nil, suppliedGas - GasVerifyTEE, err
|
|
}
|
|
|
|
result := make([]byte, 32)
|
|
if valid {
|
|
result[31] = 1
|
|
}
|
|
return result, suppliedGas - GasVerifyTEE, nil
|
|
}
|
|
|
|
func (c *AIMiningContract) runIsSpent(
|
|
accessibleState contract.AccessibleState,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasIsSpent {
|
|
return nil, 0, fmt.Errorf("out of gas")
|
|
}
|
|
|
|
if len(input) < 32 {
|
|
return nil, suppliedGas - GasIsSpent, fmt.Errorf("input too short")
|
|
}
|
|
|
|
var workId [32]byte
|
|
copy(workId[:], input[:32])
|
|
|
|
stateDB := accessibleState.GetStateDB()
|
|
spent := IsSpent(&stateDBAdapter{stateDB, ContractAddress}, workId)
|
|
|
|
result := make([]byte, 32)
|
|
if spent {
|
|
result[31] = 1
|
|
}
|
|
return result, suppliedGas - GasIsSpent, nil
|
|
}
|
|
|
|
// runMarkSpent is CLOSED.
|
|
//
|
|
// The spent set is the double-spend guard, and this selector let ANY address
|
|
// burn ANY workId. workId = BLAKE3(deviceId‖nonce‖chainId) is computable
|
|
// offline, so a caller could mark a claim spent before its owner presented it:
|
|
// the reward is denied, and where a missing claim is read as a missing
|
|
// attestation, the denial becomes a forced slash. The Solidity interface has
|
|
// always documented "only callable by authorized contracts"; no such check was
|
|
// ever implemented, and ErrUnauthorized sat declared and unused.
|
|
//
|
|
// Nothing legitimate is lost. The atomic paths (verifyAndMintWork /
|
|
// verifyAndMintData) call MarkSpent internally as the last step of admitting a
|
|
// claim — verify, reward, then spend — which is the only safe ordering. An
|
|
// externally callable mutator on the guard cannot be made safe without an
|
|
// authority to check against, and this precompile carries none, so it fails
|
|
// closed rather than inventing one.
|
|
func (c *AIMiningContract) runMarkSpent(
|
|
accessibleState contract.AccessibleState,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
return nil, suppliedGas, ErrUnauthorized
|
|
}
|
|
|
|
|
|
func (c *AIMiningContract) runComputeWorkId(
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasComputeWorkId {
|
|
return nil, 0, fmt.Errorf("out of gas")
|
|
}
|
|
|
|
if len(input) < 72 { // 32 + 32 + 8
|
|
return nil, suppliedGas - GasComputeWorkId, fmt.Errorf("input too short")
|
|
}
|
|
|
|
var deviceId, nonce [32]byte
|
|
copy(deviceId[:], input[:32])
|
|
copy(nonce[:], input[32:64])
|
|
chainId := binary.BigEndian.Uint64(input[64:72])
|
|
|
|
workId := ComputeWorkId(deviceId, nonce, chainId)
|
|
return workId[:], suppliedGas - GasComputeWorkId, nil
|
|
}
|
|
|
|
// stateDBAdapter adapts contract.StateDB to ai.StateDB
|
|
type stateDBAdapter struct {
|
|
stateDB contract.StateDB
|
|
addr common.Address
|
|
}
|
|
|
|
func (a *stateDBAdapter) GetState(addr [20]byte, key [32]byte) [32]byte {
|
|
return [32]byte(a.stateDB.GetState(common.Address(addr), common.Hash(key)))
|
|
}
|
|
|
|
func (a *stateDBAdapter) SetState(addr [20]byte, key [32]byte, value [32]byte) {
|
|
a.stateDB.SetState(common.Address(addr), common.Hash(key), common.Hash(value))
|
|
}
|
|
|
|
// RequiredGas returns the gas required for the precompile input
|
|
func (c *AIMiningContract) RequiredGas(input []byte) uint64 {
|
|
if len(input) < 4 {
|
|
return GasCalculateReward
|
|
}
|
|
|
|
selector := binary.BigEndian.Uint32(input[:4])
|
|
switch selector {
|
|
case SelectorVerifyMLDSA:
|
|
return GasVerifyMLDSA
|
|
case SelectorCalculateReward:
|
|
return GasCalculateReward
|
|
case SelectorVerifyTEE:
|
|
return GasVerifyTEE
|
|
case SelectorIsSpent:
|
|
return GasIsSpent
|
|
case SelectorMarkSpent:
|
|
return GasMarkSpent
|
|
case SelectorComputeWorkId:
|
|
return GasComputeWorkId
|
|
case SelectorVerifyAndMintWork:
|
|
return GasVerifyAndMintWork
|
|
case SelectorVerifyAndMintData:
|
|
return GasVerifyAndMintData
|
|
default:
|
|
return GasCalculateReward
|
|
}
|
|
}
|