mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
325 lines
11 KiB
Go
325 lines
11 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package anchor
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
ethtypes "github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/luxfi/precompile/precompileconfig"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- mock infrastructure ---
|
|
|
|
type testStateDB struct {
|
|
state map[common.Address]map[common.Hash]common.Hash
|
|
}
|
|
|
|
func newTestStateDB() *testStateDB {
|
|
return &testStateDB{state: make(map[common.Address]map[common.Hash]common.Hash)}
|
|
}
|
|
|
|
func (m *testStateDB) GetState(addr common.Address, key common.Hash) common.Hash {
|
|
if m.state[addr] == nil {
|
|
return common.Hash{}
|
|
}
|
|
return m.state[addr][key]
|
|
}
|
|
func (m *testStateDB) SetState(addr common.Address, key, val common.Hash) common.Hash {
|
|
if m.state[addr] == nil {
|
|
m.state[addr] = make(map[common.Hash]common.Hash)
|
|
}
|
|
old := m.state[addr][key]
|
|
m.state[addr][key] = val
|
|
return old
|
|
}
|
|
func (m *testStateDB) SetNonce(common.Address, uint64, tracing.NonceChangeReason) {}
|
|
func (m *testStateDB) GetNonce(common.Address) uint64 { return 0 }
|
|
func (m *testStateDB) GetBalance(common.Address) *uint256.Int { return new(uint256.Int) }
|
|
func (m *testStateDB) AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
|
|
return uint256.Int{}
|
|
}
|
|
func (m *testStateDB) SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int {
|
|
return uint256.Int{}
|
|
}
|
|
func (m *testStateDB) GetBalanceMultiCoin(common.Address, common.Hash) *big.Int { return big.NewInt(0) }
|
|
func (m *testStateDB) AddBalanceMultiCoin(common.Address, common.Hash, *big.Int) {}
|
|
func (m *testStateDB) SubBalanceMultiCoin(common.Address, common.Hash, *big.Int) {}
|
|
func (m *testStateDB) CreateAccount(common.Address) {}
|
|
func (m *testStateDB) Exist(common.Address) bool { return false }
|
|
func (m *testStateDB) AddLog(*ethtypes.Log) {}
|
|
func (m *testStateDB) Logs() []*ethtypes.Log { return nil }
|
|
func (m *testStateDB) GetPredicateStorageSlots(common.Address, int) ([]byte, bool) { return nil, false }
|
|
func (m *testStateDB) TxHash() common.Hash { return common.Hash{} }
|
|
func (m *testStateDB) Snapshot() int { return 0 }
|
|
func (m *testStateDB) RevertToSnapshot(int) {}
|
|
|
|
var _ contract.StateDB = (*testStateDB)(nil)
|
|
|
|
type testBlockCtx struct{}
|
|
|
|
func (t *testBlockCtx) Number() *big.Int { return big.NewInt(1) }
|
|
func (t *testBlockCtx) Timestamp() uint64 { return 1000 }
|
|
func (t *testBlockCtx) GetPredicateResults(common.Hash, common.Address) []byte { return nil }
|
|
|
|
var _ contract.BlockContext = (*testBlockCtx)(nil)
|
|
|
|
type testChainCfg struct{}
|
|
|
|
func (t *testChainCfg) IsDurango(uint64) bool { return true }
|
|
|
|
type testPrecompileEnv struct{}
|
|
|
|
func (t *testPrecompileEnv) ReadOnly() bool { return false }
|
|
|
|
type testAS struct{ db *testStateDB }
|
|
|
|
func (t *testAS) GetStateDB() contract.StateDB { return t.db }
|
|
func (t *testAS) GetBlockContext() contract.BlockContext { return &testBlockCtx{} }
|
|
func (t *testAS) GetConsensusContext() context.Context { return context.Background() }
|
|
func (t *testAS) GetChainConfig() precompileconfig.ChainConfig { return &testChainCfg{} }
|
|
func (t *testAS) GetPrecompileEnv() contract.PrecompileEnvironment { return &testPrecompileEnv{} }
|
|
|
|
var _ contract.AccessibleState = (*testAS)(nil)
|
|
|
|
func newTestAS() *testAS { return &testAS{db: newTestStateDB()} }
|
|
|
|
// --- helpers ---
|
|
|
|
func abiUint64(v uint64) []byte {
|
|
b := make([]byte, 32)
|
|
binary.BigEndian.PutUint64(b[24:], v)
|
|
return b
|
|
}
|
|
|
|
// --- Config ---
|
|
|
|
type fakeConfig struct{}
|
|
|
|
func (f *fakeConfig) Key() string { return "fake" }
|
|
func (f *fakeConfig) Timestamp() *uint64 { return nil }
|
|
func (f *fakeConfig) IsDisabled() bool { return false }
|
|
func (f *fakeConfig) Equal(precompileconfig.Config) bool { return false }
|
|
func (f *fakeConfig) Verify(precompileconfig.ChainConfig) error { return nil }
|
|
|
|
func TestConfigLifecycle(t *testing.T) {
|
|
cfg := &Config{}
|
|
require.Equal(t, ConfigKey, cfg.Key())
|
|
require.Nil(t, cfg.Timestamp())
|
|
require.False(t, cfg.IsDisabled())
|
|
require.NoError(t, cfg.Verify(&testChainCfg{}))
|
|
require.True(t, cfg.Equal(&Config{}))
|
|
require.False(t, cfg.Equal(&fakeConfig{}))
|
|
|
|
ts := uint64(42)
|
|
cfg2 := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
|
require.Equal(t, &ts, cfg2.Timestamp())
|
|
|
|
cfg3 := &Config{Upgrade: precompileconfig.Upgrade{Disable: true}}
|
|
require.True(t, cfg3.IsDisabled())
|
|
}
|
|
|
|
func TestConfigurator(t *testing.T) {
|
|
c := &configurator{}
|
|
cfg := c.MakeConfig()
|
|
require.NotNil(t, cfg)
|
|
require.NoError(t, c.Configure(&testChainCfg{}, cfg, newTestStateDB(), &testBlockCtx{}))
|
|
}
|
|
|
|
func TestModuleRegistered(t *testing.T) {
|
|
require.Equal(t, ConfigKey, Module.ConfigKey)
|
|
}
|
|
|
|
// --- Run: submit edge cases ---
|
|
|
|
func TestSubmitOutOfGas(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorSubmit, make([]byte, 96)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100, false)
|
|
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
|
}
|
|
|
|
func TestSubmitShortData(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorSubmit, make([]byte, 10)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInputTooShort)
|
|
}
|
|
|
|
// --- Run: get edge cases ---
|
|
|
|
func TestGetOutOfGas(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorGet, make([]byte, 64)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100, false)
|
|
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
|
}
|
|
|
|
func TestGetShortData(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorGet, make([]byte, 10)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInputTooShort)
|
|
}
|
|
|
|
// --- Run: getLatest edge cases ---
|
|
|
|
func TestGetLatestOutOfGas(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorGetLatest, make([]byte, 32)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100, false)
|
|
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
|
}
|
|
|
|
func TestGetLatestShortData(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorGetLatest, make([]byte, 10)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInputTooShort)
|
|
}
|
|
|
|
// --- Run: list edge cases ---
|
|
|
|
func TestListOutOfGas(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorList, make([]byte, 96)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100, false)
|
|
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
|
}
|
|
|
|
func TestListShortData(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
input := append(selectorList, make([]byte, 10)...)
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInputTooShort)
|
|
}
|
|
|
|
func TestListToBeforeFrom(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
state := newTestAS()
|
|
appID := [32]byte{0x01}
|
|
data := make([]byte, 96)
|
|
copy(data[:32], appID[:])
|
|
binary.BigEndian.PutUint64(data[56:64], 10) // from=10
|
|
binary.BigEndian.PutUint64(data[88:96], 5) // to=5 (invalid)
|
|
input := append(selectorList, data...)
|
|
_, _, err := p.Run(state, common.Address{}, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInputTooShort)
|
|
}
|
|
|
|
func TestListRangeTooLarge(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
state := newTestAS()
|
|
appID := [32]byte{0x01}
|
|
data := make([]byte, 96)
|
|
copy(data[:32], appID[:])
|
|
binary.BigEndian.PutUint64(data[56:64], 1) // from=1
|
|
binary.BigEndian.PutUint64(data[88:96], 1000) // to=1000 (range=999 > 256)
|
|
input := append(selectorList, data...)
|
|
_, _, err := p.Run(state, common.Address{}, ContractAddress, input, 10000000, false)
|
|
require.ErrorIs(t, err, ErrRangeTooLarge)
|
|
}
|
|
|
|
func TestListInsufficientGasForRange(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
state := newTestAS()
|
|
appID := [32]byte{0x01}
|
|
data := make([]byte, 96)
|
|
copy(data[:32], appID[:])
|
|
binary.BigEndian.PutUint64(data[56:64], 1)
|
|
binary.BigEndian.PutUint64(data[88:96], 100) // 99 entries
|
|
input := append(selectorList, data...)
|
|
// Base gas is enough but range gas is not
|
|
_, _, err := p.Run(state, common.Address{}, ContractAddress, input, GasListBase+10, false)
|
|
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
|
}
|
|
|
|
// --- Run: invalid selector ---
|
|
|
|
func TestRunInvalidSelector(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
_, _, err := p.Run(newTestAS(), common.Address{}, ContractAddress, []byte{0xFF, 0xFF, 0xFF, 0xFF}, 100000, false)
|
|
require.ErrorIs(t, err, ErrInvalidSelector)
|
|
}
|
|
|
|
// --- Full lifecycle: submit + get + getLatest + list ---
|
|
|
|
func TestFullLifecycle(t *testing.T) {
|
|
p := &anchorPrecompile{}
|
|
state := newTestAS()
|
|
caller := common.HexToAddress("0x1234")
|
|
|
|
appID := [32]byte{0x42}
|
|
root1 := [32]byte{0xAA}
|
|
root2 := [32]byte{0xBB}
|
|
|
|
// Submit height 1
|
|
// Layout: appID(32) + padding(24) + height(8) + root(32) = 96 bytes
|
|
data := make([]byte, 96)
|
|
copy(data[:32], appID[:])
|
|
binary.BigEndian.PutUint64(data[56:64], 1) // height=1 at offset 56 (after 24-byte ABI pad)
|
|
copy(data[64:96], root1[:])
|
|
input := append(selectorSubmit, data...)
|
|
ret, _, err := p.Run(state, caller, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ret)
|
|
|
|
// Submit height 2
|
|
data2 := make([]byte, 96)
|
|
copy(data2[:32], appID[:])
|
|
binary.BigEndian.PutUint64(data2[56:64], 2)
|
|
copy(data2[64:96], root2[:])
|
|
input2 := append(selectorSubmit, data2...)
|
|
_, _, err = p.Run(state, caller, ContractAddress, input2, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
// Submit non-monotonic (height 1 again) should fail
|
|
_, _, err = p.Run(state, caller, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrHeightNotMonotonic)
|
|
|
|
// Get height 1
|
|
getData := make([]byte, 64)
|
|
copy(getData[:32], appID[:])
|
|
binary.BigEndian.PutUint64(getData[56:64], 1)
|
|
getInput := append(selectorGet, getData...)
|
|
ret, _, err = p.Run(state, caller, ContractAddress, getInput, 100000, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, root1[:], ret)
|
|
|
|
// Get height 999 (not found)
|
|
getData2 := make([]byte, 64)
|
|
copy(getData2[:32], appID[:])
|
|
binary.BigEndian.PutUint64(getData2[56:64], 999)
|
|
getInput2 := append(selectorGet, getData2...)
|
|
_, _, err = p.Run(state, caller, ContractAddress, getInput2, 100000, false)
|
|
require.ErrorIs(t, err, ErrAnchorNotFound)
|
|
|
|
// GetLatest
|
|
latestData := make([]byte, 32)
|
|
copy(latestData[:32], appID[:])
|
|
latestInput := append(selectorGetLatest, latestData...)
|
|
ret, _, err = p.Run(state, caller, ContractAddress, latestInput, 100000, false)
|
|
require.NoError(t, err)
|
|
h := binary.BigEndian.Uint64(ret[24:32])
|
|
require.Equal(t, uint64(2), h)
|
|
|
|
// List [1, 3)
|
|
listData := make([]byte, 96)
|
|
copy(listData[:32], appID[:])
|
|
binary.BigEndian.PutUint64(listData[56:64], 1)
|
|
binary.BigEndian.PutUint64(listData[88:96], 3)
|
|
listInput := append(selectorList, listData...)
|
|
ret, _, err = p.Run(state, caller, ContractAddress, listInput, 100000, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ret)
|
|
}
|