mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
359 lines
15 KiB
Go
359 lines
15 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// module_config_test.go — the 0x0303 module surface: registration, the upgrade Config,
|
|
// the gas schedule, and every refusal Run can issue. These assert relationships and
|
|
// refusals, never the constants themselves: a test that restates GasBaseInference
|
|
// proves nothing, while "one more token costs more" and "the clamped price always
|
|
// reverts" are properties a broken implementation cannot satisfy.
|
|
|
|
package inference
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/precompile/modules"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// surplus is handed over and above RequiredGas so a refusal is never an out-of-gas
|
|
// refusal, and so the gas actually charged is observable in the return value.
|
|
const surplus uint64 = 4321
|
|
|
|
// call runs the precompile with exactly RequiredGas+surplus.
|
|
func call(input []byte) ([]byte, uint64, error) {
|
|
gas := InferencePrecompile.RequiredGas(input) + surplus
|
|
return InferencePrecompile.Run(nil, common.Address{}, common.Address{}, input, gas, false)
|
|
}
|
|
|
|
// refuse asserts the precompile rejects input, produces nothing, and still charges
|
|
// the full reserved price — otherwise the ABI could be probed for free.
|
|
func refuse(t *testing.T, input []byte, want string) {
|
|
t.Helper()
|
|
ret, rem, err := call(input)
|
|
require.ErrorContains(t, err, want)
|
|
require.Nil(t, ret, "a refusal must produce no output")
|
|
require.Equal(t, surplus, rem, "a refusal must still charge exactly RequiredGas")
|
|
}
|
|
|
|
func decode(b []byte) []uint32 {
|
|
out := make([]uint32, len(b)/4)
|
|
for i := range out {
|
|
out[i] = binary.BigEndian.Uint32(b[i*4:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func gasFor(nNew uint32, prompt []uint32) uint64 {
|
|
return InferencePrecompile.RequiredGas(encodeGenerate(nNew, prompt))
|
|
}
|
|
|
|
func tokens(n int, vocab uint32) []uint32 {
|
|
p := make([]uint32, n)
|
|
for i := range p {
|
|
p[i] = uint32(i) % vocab
|
|
}
|
|
return p
|
|
}
|
|
|
|
// ---- Run: every refusal ------------------------------------------------------------
|
|
|
|
func TestRunRefusals(t *testing.T) {
|
|
vocab := fixtureModel().Cfg.Vocab
|
|
selectorOnly := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(selectorOnly, SelectorGenerate)
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
input []byte
|
|
want string
|
|
}{
|
|
{"no input at all", nil, "input too short"},
|
|
{"one byte short of a selector", []byte{0, 0, 0}, "input too short"},
|
|
{"unknown selector", []byte{0xde, 0xad, 0xbe, 0xef}, "unknown selector"},
|
|
{"selector with nothing after it", selectorOnly, "input too short for generate"},
|
|
{"token count but no prompt", encodeGenerate(1, nil), "input too short for generate"},
|
|
{"one byte short of a token", append(encodeGenerate(1, nil), 0, 0, 0), "input too short for generate"},
|
|
{"prompt is one byte past a token", append(encodeGenerate(0, []uint32{1}), 0), "not a multiple of 4"},
|
|
{"prompt is half a token past a token", append(encodeGenerate(0, []uint32{1}), 0, 0), "not a multiple of 4"},
|
|
{"prompt is three bytes past a token", append(encodeGenerate(0, []uint32{1}), 0, 0, 0), "not a multiple of 4"},
|
|
{"one token past the generation limit", encodeGenerate(MaxNewTokens+1, []uint32{1}), "nNew 65 exceeds max"},
|
|
{"generation limit at the uint32 ceiling", encodeGenerate(math.MaxUint32, []uint32{1}), "nNew 4294967295 exceeds max"},
|
|
{"one token past the sequence limit", encodeGenerate(0, tokens(MaxSeqLen+1, vocab)), "sequence 257 exceeds max"},
|
|
{"prompt plus generation past the sequence limit", encodeGenerate(MaxNewTokens, tokens(MaxSeqLen-int(MaxNewTokens)+1, vocab)), "sequence 257 exceeds max"},
|
|
{"token id at the vocab ceiling", encodeGenerate(0, []uint32{1, vocab}), "token 48 out of vocab"},
|
|
{"token id far past the vocab", encodeGenerate(0, []uint32{math.MaxUint32}), "token 4294967295 out of vocab"},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) { refuse(t, c.input, c.want) })
|
|
}
|
|
}
|
|
|
|
// TestRunAcceptsTheLastLegalToken pins the other side of the vocab bound: the check is
|
|
// >= vocab, so vocab-1 must still decode.
|
|
func TestRunAcceptsTheLastLegalToken(t *testing.T) {
|
|
vocab := fixtureModel().Cfg.Vocab
|
|
ret, _, err := call(encodeGenerate(0, []uint32{vocab - 1}))
|
|
require.NoError(t, err)
|
|
require.Equal(t, []uint32{vocab - 1}, decode(ret))
|
|
}
|
|
|
|
// TestRunChargesBeforeItParses: the gas check must come first, or a caller could
|
|
// discover the ABI's refusals without paying for them.
|
|
func TestRunChargesBeforeItParses(t *testing.T) {
|
|
ret, rem, err := InferencePrecompile.Run(nil, common.Address{}, common.Address{}, []byte{0xde, 0xad, 0xbe, 0xef}, 0, false)
|
|
require.ErrorContains(t, err, "out of gas")
|
|
require.Nil(t, ret)
|
|
require.Zero(t, rem)
|
|
}
|
|
|
|
// TestRunGasBoundary: one gas short reverts, exactly enough succeeds and leaves nothing.
|
|
func TestRunGasBoundary(t *testing.T) {
|
|
input := encodeGenerate(1, []uint32{1})
|
|
need := InferencePrecompile.RequiredGas(input)
|
|
|
|
ret, rem, err := InferencePrecompile.Run(nil, common.Address{}, common.Address{}, input, need-1, false)
|
|
require.ErrorContains(t, err, "out of gas")
|
|
require.Nil(t, ret)
|
|
require.Zero(t, rem, "an out-of-gas revert refunds nothing")
|
|
|
|
ret, rem, err = InferencePrecompile.Run(nil, common.Address{}, common.Address{}, input, need, false)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, ret)
|
|
require.Zero(t, rem)
|
|
}
|
|
|
|
// ---- the gas schedule --------------------------------------------------------------
|
|
|
|
// TestRequiredGasMonotonic: one more requested token always costs more, and a longer
|
|
// prompt is never cheaper. The price ignores the prompt today, so the second half holds
|
|
// as an equality; it is written as an inequality so it still holds if the schedule
|
|
// starts charging for the prompt, which is quadratic work the price does not yet see.
|
|
func TestRequiredGasMonotonic(t *testing.T) {
|
|
prompt := []uint32{1}
|
|
prev := gasFor(0, prompt)
|
|
require.Equal(t, InferencePrecompile.RequiredGas(nil), prev,
|
|
"zero new tokens must cost the same as an unparseable input: the base is the floor")
|
|
|
|
for n := uint32(1); n <= MaxNewTokens; n++ {
|
|
g := gasFor(n, prompt)
|
|
require.Greater(t, g, prev, "asking for one more token must cost more")
|
|
prev = g
|
|
}
|
|
|
|
for _, n := range []uint32{0, 1, MaxNewTokens} {
|
|
require.GreaterOrEqual(t, gasFor(n, tokens(200, 48)), gasFor(n, prompt),
|
|
"a longer prompt must never be cheaper than a short one")
|
|
}
|
|
}
|
|
|
|
// TestRequiredGasFloorsAtTheBase: anything too short to carry a token count is priced
|
|
// at the base, and that base is the cheapest the precompile ever is.
|
|
func TestRequiredGasFloorsAtTheBase(t *testing.T) {
|
|
base := InferencePrecompile.RequiredGas(nil)
|
|
for n := 0; n < 8; n++ {
|
|
require.Equal(t, base, InferencePrecompile.RequiredGas(make([]byte, n)),
|
|
"input with no readable token count must be priced at the base")
|
|
}
|
|
require.Equal(t, base, gasFor(0, []uint32{1}))
|
|
for n := uint32(0); n <= MaxNewTokens; n++ {
|
|
require.GreaterOrEqual(t, gasFor(n, []uint32{1}), base)
|
|
}
|
|
}
|
|
|
|
// TestRequiredGasSaturates: past the generation limit the price stops growing, so an
|
|
// absurd token count cannot be used to reserve an absurd amount of gas.
|
|
func TestRequiredGasSaturates(t *testing.T) {
|
|
atMax := gasFor(MaxNewTokens, []uint32{1})
|
|
for _, n := range []uint32{MaxNewTokens + 1, MaxNewTokens + 2, 1000, math.MaxUint32 / 2, math.MaxUint32} {
|
|
require.Equal(t, atMax, gasFor(n, []uint32{1}), "price must saturate at the generation limit")
|
|
}
|
|
}
|
|
|
|
// TestClampedPriceAlwaysReverts is the invariant that keeps the price and the work
|
|
// honest. RequiredGas CLAMPS nNew to MaxNewTokens; Run REJECTS it. Those two must
|
|
// never disagree — every request that receives the clamped price must revert, so a
|
|
// caller can never be quoted for 64 tokens and served more.
|
|
func TestClampedPriceAlwaysReverts(t *testing.T) {
|
|
prompt := []uint32{1}
|
|
atMax := gasFor(MaxNewTokens, prompt)
|
|
|
|
for _, n := range []uint32{MaxNewTokens + 1, MaxNewTokens + 2, 100, 1000, math.MaxUint32} {
|
|
input := encodeGenerate(n, prompt)
|
|
require.Equal(t, atMax, InferencePrecompile.RequiredGas(input), "the price is clamped here")
|
|
ret, _, err := call(input)
|
|
require.Error(t, err, "a clamped price must buy no work")
|
|
require.Nil(t, ret)
|
|
}
|
|
|
|
// The boundary itself is not clamped and not rejected: nNew == MaxNewTokens clears
|
|
// the generation gate and fails later, on the vocab check.
|
|
refuse(t, encodeGenerate(MaxNewTokens, []uint32{fixtureModel().Cfg.Vocab}), "out of vocab")
|
|
}
|
|
|
|
// ---- Run: the successful shape -----------------------------------------------------
|
|
|
|
// TestRunReturnsPromptThenGeneration: the reply is the prompt verbatim followed by the
|
|
// requested tokens, so a caller can locate the generated span without re-encoding.
|
|
func TestRunReturnsPromptThenGeneration(t *testing.T) {
|
|
prompt := []uint32{1, 7, 13, 2}
|
|
|
|
ret, _, err := call(encodeGenerate(0, prompt))
|
|
require.NoError(t, err)
|
|
require.Equal(t, prompt, decode(ret), "zero new tokens echoes the prompt untouched")
|
|
|
|
ret, _, err = call(encodeGenerate(2, prompt))
|
|
require.NoError(t, err)
|
|
got := decode(ret)
|
|
require.Len(t, got, len(prompt)+2)
|
|
require.Equal(t, prompt, got[:len(prompt)], "the prompt prefix must survive generation")
|
|
}
|
|
|
|
// TestRunAtTheSequenceLimit: a prompt that exactly fills the window is accepted, one
|
|
// token more is not.
|
|
func TestRunAtTheSequenceLimit(t *testing.T) {
|
|
vocab := fixtureModel().Cfg.Vocab
|
|
full := tokens(MaxSeqLen, vocab)
|
|
|
|
ret, _, err := call(encodeGenerate(0, full))
|
|
require.NoError(t, err)
|
|
require.Equal(t, full, decode(ret))
|
|
|
|
refuse(t, encodeGenerate(0, tokens(MaxSeqLen+1, vocab)), "sequence 257 exceeds max")
|
|
}
|
|
|
|
// TestRunIsByteIdentical: the precompile is on the consensus path, so repeated
|
|
// invocations of the same input must not differ by a single byte or a single gas.
|
|
func TestRunIsByteIdentical(t *testing.T) {
|
|
input := encodeGenerate(3, []uint32{1, 7, 13, 2})
|
|
first, rem, err := call(input)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, first)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
again, rem2, err2 := call(input)
|
|
require.NoError(t, err2)
|
|
require.Equal(t, first, again, "the consensus path must be byte-identical across invocations")
|
|
require.Equal(t, rem, rem2, "and must charge identically")
|
|
}
|
|
}
|
|
|
|
// ---- module + config ---------------------------------------------------------------
|
|
|
|
func TestModuleWiring(t *testing.T) {
|
|
byAddr, ok := modules.GetPrecompileModuleByAddress(ContractAddress)
|
|
require.True(t, ok, "init must have registered the module")
|
|
byKey, ok := modules.GetPrecompileModule(ConfigKey)
|
|
require.True(t, ok, "the config key must resolve to the same module")
|
|
require.Equal(t, byAddr.Address, byKey.Address)
|
|
require.Equal(t, byAddr.ConfigKey, byKey.ConfigKey)
|
|
require.Same(t, InferencePrecompile, byAddr.Contract, "the registry must hold the singleton")
|
|
require.Equal(t, byAddr.ConfigKey, byAddr.Configurator.MakeConfig().Key(),
|
|
"the configurator must mint configs under the key the registry indexes by")
|
|
|
|
// init panics if registration fails; prove the collision it guards against is real.
|
|
require.Error(t, modules.RegisterModule(Module), "a second registration at this address must be refused")
|
|
}
|
|
|
|
// TestMakeConfigIsFreshAndUnset: every call hands back its own unscheduled config. A
|
|
// shared value would leak one chain's upgrade into the next.
|
|
func TestMakeConfigIsFreshAndUnset(t *testing.T) {
|
|
first, ok := Module.Configurator.MakeConfig().(*Config)
|
|
require.True(t, ok, "MakeConfig must mint this precompile's config type")
|
|
require.Nil(t, first.Timestamp(), "a fresh config is not scheduled")
|
|
require.False(t, first.IsDisabled())
|
|
require.NoError(t, first.Verify(nil))
|
|
|
|
first.Upgrade = precompileconfig.Upgrade{BlockTimestamp: ptr(9), Disable: true}
|
|
second := Module.Configurator.MakeConfig()
|
|
require.Nil(t, second.Timestamp(), "a later config must not inherit an earlier one")
|
|
require.False(t, second.IsDisabled())
|
|
require.True(t, second.Equal(Module.Configurator.MakeConfig()), "two fresh configs are equal")
|
|
}
|
|
|
|
// TestConfigReportsTheUpgradeItHolds: Key, Timestamp and IsDisabled must read the
|
|
// embedded upgrade rather than answer from a constant.
|
|
func TestConfigReportsTheUpgradeItHolds(t *testing.T) {
|
|
c := new(Config)
|
|
require.Equal(t, Module.ConfigKey, c.Key())
|
|
require.Nil(t, c.Timestamp())
|
|
require.False(t, c.IsDisabled())
|
|
|
|
for _, at := range []uint64{0, 7, 1_700_000_000} {
|
|
c.Upgrade.BlockTimestamp = ptr(at)
|
|
require.Equal(t, at, *c.Timestamp())
|
|
}
|
|
|
|
c.Upgrade.Disable = true
|
|
require.True(t, c.IsDisabled())
|
|
require.NoError(t, c.Verify(nil), "a disabled upgrade still verifies")
|
|
require.Equal(t, Module.ConfigKey, c.Key(), "the key does not move with the upgrade")
|
|
}
|
|
|
|
// TestConfigEqualComparesEveryField mutates one field at a time and requires equality
|
|
// to break in both directions.
|
|
func TestConfigEqualComparesEveryField(t *testing.T) {
|
|
// Equal forwards the whole embedded upgrade, so the exhaustiveness claim below is
|
|
// only true for these shapes. A new field must teach Equal about itself.
|
|
require.Equal(t, 1, reflect.TypeOf(Config{}).NumField(), "new Config field: teach Equal about it")
|
|
require.Equal(t, 2, reflect.TypeOf(precompileconfig.Upgrade{}).NumField(), "new Upgrade field: teach Equal about it")
|
|
|
|
base := func() *Config { return &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: ptr(100)}} }
|
|
// Distinct pointers holding the same instant: Equal compares values, not addresses.
|
|
require.True(t, base().Equal(base()))
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
other *Config
|
|
}{
|
|
{"timestamp differs", &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: ptr(101)}}},
|
|
{"timestamp unset", &Config{}},
|
|
{"disable differs", &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: ptr(100), Disable: true}}},
|
|
{"both differ", &Config{Upgrade: precompileconfig.Upgrade{Disable: true}}},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
require.False(t, base().Equal(c.other))
|
|
require.False(t, c.other.Equal(base()), "Equal must be symmetric")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigEqualRejectsAForeignConfig: the foreign config carries an identical
|
|
// upgrade, so only the type can tell the two apart.
|
|
func TestConfigEqualRejectsAForeignConfig(t *testing.T) {
|
|
c := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: ptr(100)}}
|
|
require.False(t, c.Equal(nil), "a missing config is not this config")
|
|
require.False(t, c.Equal(&foreign{Upgrade: c.Upgrade}), "another precompile's config is not this config")
|
|
require.True(t, c.Equal(&Config{Upgrade: c.Upgrade}), "but the same config still is")
|
|
}
|
|
|
|
// TestConfigureTouchesNoState: this precompile is pure compute. Any StateDB or block
|
|
// call would reach the embedded nil interface and panic.
|
|
func TestConfigureTouchesNoState(t *testing.T) {
|
|
cfg := Module.Configurator.MakeConfig()
|
|
require.NoError(t, Module.Configurator.Configure(nil, cfg, deadState{}, deadBlock{}))
|
|
}
|
|
|
|
// ---- helpers -----------------------------------------------------------------------
|
|
|
|
func ptr(v uint64) *uint64 { return &v }
|
|
|
|
// foreign is another precompile's config: a different type carrying the same upgrade.
|
|
type foreign struct{ precompileconfig.Upgrade }
|
|
|
|
func (*foreign) Key() string { return "someOtherPrecompileConfig" }
|
|
func (*foreign) Verify(precompileconfig.ChainConfig) error { return nil }
|
|
func (*foreign) Equal(cfg precompileconfig.Config) bool { _, ok := cfg.(*foreign); return ok }
|
|
|
|
// deadState and deadBlock panic on any use: they hold a nil interface.
|
|
type deadState struct{ contract.StateDB }
|
|
|
|
type deadBlock struct {
|
|
contract.ConfigurationBlockContext
|
|
}
|