Files
precompile/graph/graphvm_client.go
T
zeekay 7883fef008 ai, graph, fhe: price the work, enforce readOnly, refuse the impossible
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.
2026-08-28 11:53:22 -07:00

124 lines
3.7 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package graph
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/luxfi/database"
gvm "github.com/luxfi/precompile/graph/graphvm"
)
// GraphVMClient implements GChainClient by wrapping a real gvm.QueryExecutor.
// This provides the bridge between the EVM precompile interface and the
// actual GraphVM query execution engine.
type GraphVMClient struct {
executor *gvm.QueryExecutor
chainID uint64 // The chain ID this client is connected to
}
// Ensure GraphVMClient implements GChainClient
var _ GChainClient = (*GraphVMClient)(nil)
// NewGraphVMClient creates a new GraphVMClient wrapping a real QueryExecutor.
// If db is nil, a no-op client is returned that returns empty results.
func NewGraphVMClient(db database.Database, config *gvm.GConfig) *GraphVMClient {
var executor *gvm.QueryExecutor
if db != nil {
executor = gvm.NewQueryExecutor(db, config)
}
return &GraphVMClient{
executor: executor,
chainID: 0, // Default to local/primary chain
}
}
// NewGraphVMClientWithChainID creates a client for a specific chain ID.
func NewGraphVMClientWithChainID(db database.Database, config *gvm.GConfig, chainID uint64) *GraphVMClient {
client := NewGraphVMClient(db, config)
client.chainID = chainID
return client
}
// Query executes a GraphQL query against the GraphVM.
// This is the main entry point for EVM contracts calling the precompile.
func (c *GraphVMClient) Query(ctx context.Context, query string, variables map[string]any) ([]byte, error) {
if c.executor == nil {
return nil, fmt.Errorf("GraphVM client not initialized")
}
// Create GraphQL request
req := &gvm.GraphQLRequest{
Query: query,
Variables: variables,
}
// Execute against the real GraphVM
resp := c.executor.Execute(ctx, req)
// Check for errors
if len(resp.Errors) > 0 {
msgs := make([]string, len(resp.Errors))
for i, e := range resp.Errors {
msgs[i] = e.Message
}
return nil, fmt.Errorf("GraphQL error: %s", strings.Join(msgs, "; "))
}
// Marshal the data response to JSON
result, err := json.Marshal(resp.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}
return result, nil
}
// QueryChain executes a query against a specific chain.
// For the local GraphVM, this checks if chainID matches the connected chain.
// For cross-chain queries, this would route to the appropriate chain's GraphVM.
func (c *GraphVMClient) QueryChain(ctx context.Context, chainID uint64, query string, variables map[string]any) ([]byte, error) {
if c.executor == nil {
return nil, fmt.Errorf("GraphVM client not initialized")
}
// For now, only support local chain queries
// Cross-chain queries would require Warp messaging to route to other chains
if chainID != 0 && chainID != c.chainID {
return nil, fmt.Errorf("cross-chain queries not yet supported: requested chain %d, connected to %d", chainID, c.chainID)
}
// Execute the query locally
return c.Query(ctx, query, variables)
}
// GetDB returns the underlying database for direct access if needed.
func (c *GraphVMClient) GetDB() database.Database {
if c.executor == nil {
return nil
}
return c.executor.GetDB()
}
// RegisterResolver allows adding custom resolvers to the GraphVM.
// This enables extending the GraphQL schema with precompile-specific queries.
func (c *GraphVMClient) RegisterResolver(name string, resolver gvm.ResolverFunc) {
if c.executor != nil {
c.executor.RegisterResolver(name, resolver)
}
}
// ChainID returns the chain ID this client is connected to.
func (c *GraphVMClient) ChainID() uint64 {
return c.chainID
}
// SetChainID updates the chain ID for this client.
func (c *GraphVMClient) SetChainID(chainID uint64) {
c.chainID = chainID
}