mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
graph 58.0 -> 99.1, graphvm 0 -> 100, ai 90.0 -> 96.2, inference 87.3 -> 98.0, modelregistry 70.2 -> 98.9, aivmbridge 93.1 -> 99.8, anchor 98.1 -> 99.1, fhe 33.0 -> 80.1. graph charged one flat fee for any amount of work. The sharpest form was not response size but TargetChains: decoded from caller JSON with no cap, one GraphQL query per entry, so seven bytes of calldata bought an unbounded fan-out. Response size is only known after the work, so the fee now reserves the worst case up front and refunds down -- there is no deduction that can come up short, and a failed run consumes the reservation rather than everything supplied, so malformed input is not a cheap probe. ai halted the chain from eight bytes. Two handlers bounded their length prefixes in uint32, so near 2^32 the sum wrapped, the check passed and the slice panicked. Reproduced at both 96 and 8 bytes. The correct int-based parser already existed in the same package; the handlers just did not call it, and the existing test only probed 0xFFFF, which never wraps. fhe never enforced readOnly. It was threaded through Run and all 42 handlers and read by none, so every op but decrypt and sealOutput wrote ciphertext, metadata and ownership to the trie under STATICCALL. Decided once at dispatch; an unknown selector counts as a writer. fhe also allocated before it checked: five bytes of calldata asked for 34 GB behind a flat 50,000 gas fee. div and rem were registered and priced at 655M gas with no case in the switch, so both fell through and returned the zero handle with a nil error -- success for a guaranteed no-op, with the real implementations written and never wired. select decoded its control as a bare Ciphertext when every ebool the precompile stores is a BitCiphertext, so it could not have worked for any input; it surfaced only once failures stopped being reported as success. graph decodeArgs accepted truncated calldata as a shorter argument list, so trimming bytes changed which variables a query expanded with. graphvm chose resolvers by map iteration order, which is consensus-visible. Deleted a dead second gas table that disagreed with the real one, an address constant appearing only at its own declaration, and ten other symbols with no reader.
173 lines
5.8 KiB
Go
173 lines
5.8 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
gvm "github.com/luxfi/precompile/graph/graphvm"
|
|
"github.com/luxfi/precompile/modules"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
)
|
|
|
|
var _ contract.Configurator = (*configurator)(nil)
|
|
var _ contract.StatefulPrecompiledContract = (*GraphQLContract)(nil)
|
|
|
|
// ConfigKey is the key used in json config files to specify this precompile config.
|
|
const ConfigKey = "graphConfig"
|
|
|
|
// Contract addresses for Graph/Query layer (0x0500-0x05FF)
|
|
// Note: Hashing precompiles (Poseidon2, Pedersen, Blake3) use 0x0500...0001-0004
|
|
// Graph precompiles start at 0x0500...0010 to avoid conflicts
|
|
var (
|
|
ContractGraphQLAddress = common.HexToAddress("0x0500000000000000000000000000000000000010")
|
|
ContractSubscribeAddress = common.HexToAddress("0x0500000000000000000000000000000000000011")
|
|
ContractCacheAddress = common.HexToAddress("0x0500000000000000000000000000000000000012")
|
|
ContractIndexAddress = common.HexToAddress("0x0500000000000000000000000000000000000013")
|
|
)
|
|
|
|
// GraphQLContract wraps GraphQLPrecompile to implement StatefulPrecompiledContract
|
|
type GraphQLContract struct {
|
|
precompile *GraphQLPrecompile
|
|
}
|
|
|
|
// GraphContractInstance is the singleton
|
|
var GraphContractInstance = &GraphQLContract{
|
|
precompile: NewGraphQLPrecompile(nil),
|
|
}
|
|
|
|
// SetGraphVMClient configures the Graph precompile to use a real GraphVM client.
|
|
// This should be called during VM initialization when the database is available.
|
|
func SetGraphVMClient(db database.Database, config *gvm.GConfig) {
|
|
client := NewGraphVMClient(db, config)
|
|
GraphContractInstance.precompile.client = client
|
|
}
|
|
|
|
// SetGraphVMClientWithChainID configures the client for a specific chain.
|
|
func SetGraphVMClientWithChainID(db database.Database, config *gvm.GConfig, chainID uint64) {
|
|
client := NewGraphVMClientWithChainID(db, config, chainID)
|
|
GraphContractInstance.precompile.client = client
|
|
}
|
|
|
|
// Module is the precompile module
|
|
var Module = modules.Module{
|
|
ConfigKey: ConfigKey,
|
|
Address: ContractGraphQLAddress,
|
|
Contract: GraphContractInstance,
|
|
Configurator: &configurator{},
|
|
}
|
|
|
|
type configurator struct{}
|
|
|
|
func init() {
|
|
if err := modules.RegisterModule(Module); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (*configurator) MakeConfig() precompileconfig.Config {
|
|
return new(GraphConfig)
|
|
}
|
|
|
|
func (*configurator) Configure(
|
|
chainConfig precompileconfig.ChainConfig,
|
|
cfg precompileconfig.Config,
|
|
state contract.StateDB,
|
|
blockContext contract.ConfigurationBlockContext,
|
|
) error {
|
|
config, ok := cfg.(*GraphConfig)
|
|
if !ok {
|
|
return fmt.Errorf("expected config type %T, got %T: %v", &GraphConfig{}, cfg, cfg)
|
|
}
|
|
|
|
// Update precompile configuration
|
|
if config.GChainEndpoint != "" {
|
|
GraphContractInstance.precompile.config.GChainEndpoint = config.GChainEndpoint
|
|
}
|
|
if config.QueryTimeout > 0 {
|
|
GraphContractInstance.precompile.config.QueryTimeout = time.Duration(config.QueryTimeout) * time.Second
|
|
}
|
|
if config.MaxCacheSize > 0 {
|
|
GraphContractInstance.precompile.config.MaxCacheSize = config.MaxCacheSize
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GraphConfig implements the precompileconfig.Config interface
|
|
type GraphConfig struct {
|
|
Upgrade precompileconfig.Upgrade `json:"upgrade"`
|
|
GChainEndpoint string `json:"gChainEndpoint,omitempty"`
|
|
QueryTimeout int `json:"queryTimeout,omitempty"`
|
|
MaxCacheSize int `json:"maxCacheSize,omitempty"`
|
|
}
|
|
|
|
func (c *GraphConfig) Key() string {
|
|
return ConfigKey
|
|
}
|
|
|
|
func (c *GraphConfig) Timestamp() *uint64 {
|
|
return c.Upgrade.Timestamp()
|
|
}
|
|
|
|
func (c *GraphConfig) IsDisabled() bool {
|
|
return c.Upgrade.Disable
|
|
}
|
|
|
|
func (c *GraphConfig) Equal(cfg precompileconfig.Config) bool {
|
|
other, ok := cfg.(*GraphConfig)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return c.Upgrade.Equal(&other.Upgrade) &&
|
|
c.GChainEndpoint == other.GChainEndpoint &&
|
|
c.QueryTimeout == other.QueryTimeout &&
|
|
c.MaxCacheSize == other.MaxCacheSize
|
|
}
|
|
|
|
func (c *GraphConfig) Verify(chainConfig precompileconfig.ChainConfig) error {
|
|
return nil
|
|
}
|
|
|
|
// Run implements StatefulPrecompiledContract.
|
|
//
|
|
// Gas is reserved before any work and refunded down to the actual figure afterwards. The
|
|
// reservation covers the largest response the precompile may return, so a query never
|
|
// executes unless its caller has already paid for the worst case — the alternative,
|
|
// charging for the response after producing it, would have the node perform caller-sized
|
|
// work before discovering it cannot be paid for.
|
|
func (c *GraphQLContract) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) (ret []byte, remainingGas uint64, err error) {
|
|
reserved := c.precompile.RequiredGas(input)
|
|
if suppliedGas < reserved {
|
|
return nil, 0, fmt.Errorf("out of gas: required %d, supplied %d", reserved, suppliedGas)
|
|
}
|
|
|
|
result, used, runErr := c.precompile.Run(input)
|
|
if runErr != nil {
|
|
// A failed run consumes the whole reservation. How much work it managed before
|
|
// failing is not measurable — an oversized response is rejected only after it has
|
|
// been produced — so the caller pays the bound it agreed to, which is by
|
|
// construction at least the work performed. Refunding here would make a malformed
|
|
// request a cheap way to probe the precompile.
|
|
return nil, suppliedGas - reserved, runErr
|
|
}
|
|
|
|
// min keeps the subtraction total. RequiredGas and Run price the same decoded request
|
|
// with the same functions, and the response term is bounded by the allowance the
|
|
// reservation already holds, so used <= reserved by construction —
|
|
// TestGraphQL_UpfrontBoundCoversActual asserts it directly rather than relying on it.
|
|
return result, suppliedGas - min(used, reserved), nil
|
|
}
|