Clean up project

This commit is contained in:
2025-07-24 00:41:34 -05:00
parent 065c36213e
commit 30986b4f91
167 changed files with 45 additions and 37255 deletions
+14 -14
View File
@@ -13,19 +13,19 @@ import (
// Context is the context for the chain
type Context struct {
NetworkID uint32
SubnetID ids.ID
ChainID ids.ID
NodeID ids.NodeID
PublicKey *bls.PublicKey
XChainID ids.ID
CChainID ids.ID
AVAXAssetID ids.ID
Log logging.Logger
Metrics metrics.MultiGatherer
ChainDataDir string
AliasManager AliasManager
Validators ValidatorManager
NetworkID uint32
SubnetID ids.ID
ChainID ids.ID
NodeID ids.NodeID
PublicKey *bls.PublicKey
XChainID ids.ID
CChainID ids.ID
LUXAssetID ids.ID
Log logging.Logger
Metrics metrics.MultiGatherer
ChainDataDir string
AliasManager AliasManager
Validators ValidatorManager
}
// AliasManager interface
@@ -45,4 +45,4 @@ type ValidatorManager interface {
type Validator struct {
NodeID ids.NodeID
Weight uint64
}
}
-461
View File
@@ -1,461 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package core
import (
"errors"
"testing"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/node/utils/set"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type predicateCheckTest struct {
accessList types.AccessList
gas uint64
predicateContext *precompileconfig.PredicateContext
createPredicates func(t testing.TB) map[common.Address]precompileconfig.Predicater
expectedRes map[common.Address][]byte
expectedErr error
}
func TestCheckPredicate(t *testing.T) {
testErr := errors.New("test error")
addr1 := common.HexToAddress("0xaa")
addr2 := common.HexToAddress("0xbb")
addr3 := common.HexToAddress("0xcc")
addr4 := common.HexToAddress("0xdd")
predicateContext := &precompileconfig.PredicateContext{
ProposerVMBlockCtx: &block.Context{
PChainHeight: 10,
},
}
for name, test := range map[string]predicateCheckTest{
"no predicates, no access list, no context passes": {
gas: 53000,
predicateContext: nil,
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"no predicates, no access list, with context passes": {
gas: 53000,
predicateContext: predicateContext,
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"no predicates, with access list, no context passes": {
gas: 57300,
predicateContext: nil,
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"predicate, no access list, no context passes": {
gas: 53000,
predicateContext: nil,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"predicate, no access list, no block context passes": {
gas: 53000,
predicateContext: &precompileconfig.PredicateContext{
ProposerVMBlockCtx: nil,
},
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"predicate named by access list, without context errors": {
gas: 53000,
predicateContext: nil,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(0), nil).Times(1)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedErr: ErrMissingPredicateContext,
},
"predicate named by access list, without block context errors": {
gas: 53000,
predicateContext: &precompileconfig.PredicateContext{
ProposerVMBlockCtx: nil,
},
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(0), nil).Times(1)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedErr: ErrMissingPredicateContext,
},
"predicate named by access list returns non-empty": {
gas: 53000,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(0), nil).Times(2)
predicater.EXPECT().VerifyPredicate(gomock.Any(), arg[:]).Return(nil)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedRes: map[common.Address][]byte{
addr1: {}, // valid bytes
},
expectedErr: nil,
},
"predicate returns gas err": {
gas: 53000,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(0), testErr)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedErr: testErr,
},
"two predicates one named by access list returns non-empty": {
gas: 53000,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(0), nil).Times(2)
predicater.EXPECT().VerifyPredicate(gomock.Any(), arg[:]).Return(nil)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
addr2: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedRes: map[common.Address][]byte{
addr1: {}, // valid bytes
},
expectedErr: nil,
},
"two predicates both named by access list returns non-empty": {
gas: 53000,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
ctrl := gomock.NewController(t)
predicate1 := precompileconfig.NewMockPredicater(ctrl)
arg1 := common.Hash{1}
predicate1.EXPECT().PredicateGas(arg1[:]).Return(uint64(0), nil).Times(2)
predicate1.EXPECT().VerifyPredicate(gomock.Any(), arg1[:]).Return(nil)
predicate2 := precompileconfig.NewMockPredicater(ctrl)
arg2 := common.Hash{2}
predicate2.EXPECT().PredicateGas(arg2[:]).Return(uint64(0), nil).Times(2)
predicate2.EXPECT().VerifyPredicate(gomock.Any(), arg2[:]).Return(testErr)
return map[common.Address]precompileconfig.Predicater{
addr1: predicate1,
addr2: predicate2,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
{
Address: addr2,
StorageKeys: []common.Hash{
{2},
},
},
}),
expectedRes: map[common.Address][]byte{
addr1: {}, // valid bytes
addr2: {1}, // invalid bytes
},
expectedErr: nil,
},
"two predicates neither named by access list": {
gas: 61600,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
addr2: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr3,
StorageKeys: []common.Hash{
{1},
},
},
{
Address: addr4,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedRes: make(map[common.Address][]byte),
expectedErr: nil,
},
"insufficient gas": {
gas: 53000,
predicateContext: predicateContext,
createPredicates: func(t testing.TB) map[common.Address]precompileconfig.Predicater {
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
arg := common.Hash{1}
predicater.EXPECT().PredicateGas(arg[:]).Return(uint64(1), nil)
return map[common.Address]precompileconfig.Predicater{
addr1: predicater,
}
},
accessList: types.AccessList([]types.AccessTuple{
{
Address: addr1,
StorageKeys: []common.Hash{
{1},
},
},
}),
expectedErr: ErrIntrinsicGas,
},
} {
t.Run(name, func(t *testing.T) {
require := require.New(t)
// Create the rules from TestChainConfig and update the predicates based on the test params
rules := params.TestChainConfig.LuxRules(common.Big0, 0)
if test.createPredicates != nil {
for address, predicater := range test.createPredicates(t) {
rules := params.GetRulesExtra(rules)
rules.Predicaters[address] = predicater
}
}
// Specify only the access list, since this test should not depend on any other values
tx := types.NewTx(&types.DynamicFeeTx{
AccessList: test.accessList,
Gas: test.gas,
})
predicateRes, err := CheckPredicates(rules, test.predicateContext, tx)
require.ErrorIs(err, test.expectedErr)
if test.expectedErr != nil {
return
}
require.Equal(test.expectedRes, predicateRes)
intrinsicGas, err := IntrinsicGas(tx.Data(), tx.AccessList(), true, rules)
require.NoError(err)
require.Equal(tx.Gas(), intrinsicGas) // Require test specifies exact amount of gas consumed
})
}
}
func TestCheckPredicatesOutput(t *testing.T) {
testErr := errors.New("test error")
addr1 := common.HexToAddress("0xaa")
addr2 := common.HexToAddress("0xbb")
validHash := common.Hash{1}
invalidHash := common.Hash{2}
predicateContext := &precompileconfig.PredicateContext{
ProposerVMBlockCtx: &block.Context{
PChainHeight: 10,
},
}
type testTuple struct {
address common.Address
isValidPredicate bool
}
type resultTest struct {
name string
expectedRes map[common.Address][]byte
testTuple []testTuple
}
tests := []resultTest{
{name: "no predicates", expectedRes: map[common.Address][]byte{}},
{
name: "one address one predicate",
testTuple: []testTuple{
{address: addr1, isValidPredicate: true},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits().Bytes()},
},
{
name: "one address one invalid predicate",
testTuple: []testTuple{
{address: addr1, isValidPredicate: false},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(0).Bytes()},
},
{
name: "one address two invalid predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(0, 1).Bytes()},
},
{
name: "one address two mixed predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: true},
{address: addr1, isValidPredicate: false},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(1).Bytes()},
},
{
name: "one address mixed predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: true},
{address: addr1, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
{address: addr1, isValidPredicate: true},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(1, 2).Bytes()},
},
{
name: "two addresses mixed predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: true},
{address: addr2, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
{address: addr2, isValidPredicate: true},
{address: addr2, isValidPredicate: true},
{address: addr2, isValidPredicate: false},
{address: addr2, isValidPredicate: true},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(1, 2).Bytes(), addr2: set.NewBits(0, 3).Bytes()},
},
{
name: "two addresses all valid predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: true},
{address: addr2, isValidPredicate: true},
{address: addr1, isValidPredicate: true},
{address: addr1, isValidPredicate: true},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits().Bytes(), addr2: set.NewBits().Bytes()},
},
{
name: "two addresses all invalid predicates",
testTuple: []testTuple{
{address: addr1, isValidPredicate: false},
{address: addr2, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
{address: addr1, isValidPredicate: false},
},
expectedRes: map[common.Address][]byte{addr1: set.NewBits(0, 1, 2).Bytes(), addr2: set.NewBits(0).Bytes()},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
// Create the rules from TestChainConfig and update the predicates based on the test params
rules := params.TestChainConfig.LuxRules(common.Big0, 0)
predicater := precompileconfig.NewMockPredicater(gomock.NewController(t))
predicater.EXPECT().PredicateGas(gomock.Any()).Return(uint64(0), nil).Times(len(test.testTuple))
var txAccessList types.AccessList
for _, tuple := range test.testTuple {
var predicateHash common.Hash
if tuple.isValidPredicate {
predicateHash = validHash
predicater.EXPECT().VerifyPredicate(gomock.Any(), validHash[:]).Return(nil)
} else {
predicateHash = invalidHash
predicater.EXPECT().VerifyPredicate(gomock.Any(), invalidHash[:]).Return(testErr)
}
txAccessList = append(txAccessList, types.AccessTuple{
Address: tuple.address,
StorageKeys: []common.Hash{
predicateHash,
},
})
}
rulesExtra := params.GetRulesExtra(rules)
rulesExtra.Predicaters[addr1] = predicater
rulesExtra.Predicaters[addr2] = predicater
// Specify only the access list, since this test should not depend on any other values
tx := types.NewTx(&types.DynamicFeeTx{
AccessList: txAccessList,
Gas: 53000,
})
predicateRes, err := CheckPredicates(rules, predicateContext, tx)
require.NoError(err)
require.Equal(test.expectedRes, predicateRes)
})
}
}
@@ -1,191 +0,0 @@
// (c) 2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"crypto/rand"
"encoding/binary"
"fmt"
"os"
"path"
"strconv"
"testing"
"github.com/luxfi/node/database"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/geth/ethdb"
"github.com/luxfi/evm/interfaces/metrics"
"github.com/ethereum/go-ethereum/triedb"
"github.com/luxfi/evm/core/state/snapshot"
"github.com/luxfi/evm/triedb/hashdb"
"github.com/stretchr/testify/require"
)
const (
namespace = "chain"
// triePrefetchMetricsPrefix is the prefix for trie prefetcher metrics
// and MUST match upstream for the benchmark to work.
triePrefetchMetricsPrefix = "trie/prefetch/"
)
// BenchmarkPrefetcherDatabase benchmarks the performance of the trie
// prefetcher. By default, a state with 100k storage keys is created and stored
// in a temporary directory. Setting the TEST_DB_KVS and TEST_DB_DIR environment
// variables modifies the defaults. The benchmark measures the time to update
// the trie after 100, 200, and 500 storage slot updates per iteration,
// simulating a block with that number of storage slot updates. For performance
// reasons, when making changes involving the trie prefetcher, this benchmark
// should be run against a state including around 100m storage entries.
func BenchmarkPrefetcherDatabase(b *testing.B) {
require := require.New(b)
dir := b.TempDir()
if env := os.Getenv("TEST_DB_DIR"); env != "" {
dir = env
}
wantKVs := 100_000
if env := os.Getenv("TEST_DB_KVS"); env != "" {
var err error
wantKVs, err = strconv.Atoi(env)
require.NoError(err)
}
levelDB, err := rawdb.NewLevelDBDatabase(path.Join(dir, "level.db"), 0, 0, "", false)
require.NoError(err)
root := types.EmptyRootHash
count := uint64(0)
block := uint64(0)
rootKey := []byte("root")
countKey := []byte("count")
blockKey := []byte("block")
got, err := levelDB.Get(rootKey)
if err == nil { // on success
root = common.BytesToHash(got)
}
got, err = levelDB.Get(countKey)
if err == nil { // on success
count = binary.BigEndian.Uint64(got)
}
got, err = levelDB.Get(blockKey)
if err == nil { // on success
block = binary.BigEndian.Uint64(got)
}
// Make a trie on the levelDB
address1 := common.Address{42}
address2 := common.Address{43}
addBlock := func(db Database, snaps *snapshot.Tree, kvsPerBlock int, prefetchers int) {
_, root, err = addKVs(db, snaps, address1, address2, root, block, kvsPerBlock, prefetchers)
require.NoError(err)
count += uint64(kvsPerBlock)
block++
}
lastCommit := block
commit := func(levelDB ethdb.Database, snaps *snapshot.Tree, db Database) {
require.NoError(db.TrieDB().Commit(root, false))
for i := lastCommit + 1; i <= block; i++ {
require.NoError(snaps.Flatten(fakeHash(i)))
}
lastCommit = block
// update the tracking keys
require.NoError(levelDB.Put(rootKey, root.Bytes()))
require.NoError(database.PutUInt64(levelDB, blockKey, block))
require.NoError(database.PutUInt64(levelDB, countKey, count))
}
tdbConfig := &triedb.Config{
DBOverride: hashdb.Config{
CleanCacheSize: 3 * 1024 * 1024 * 1024,
}.BackendConstructor,
}
db := NewDatabaseWithConfig(levelDB, tdbConfig)
snaps := snapshot.NewTestTree(levelDB, fakeHash(block), root)
for count < uint64(wantKVs) {
previous := root
addBlock(db, snaps, 100_000, 0) // Note this updates root and count
b.Logf("Root: %v, kvs: %d, block: %d", root, count, block)
// Commit every 10 blocks or on the last iteration
if block%10 == 0 || count >= uint64(wantKVs) {
commit(levelDB, snaps, db)
b.Logf("Root: %v, kvs: %d, block: %d (committed)", root, count, block)
}
if previous != root {
require.NoError(db.TrieDB().Dereference(previous))
} else {
b.Fatal("root did not change")
}
}
require.NoError(levelDB.Close())
b.Logf("Starting benchmarks")
b.Logf("Root: %v, kvs: %d, block: %d", root, count, block)
for _, updates := range []int{100, 200, 500} {
for _, prefetchers := range []int{0, 1, 4, 16} {
b.Run(fmt.Sprintf("updates_%d_prefetchers_%d", updates, prefetchers), func(b *testing.B) {
startRoot, startBlock, startCount := root, block, count
defer func() { root, block, count = startRoot, startBlock, startCount }()
levelDB, err := rawdb.NewLevelDBDatabase(path.Join(dir, "level.db"), 0, 0, "", false)
require.NoError(err)
snaps := snapshot.NewTestTree(levelDB, fakeHash(block), root)
db := NewDatabaseWithConfig(levelDB, tdbConfig)
getMetric := func(metric string) int64 {
meter := metrics.GetOrRegisterMeter(triePrefetchMetricsPrefix+namespace+"/storage/"+metric, nil)
return meter.Snapshot().Count()
}
startLoads := getMetric("load")
for i := 0; i < b.N; i++ {
addBlock(db, snaps, updates, prefetchers)
}
require.NoError(levelDB.Close())
b.ReportMetric(float64(getMetric("load")-startLoads)/float64(b.N), "loads")
})
}
}
}
func fakeHash(block uint64) common.Hash {
return common.BytesToHash(binary.BigEndian.AppendUint64(nil, block))
}
// addKVs adds count random key-value pairs to the state trie of address1 and
// address2 (each count/2) and returns the new state db and root.
func addKVs(
db Database, snaps *snapshot.Tree,
address1, address2 common.Address, root common.Hash, block uint64,
count int, prefetchers int,
) (*StateDB, common.Hash, error) {
statedb, err := New(root, db, snaps)
if err != nil {
return nil, common.Hash{}, fmt.Errorf("creating state with snapshot: %w", err)
}
if prefetchers > 0 {
statedb.StartPrefetcher(namespace, nil)
defer statedb.StopPrefetcher()
}
for _, address := range []common.Address{address1, address2} {
statedb.SetNonce(address, 1)
for i := 0; i < count/2; i++ {
key := make([]byte, 32)
value := make([]byte, 32)
rand.Read(key)
rand.Read(value)
statedb.SetState(address, common.BytesToHash(key), common.BytesToHash(value))
}
}
root, err = statedb.Commit(block+1, true)
if err != nil {
return nil, common.Hash{}, fmt.Errorf("committing with snap: %w", err)
}
return statedb, root, nil
}
+3 -3
View File
@@ -36,7 +36,7 @@ import (
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/params"
customheader "github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/plugin/evm/upgrade/acp176"
"github.com/luxfi/evm/plugin/evm/upgrade/lip176"
"github.com/luxfi/geth/rpc"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/common/lru"
@@ -63,8 +63,8 @@ const (
var (
DefaultMaxPrice = big.NewInt(150 * params.GWei)
DefaultMinPrice = big.NewInt(acp176.MinGasPrice)
DefaultMinGasUsed = big.NewInt(acp176.MinTargetPerSecond)
DefaultMinPrice = big.NewInt(lip176.MinGasPrice)
DefaultMinGasUsed = big.NewInt(lip176.MinTargetPerSecond)
DefaultMaxLookbackSeconds = uint64(80)
)
+2 -2
View File
@@ -40,7 +40,7 @@ import (
"github.com/luxfi/geth/core/vm"
"github.com/luxfi/geth/params"
customheader "github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/plugin/evm/upgrade/acp176"
"github.com/luxfi/evm/plugin/evm/upgrade/lip176"
"github.com/luxfi/evm/plugin/evm/upgrade/ap1"
"github.com/luxfi/evm/plugin/evm/upgrade/ap4"
"github.com/luxfi/geth/rpc"
@@ -402,7 +402,7 @@ func TestSuggestTipCapIncludesExtraDataGas(t *testing.T) {
applyGasPriceTest(t, suggestTipCapTest{
chainConfig: params.TestChainConfig,
numBlocks: 1000,
extDataGasUsage: big.NewInt(acp176.MinMaxPerSecond - int64(params.TxGas)),
extDataGasUsage: big.NewInt(lip176.MinMaxPerSecond - int64(params.TxGas)),
// The tip on the transaction is very large to pay the block gas cost.
genBlock: testGenBlock(t, 100_000, 1),
// The actual tip doesn't matter, we just want to ensure that the tip is
-280
View File
@@ -1,280 +0,0 @@
// Copyright 2023 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package simulated
import (
"errors"
"math/big"
"time"
"github.com/luxfi/node/utils/timer/mockable"
ethereum "github.com/luxfi/evm/interfaces"
"github.com/ethereum/go-ethereum/common"
"github.com/luxfi/evm/interfaces/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/constants"
"github.com/luxfi/evm/core"
"github.com/ethereum/go-ethereum/eth"
"github.com/luxfi/evm/geth/eth/ethconfig"
"github.com/luxfi/evm/geth/ethclient"
"github.com/luxfi/evm/interfaces"
"github.com/ethereum/go-ethereum/node"
"github.com/luxfi/evm/params"
"github.com/ethereum/go-ethereum/rpc"
)
var _ eth.PushGossiper = (*fakePushGossiper)(nil)
type fakePushGossiper struct{}
func (*fakePushGossiper) Add(*types.Transaction) {}
// Client exposes the methods provided by the Ethereum RPC client.
type Client interface {
ethereum.BlockNumberReader
ethereum.ChainReader
ethereum.ChainStateReader
ethereum.ContractCaller
ethereum.GasEstimator
ethereum.GasPricer
ethereum.GasPricer1559
ethereum.FeeHistoryReader
ethereum.LogFilterer
interfaces.AcceptedStateReader
interfaces.AcceptedContractCaller
ethereum.TransactionReader
ethereum.TransactionSender
ethereum.ChainIDReader
}
// simClient wraps ethclient. This exists to prevent extracting ethclient.Client
// from the Client interface returned by Backend.
type simClient struct {
ethclient.Client
}
// Backend is a simulated blockchain. You can use it to test your contracts or
// other code that interacts with the Ethereum chain.
type Backend struct {
eth *eth.Ethereum
client simClient
clock *mockable.Clock
server *rpc.Server
}
// NewBackend creates a new simulated blockchain that can be used as a backend for
// contract bindings in unit tests.
//
// A simulated backend always uses chainID 1337.
func NewBackend(alloc types.GenesisAlloc, options ...func(nodeConf *node.Config, ethConf *ethconfig.Config)) *Backend {
chainConfig := params.Copy(params.TestChainConfig)
chainConfig.ChainID = big.NewInt(1337)
// Create the default configurations for the outer node shell and the Ethereum
// service to mutate with the options afterwards
nodeConf := node.DefaultConfig
ethConf := ethconfig.DefaultConfig
ethConf.Genesis = &core.Genesis{
Config: &chainConfig,
GasLimit: params.GetExtra(&chainConfig).FeeConfig.GasLimit.Uint64(),
Alloc: alloc,
}
ethConf.AllowUnfinalizedQueries = true
ethConf.Miner.Etherbase = constants.BlackholeAddr
ethConf.Miner.TestOnlyAllowDuplicateBlocks = true
ethConf.TxPool.NoLocals = true
for _, option := range options {
option(&nodeConf, &ethConf)
}
// Assemble the Ethereum stack to run the chain with
stack, err := node.New(&nodeConf)
if err != nil {
panic(err) // this should never happen
}
sim, err := newWithNode(stack, &ethConf, 0)
if err != nil {
panic(err) // this should never happen
}
return sim
}
// newWithNode sets up a simulated backend on an existing node. The provided node
// must not be started and will be started by this method.
func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backend, error) {
chaindb := rawdb.NewMemoryDatabase()
clock := &mockable.Clock{}
clock.Set(time.Unix(0, 0))
engine := dummy.NewFakerWithModeAndClock(
dummy.Mode{ModeSkipCoinbase: true}, clock,
)
backend, err := eth.New(
stack, conf, &fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{},
engine, clock,
)
if err != nil {
return nil, err
}
server := rpc.NewServer(0)
for _, api := range backend.APIs() {
if err := server.RegisterName(api.Namespace, api.Service); err != nil {
return nil, err
}
}
return &Backend{
eth: backend,
client: simClient{ethclient.NewClient(rpc.DialInProc(server))},
clock: clock,
server: server,
}, nil
}
// Close shuts down the simBackend.
// The simulated backend can't be used afterwards.
func (n *Backend) Close() error {
if n.client.Client != nil {
n.client.Close()
}
n.server.Stop()
return nil
}
// Commit seals a block and moves the chain forward to a new empty block.
func (n *Backend) Commit(accept bool) common.Hash {
hash, err := n.buildBlock(accept, 10)
if err != nil {
panic(err)
}
return hash
}
func (n *Backend) buildBlock(accept bool, gap uint64) (common.Hash, error) {
chain := n.eth.BlockChain()
parent := chain.CurrentBlock()
if err := n.eth.TxPool().Sync(); err != nil {
return common.Hash{}, err
}
n.clock.Set(time.Unix(int64(parent.Time+gap), 0))
block, err := n.eth.Miner().GenerateBlock(nil)
if err != nil {
return common.Hash{}, err
}
if err := chain.InsertBlock(block); err != nil {
return common.Hash{}, err
}
if accept {
if err := n.acceptAncestors(block); err != nil {
return common.Hash{}, err
}
chain.DrainAcceptorQueue()
}
return block.Hash(), nil
}
func (n *Backend) acceptAncestors(block *types.Block) error {
chain := n.eth.BlockChain()
lastAccepted := chain.LastConsensusAcceptedBlock()
// Accept all ancestors of the block
toAccept := []*types.Block{block}
for block.ParentHash() != lastAccepted.Hash() {
block = chain.GetBlockByHash(block.ParentHash())
toAccept = append(toAccept, block)
if block.NumberU64() < lastAccepted.NumberU64() {
return errors.New("last accepted must be an ancestor of the block to accept")
}
}
for i := len(toAccept) - 1; i >= 0; i-- {
if err := chain.Accept(toAccept[i]); err != nil {
return err
}
}
return nil
}
// Rollback removes all pending transactions, reverting to the last committed state.
func (n *Backend) Rollback() {
// Flush all transactions from the transaction pools
maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(common.Big1, 256), common.Big1)
original := n.eth.TxPool().GasTip()
n.eth.TxPool().SetGasTip(maxUint256)
n.eth.TxPool().SetGasTip(original)
}
// Fork creates a side-chain that can be used to simulate reorgs.
//
// This function should be called with the ancestor block where the new side
// chain should be started. Transactions (old and new) can then be applied on
// top and Commit-ed.
//
// Note, the side-chain will only become canonical (and trigger the events) when
// it becomes longer. Until then CallContract will still operate on the current
// canonical chain.
//
// There is a % chance that the side chain becomes canonical at the same length
// to simulate live network behavior.
func (n *Backend) Fork(parentHash common.Hash) error {
chain := n.eth.BlockChain()
if chain.CurrentBlock().Hash() == parentHash {
return nil
}
parent := chain.GetBlockByHash(parentHash)
if parent == nil {
return errors.New("parent block not found")
}
ch := make(chan core.NewTxPoolReorgEvent, 1)
sub := n.eth.TxPool().SubscribeNewReorgEvent(ch)
defer sub.Unsubscribe()
if err := n.eth.BlockChain().SetPreference(parent); err != nil {
return err
}
for {
select {
case reorg := <-ch:
// Wait for tx pool to reorg, then flush the tx pool
if reorg.Head.Hash() == parent.Hash() {
n.Rollback()
return nil
}
case <-time.After(2 * time.Second):
return errors.New("fork not accepted")
}
}
}
// AdjustTime changes the block timestamp and creates a new block.
// It can only be called on empty blocks.
func (n *Backend) AdjustTime(adjustment time.Duration) error {
_, err := n.buildBlock(false, uint64(adjustment))
return err
}
// Client returns a client that accesses the simulated chain.
func (n *Backend) Client() Client {
return n.client
}
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"github.com/luxfi/geth/core/types"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/geth/params"
"github.com/luxfi/evm/plugin/evm/upgrade/acp176"
"github.com/luxfi/evm/plugin/evm/upgrade/lip176"
)
// Tests that the simulator starts with the initial gas limit in the genesis block,
@@ -50,8 +50,8 @@ func TestWithBlockGasLimitOption(t *testing.T) {
if err != nil {
t.Fatalf("failed to retrieve head block: %v", err)
}
if head.GasLimit() != acp176.MinMaxCapacity {
t.Errorf("head gas limit mismatch: have %v, want %v", head.GasLimit(), acp176.MinMaxCapacity)
if head.GasLimit() != lip176.MinMaxCapacity {
t.Errorf("head gas limit mismatch: have %v, want %v", head.GasLimit(), lip176.MinMaxCapacity)
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"fmt"
"math/big"
"github.com/luxfi/evm/plugin/evm/upgrade/acp176"
"github.com/luxfi/evm/plugin/evm/upgrade/lip176"
"github.com/luxfi/evm/plugin/evm/upgrade/etna"
"github.com/luxfi/geth/common/hexutil"
"github.com/luxfi/geth/common/math"
@@ -67,7 +67,7 @@ func (s *EthereumAPI) SuggestPriceOptions(ctx context.Context) (*PriceOptions, e
chainConfig := s.b.ChainConfig()
minBaseFee := new(big.Int)
if chainConfig.IsFortuna(time) {
minBaseFee.SetUint64(acp176.MinGasPrice)
minBaseFee.SetUint64(lip176.MinGasPrice)
} else {
minBaseFee.SetUint64(etna.MinBaseFee)
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/params"
"github.com/luxfi/evm/plugin/evm/upgrade/acp176"
"github.com/luxfi/evm/plugin/evm/upgrade/lip176"
"github.com/luxfi/evm/plugin/evm/upgrade/etna"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/common/hexutil"
@@ -56,7 +56,7 @@ func TestSuggestPriceOptions(t *testing.T) {
}
minBaseFee := etna.MinBaseFee
bigMinBaseFee := big.NewInt(int64(minBaseFee))
fortunaMinBaseFee := acp176.MinGasPrice
fortunaMinBaseFee := lip176.MinGasPrice
bigFortunaMinBaseFee := big.NewInt(int64(fortunaMinBaseFee))
slowFeeNumerator := testCfg.SlowFeePercentage
fastFeeNumerator := testCfg.FastFeePercentage
-76
View File
@@ -1,76 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
// Copyright 2014 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package miner implements Ethereum block creation and mining.
package miner
import (
"github.com/luxfi/node/utils/timer/mockable"
"github.com/luxfi/evm/consensus"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
)
// Backend wraps all methods required for mining.
type Backend interface {
BlockChain() *core.BlockChain
TxPool() *txpool.TxPool
}
// Config is the configuration parameters of mining.
type Config struct {
Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards
TestOnlyAllowDuplicateBlocks bool // Allow mining of duplicate blocks (used in tests only)
}
type Miner struct {
worker *worker
}
func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, clock *mockable.Clock) *Miner {
return &Miner{
worker: newWorker(config, chainConfig, engine, eth, mux, clock),
}
}
func (miner *Miner) SetEtherbase(addr common.Address) {
miner.worker.setEtherbase(addr)
}
func (miner *Miner) GenerateBlock(predicateContext *precompileconfig.PredicateContext) (*types.Block, error) {
return miner.worker.commitNewWork(predicateContext)
}
// SubscribePendingLogs starts delivering logs from pending transactions
// to the given channel.
func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscription {
return miner.worker.pendingLogsFeed.Subscribe(ch)
}
-585
View File
@@ -1,585 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
//
// NOTE: this piece of code is modified by Ted Yin.
// The modification is also licensed under the same LGPL.
package miner
import (
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/units"
"github.com/luxfi/evm/consensus"
"github.com/luxfi/evm/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/luxfi/evm/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/commontype"
customheader "github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/predicate"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/consensus/misc/eip4844"
"github.com/holiman/uint256"
ethparams "github.com/luxfi/evm/params"
eparams "github.com/luxfi/evm/params"
)
const (
targetTxsSize = 1800 * units.KiB
blobTxBlobGasPerBlob = 131072 // Gas consumption of a single data blob
maxBlobGasPerBlock = 6 * blobTxBlobGasPerBlob
)
// convertToLazyTransactions converts regular transactions to lazy transactions
func convertToLazyTransactions(txs map[common.Address][]*types.Transaction) map[common.Address][]*txpool.LazyTransaction {
result := make(map[common.Address][]*txpool.LazyTransaction)
for addr, list := range txs {
lazy := make([]*txpool.LazyTransaction, len(list))
for i, tx := range list {
lazy[i] = &txpool.LazyTransaction{
Hash: tx.Hash(),
Tx: tx,
Time: time.Now(),
GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()),
GasTipCap: uint256.MustFromBig(tx.GasTipCap()),
Gas: tx.Gas(),
BlobGas: tx.BlobGas(),
}
}
result[addr] = lazy
}
return result
}
// environment is the worker's current environment and holds all of the current state information.
type environment struct {
signer types.Signer
state *state.StateDB // apply state changes here
tcount int // tx count in cycle
gasPool *core.GasPool // available gas used to pack transactions
parent *types.Header
header *types.Header
txs []*types.Transaction
receipts []*types.Receipt
sidecars []*types.BlobTxSidecar
blobs int
size uint64
rules params.Rules
predicateContext *precompileconfig.PredicateContext
// predicateResults contains the results of checking the predicates for each transaction in the miner.
// The results are accumulated as transactions are executed by the miner and set on the BlockContext.
// If a transaction is dropped, its results must explicitly be removed from predicateResults in the same
// way that the gas pool and state is reset.
predicateResults *predicate.Results
start time.Time // Time that block building began
}
// worker is the main object which takes care of submitting new work to consensus engine
// and gathering the sealing result.
type worker struct {
config *Config
chainConfig *params.ChainConfig
engine consensus.Engine
eth Backend
chain *core.BlockChain
// Feeds
// TODO remove since this will never be written to
pendingLogsFeed event.Feed
// Subscriptions
mux *event.TypeMux // TODO replace
mu sync.RWMutex // The lock used to protect the coinbase and extra fields
coinbase common.Address
clock *interfaces.MockableTimer // Allows us mock the clock for testing
beaconRoot *common.Hash // TODO: set to empty hash, retained for upstream compatibility and future use
}
func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, clock *interfaces.MockableTimer) *worker {
worker := &worker{
config: config,
chainConfig: chainConfig,
engine: engine,
eth: eth,
chain: eth.BlockChain(),
mux: mux,
coinbase: config.Etherbase,
clock: clock,
beaconRoot: &common.Hash{},
}
return worker
}
// setEtherbase sets the etherbase used to initialize the block coinbase field.
func (w *worker) setEtherbase(addr common.Address) {
w.mu.Lock()
defer w.mu.Unlock()
w.coinbase = addr
}
// commitNewWork generates several new sealing tasks based on the parent block.
func (w *worker) commitNewWork(predicateContext *precompileconfig.PredicateContext) (*types.Block, error) {
w.mu.RLock()
defer w.mu.RUnlock()
tstart := w.clock.Time()
timestamp := uint64(tstart.Unix())
parent := w.chain.CurrentBlock()
// Note: in order to support asynchronous block production, blocks are allowed to have
// the same timestamp as their parent. This allows more than one block to be produced
// per second.
if parent.Time >= timestamp {
timestamp = parent.Time
}
// The fee manager relies on the state of the parent block to set the fee config
// because the fee config may be changed by the current block.
feeConfig, _, err := w.chain.GetFeeConfigAt(parent)
if err != nil {
return nil, err
}
chainConfig := params.GetExtra(w.chainConfig)
gasLimit, err := customheader.GasLimit(chainConfig, feeConfig, parent, timestamp)
if err != nil {
return nil, fmt.Errorf("calculating new gas limit: %w", err)
}
baseFee, err := customheader.BaseFee(chainConfig, feeConfig, parent, timestamp)
if err != nil {
return nil, fmt.Errorf("failed to calculate new base fee: %w", err)
}
header := &types.Header{
ParentHash: parent.Hash(),
Number: new(big.Int).Add(parent.Number, common.Big1),
GasLimit: gasLimit,
Time: timestamp,
BaseFee: baseFee,
}
// Apply EIP-4844, EIP-4788.
if w.chainConfig.IsCancun(header.Time) {
var excessBlobGas uint64
if w.chainConfig.IsCancun(parent.Time) {
excessBlobGas = eip4844.CalcExcessBlobGas(*parent.ExcessBlobGas, *parent.BlobGasUsed)
} else {
// For the first post-fork block, both parent.data_gas_used and parent.excess_data_gas are evaluated as 0
excessBlobGas = eip4844.CalcExcessBlobGas(0, 0)
}
header.BlobGasUsed = new(uint64)
header.ExcessBlobGas = &excessBlobGas
header.ParentBeaconRoot = w.beaconRoot
}
if w.coinbase == (common.Address{}) {
return nil, errors.New("cannot mine without etherbase")
}
header.Coinbase = w.coinbase
configuredCoinbase, isAllowFeeRecipient, err := w.chain.GetCoinbaseAt(parent)
if err != nil {
return nil, fmt.Errorf("failed to get configured coinbase: %w", err)
}
// if fee recipients are not allowed, then the coinbase is the configured coinbase
// don't set w.coinbase directly to the configured coinbase because that would override the
// coinbase set by the user
if !isAllowFeeRecipient && w.coinbase != configuredCoinbase {
log.Info("fee recipients are not allowed, using required coinbase for the mining", "currentminer", w.coinbase, "required", configuredCoinbase)
header.Coinbase = configuredCoinbase
}
if err := w.engine.Prepare(w.chain, header); err != nil {
return nil, fmt.Errorf("failed to prepare header for mining: %w", err)
}
env, err := w.createCurrentEnvironment(predicateContext, parent, header, feeConfig, tstart)
if err != nil {
return nil, fmt.Errorf("failed to create new current environment: %w", err)
}
if header.ParentBeaconRoot != nil {
blockCtx := core.NewEVMBlockContext(header, w.chain, nil)
ethConfig := &ethparams.ChainConfig{ChainID: w.chainConfig.ChainID}
vmenv := vm.NewEVM(blockCtx, vm.TxContext{}, env.state, ethConfig, vm.Config{})
core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, vmenv, env.state)
}
// Ensure we always stop prefetcher after block building is complete.
defer func() {
if env.state == nil {
return
}
env.state.StopPrefetcher()
}()
// Configure any upgrades that should go into effect during this block.
blockContext := core.NewBlockContext(header.Number, header.Time)
err = core.ApplyUpgrades(w.chainConfig, &parent.Time, blockContext, env.state)
if err != nil {
log.Error("failed to configure precompiles mining new block", "parent", parent.Hash(), "number", header.Number, "timestamp", header.Time, "err", err)
return nil, err
}
// Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees
filter := txpool.PendingFilter{
MinTip: uint256.MustFromBig(w.eth.TxPool().GasTip()),
}
if env.header.BaseFee != nil {
filter.BaseFee = uint256.MustFromBig(env.header.BaseFee)
}
if env.header.ExcessBlobGas != nil {
filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(*env.header.ExcessBlobGas))
}
filter.OnlyPlainTxs, filter.OnlyBlobTxs = true, false
pendingPlainTxs := w.eth.TxPool().Pending(filter)
filter.OnlyPlainTxs, filter.OnlyBlobTxs = false, true
pendingBlobTxs := w.eth.TxPool().Pending(filter)
// Convert to lazy transactions
lazyPlainTxs := convertToLazyTransactions(pendingPlainTxs)
lazyBlobTxs := convertToLazyTransactions(pendingBlobTxs)
// Split the pending transactions into locals and remotes.
localPlainTxs, remotePlainTxs := make(map[common.Address][]*txpool.LazyTransaction), lazyPlainTxs
localBlobTxs, remoteBlobTxs := make(map[common.Address][]*txpool.LazyTransaction), lazyBlobTxs
for _, account := range w.eth.TxPool().Locals() {
if txs := remotePlainTxs[account]; len(txs) > 0 {
delete(remotePlainTxs, account)
localPlainTxs[account] = txs
}
if txs := remoteBlobTxs[account]; len(txs) > 0 {
delete(remoteBlobTxs, account)
localBlobTxs[account] = txs
}
}
// Fill the block with all available pending transactions.
if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 {
plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, env.header.BaseFee)
blobTxs := newTransactionsByPriceAndNonce(env.signer, localBlobTxs, env.header.BaseFee)
w.commitTransactions(env, plainTxs, blobTxs, env.header.Coinbase)
}
if len(remotePlainTxs) > 0 || len(remoteBlobTxs) > 0 {
plainTxs := newTransactionsByPriceAndNonce(env.signer, remotePlainTxs, env.header.BaseFee)
blobTxs := newTransactionsByPriceAndNonce(env.signer, remoteBlobTxs, env.header.BaseFee)
w.commitTransactions(env, plainTxs, blobTxs, env.header.Coinbase)
}
return w.commit(env)
}
func (w *worker) createCurrentEnvironment(predicateContext *precompileconfig.PredicateContext, parent *types.Header, header *types.Header, feeConfig commontype.FeeConfig, tstart time.Time) (*environment, error) {
currentState, err := w.chain.StateAt(parent.Root)
if err != nil {
return nil, err
}
chainConfig := params.GetExtra(w.chainConfig)
_, err = customheader.GasCapacity(chainConfig, feeConfig, parent, header.Time)
if err != nil {
return nil, fmt.Errorf("calculating gas capacity: %w", err)
}
currentState.StartPrefetcher("miner")
return &environment{
signer: types.MakeSigner(&eparams.ChainConfig{ChainID: w.chainConfig.ChainID}, header.Number, header.Time),
state: currentState,
parent: parent,
header: header,
tcount: 0,
gasPool: new(core.GasPool).AddGas(header.GasLimit),
rules: w.chainConfig.LuxRules(header.Number, header.Time),
predicateContext: predicateContext,
predicateResults: predicate.NewResults(),
start: tstart,
}, nil
}
func (w *worker) commitTransaction(env *environment, tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
if tx.Type() == types.BlobTxType {
return w.commitBlobTransaction(env, tx, coinbase)
}
receipt, err := w.applyTransaction(env, tx, coinbase)
if err != nil {
return nil, err
}
env.txs = append(env.txs, tx)
env.receipts = append(env.receipts, receipt)
env.size += tx.Size()
return receipt.Logs, nil
}
func (w *worker) commitBlobTransaction(env *environment, tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
sc := tx.BlobTxSidecar()
if sc == nil {
panic("blob transaction without blobs in miner")
}
// Checking against blob gas limit: It's kind of ugly to perform this check here, but there
// isn't really a better place right now. The blob gas limit is checked at block validation time
// and not during execution. This means core.ApplyTransaction will not return an error if the
// tx has too many blobs. So we have to explicitly check it here.
if (env.blobs+len(sc.Blobs))*blobTxBlobGasPerBlob > maxBlobGasPerBlock {
return nil, errors.New("max data blobs reached")
}
receipt, err := w.applyTransaction(env, tx, coinbase)
if err != nil {
return nil, err
}
env.txs = append(env.txs, tx.WithoutBlobTxSidecar())
env.receipts = append(env.receipts, receipt)
env.sidecars = append(env.sidecars, sc)
env.blobs += len(sc.Blobs)
*env.header.BlobGasUsed += receipt.BlobGasUsed
return receipt.Logs, nil
}
// applyTransaction runs the transaction. If execution fails, state and gas pool are reverted.
func (w *worker) applyTransaction(env *environment, tx *types.Transaction, coinbase common.Address) (*types.Receipt, error) {
var (
snap = env.state.Snapshot()
gp = env.gasPool.Gas()
blockContext vm.BlockContext
)
if params.GetRulesExtra(env.rules).IsDurango {
results, err := core.CheckPredicates(env.rules, env.predicateContext, tx)
if err != nil {
log.Debug("Transaction predicate failed verification in miner", "tx", tx.Hash(), "err", err)
return nil, err
}
env.predicateResults.SetTxResults(tx.Hash(), results)
predicateResultsBytes, err := env.predicateResults.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to marshal predicate results: %w", err)
}
blockContext = core.NewEVMBlockContextWithPredicateResults(env.header, w.chain, &coinbase, predicateResultsBytes)
} else {
blockContext = core.NewEVMBlockContext(env.header, w.chain, &coinbase)
}
receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, blockContext, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, *w.chain.GetVMConfig())
if err != nil {
env.state.RevertToSnapshot(snap)
env.gasPool.SetGas(gp)
env.predicateResults.DeleteTxResults(tx.Hash())
}
return receipt, err
}
func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, coinbase common.Address) {
for {
// If we don't have enough gas for any further transactions then we're done.
if env.gasPool.Gas() < ethparams.TxGas {
log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", ethparams.TxGas)
break
}
// If we don't have enough blob space for any further blob transactions,
// skip that list altogether
if !blobTxs.Empty() && env.blobs*blobTxBlobGasPerBlob >= maxBlobGasPerBlock {
log.Trace("Not enough blob space for further blob transactions")
blobTxs.Clear()
// Fall though to pick up any plain txs
}
// If we don't have enough blob space for any further blob transactions,
// skip that list altogether
if !blobTxs.Empty() && env.blobs*blobTxBlobGasPerBlob >= maxBlobGasPerBlock {
log.Trace("Not enough blob space for further blob transactions")
blobTxs.Clear()
// Fall though to pick up any plain txs
}
// Retrieve the next transaction and abort if all done.
var (
ltx *txpool.LazyTransaction
txs *transactionsByPriceAndNonce
)
pltx, ptip := plainTxs.Peek()
bltx, btip := blobTxs.Peek()
switch {
case pltx == nil:
txs, ltx = blobTxs, bltx
case bltx == nil:
txs, ltx = plainTxs, pltx
default:
if ptip.Lt(btip) {
txs, ltx = blobTxs, bltx
} else {
txs, ltx = plainTxs, pltx
}
}
if ltx == nil {
break
}
// If we don't have enough space for the next transaction, skip the account.
if env.gasPool.Gas() < ltx.Gas {
log.Trace("Not enough gas left for transaction", "hash", ltx.Hash, "left", env.gasPool.Gas(), "needed", ltx.Gas)
txs.Pop()
continue
}
if left := uint64(maxBlobGasPerBlock - env.blobs*blobTxBlobGasPerBlob); left < ltx.BlobGas {
log.Trace("Not enough blob gas left for transaction", "hash", ltx.Hash, "left", left, "needed", ltx.BlobGas)
txs.Pop()
continue
}
// Transaction seems to fit, pull it up from the pool
tx := ltx.Resolve()
if tx == nil {
log.Trace("Ignoring evicted transaction", "hash", ltx.Hash)
txs.Pop()
continue
}
// Abort transaction if it won't fit in the block and continue to search for a smaller
// transaction that will fit.
if totalTxsSize := env.size + tx.Size(); totalTxsSize > targetTxsSize {
log.Trace("Skipping transaction that would exceed target size", "hash", tx.Hash(), "totalTxsSize", totalTxsSize, "txSize", tx.Size())
txs.Pop()
continue
}
// Error may be ignored here. The error has already been checked
// during transaction acceptance is the transaction pool.
from, _ := types.Sender(env.signer, tx)
// Check whether the tx is replay protected. If we're not in the EIP155 hf
// phase, start ignoring the sender until we do.
if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
log.Trace("Ignoring replay protected transaction", "hash", ltx.Hash, "eip155", w.chainConfig.EIP155Block)
txs.Pop()
continue
}
// Start executing the transaction
env.state.SetTxContext(tx.Hash(), env.tcount)
_, err := w.commitTransaction(env, tx, coinbase)
switch {
case errors.Is(err, core.ErrNonceTooLow):
// New head notification data race between the transaction pool and miner, shift
log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce())
txs.Shift()
case errors.Is(err, nil):
env.tcount++
txs.Shift()
default:
// Transaction is regarded as invalid, drop all consecutive transactions from
// the same sender because of `nonce-too-high` clause.
log.Debug("Transaction failed, account skipped", "hash", ltx.Hash, "err", err)
txs.Pop()
}
}
}
// commit runs any post-transaction state modifications, assembles the final block
// and commits new work if consensus engine is running.
func (w *worker) commit(env *environment) (*types.Block, error) {
if params.GetRulesExtra(env.rules).IsDurango {
predicateResultsBytes, err := env.predicateResults.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to marshal predicate results: %w", err)
}
env.header.Extra = append(env.header.Extra, predicateResultsBytes...)
}
// Deep copy receipts here to avoid interaction between different tasks.
receipts := copyReceipts(env.receipts)
block, err := w.engine.FinalizeAndAssemble(w.chain, env.header, env.parent, env.state, env.txs, nil, receipts)
if err != nil {
return nil, err
}
return w.handleResult(env, block, time.Now(), receipts)
}
func (w *worker) handleResult(env *environment, block *types.Block, createdAt time.Time, unfinishedReceipts []*types.Receipt) (*types.Block, error) {
// Short circuit when receiving duplicate result caused by resubmitting.
if !w.config.TestOnlyAllowDuplicateBlocks && w.chain.HasBlock(block.Hash(), block.NumberU64()) {
return nil, fmt.Errorf("produced duplicate block (Hash: %s, Number %d)", block.Hash(), block.NumberU64())
}
// Different block could share same sealhash, deep copy here to prevent write-write conflict.
var (
hash = block.Hash()
receipts = make([]*types.Receipt, len(unfinishedReceipts))
logs []*types.Log
)
for i, unfinishedReceipt := range unfinishedReceipts {
receipt := new(types.Receipt)
receipts[i] = receipt
*receipt = *unfinishedReceipt
// add block location fields
receipt.BlockHash = hash
receipt.BlockNumber = block.Number()
receipt.TransactionIndex = uint(i)
// Update the block hash in all logs since it is now available and not when the
// receipt/log of individual transactions were created.
receipt.Logs = make([]*types.Log, len(unfinishedReceipt.Logs))
for j, unfinishedLog := range unfinishedReceipt.Logs {
log := new(types.Log)
receipt.Logs[j] = log
*log = *unfinishedLog
log.BlockHash = hash
}
logs = append(logs, receipt.Logs...)
}
feesInEther, err := core.TotalFeesFloat(block, receipts)
if err != nil {
log.Error("TotalFeesFloat error: %s", err)
}
log.Info("Commit new mining work", "number", block.Number(), "hash", hash,
"uncles", 0, "txs", env.tcount,
"gas", block.GasUsed(), "fees", feesInEther,
"elapsed", common.PrettyDuration(time.Since(env.start)))
// Note: the miner no longer emits a NewMinedBlock event. Instead the caller
// is responsible for running any additional verification and then inserting
// the block with InsertChain, which will also emit a new head event.
return block, nil
}
// copyReceipts makes a deep copy of the given receipts.
func copyReceipts(receipts []*types.Receipt) []*types.Receipt {
result := make([]*types.Receipt, len(receipts))
for i, l := range receipts {
cpy := *l
result[i] = &cpy
}
return result
}
-1017
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-383
View File
@@ -1,383 +0,0 @@
// (c) 2024 Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"encoding/json"
"fmt"
"math/big"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/utils/constants"
"github.com/ethereum/go-ethereum/common"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/utils"
gethparams "github.com/luxfi/evm/params"
)
var (
DefaultFeeConfig = commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 2, // in seconds
MinBaseFee: big.NewInt(25_000_000_000),
TargetGas: big.NewInt(15_000_000),
BaseFeeChangeDenominator: big.NewInt(36),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(1_000_000),
BlockGasCostStep: big.NewInt(200_000),
}
EVMDefaultChainConfig = &ChainConfig{
FeeConfig: DefaultFeeConfig,
NetworkUpgrades: getDefaultNetworkUpgrades(upgrade.GetConfig(constants.MainnetID)),
GenesisPrecompiles: Precompiles{},
}
TestChainConfig = &ChainConfig{
LuxContext: LuxContext{ConsensusCtx: utils.TestConsensusContext()},
FeeConfig: DefaultFeeConfig,
NetworkUpgrades: getDefaultNetworkUpgrades(upgrade.GetConfig(constants.UnitTestID)), // This can be changed to correct network (local, test) via VM.
GenesisPrecompiles: Precompiles{},
}
TestPreEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestDurangoChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestEtnaChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestFortunaChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
}
})
TestGraniteChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
GraniteTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
}
})
)
func copyAndSet(c *ChainConfig, set func(*ChainConfig)) *ChainConfig {
newConfig := *c
set(&newConfig)
return &newConfig
}
// UpgradeConfig includes the following configs that may be specified in upgradeBytes:
// - Timestamps that enable lux network upgrades,
// - Enabling or disabling precompiles as network upgrades.
type UpgradeConfig struct {
// Config for timestamps that enable network upgrades.
NetworkUpgradeOverrides *NetworkUpgrades `json:"networkUpgradeOverrides,omitempty"`
// Config for modifying state as a network upgrade.
StateUpgrades []StateUpgrade `json:"stateUpgrades,omitempty"`
// Config for enabling and disabling precompiles as network upgrades.
PrecompileUpgrades []PrecompileUpgrade `json:"precompileUpgrades,omitempty"`
}
// LuxContext provides Lux specific context directly into the EVM.
type LuxContext struct {
ConsensusCtx *interfaces.ChainContext
}
type ChainConfig struct {
NetworkUpgrades // Config for timestamps that enable network upgrades.
LuxContext `json:"-"` // Lux specific context set during VM initialization. Not serialized.
FeeConfig commontype.FeeConfig `json:"feeConfig"` // Set the configuration for the dynamic fee algorithm
AllowFeeRecipients bool `json:"allowFeeRecipients,omitempty"` // Allows fees to be collected by block builders.
GenesisPrecompiles Precompiles `json:"-"` // Config for enabling precompiles from genesis. JSON encode/decode will be handled by the custom marshaler/unmarshaler.
UpgradeConfig `json:"-"` // Config specified in upgradeBytes (lux network upgrades or enable/disabling precompiles). Not serialized.
}
func (c *ChainConfig) CheckConfigCompatible(newConfig *ChainConfig, headNumber *big.Int, headTimestamp uint64) *gethparams.ConfigCompatError {
if c == nil {
return nil
}
return c.checkConfigCompatible(newConfig, headNumber, headTimestamp)
}
func (c *ChainConfig) checkConfigCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64) *gethparams.ConfigCompatError {
if err := c.checkNetworkUpgradesCompatible(&newcfg.NetworkUpgrades, headTimestamp); err != nil {
return err
}
// Check that the precompiles on the new config are compatible with the existing precompile config.
if err := c.checkPrecompilesCompatible(newcfg.PrecompileUpgrades, headTimestamp); err != nil {
return err
}
// Check that the state upgrades on the new config are compatible with the existing state upgrade config.
if err := c.checkStateUpgradesCompatible(newcfg.StateUpgrades, headTimestamp); err != nil {
return err
}
return nil
}
func (c *ChainConfig) Description() string {
if c == nil {
return ""
}
var banner string
banner += "Lux Upgrades (timestamp based):\n"
banner += c.NetworkUpgrades.Description()
banner += "\n"
upgradeConfigBytes, err := json.Marshal(c.UpgradeConfig)
if err != nil {
upgradeConfigBytes = []byte("cannot marshal UpgradeConfig")
}
banner += fmt.Sprintf("Upgrade Config: %s", string(upgradeConfigBytes))
banner += "\n"
feeBytes, err := json.Marshal(c.FeeConfig)
if err != nil {
feeBytes = []byte("cannot marshal FeeConfig")
}
banner += fmt.Sprintf("Fee Config: %s\n", string(feeBytes))
banner += fmt.Sprintf("Allow Fee Recipients: %v\n", c.AllowFeeRecipients)
return banner
}
// isForkTimestampIncompatible returns true if a fork scheduled at timestamp s1
// cannot be rescheduled to timestamp s2 because head is already past the fork.
func isForkTimestampIncompatible(s1, s2 *uint64, head uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2)
}
// isTimestampForked returns whether a fork scheduled at timestamp s is active
// at the given head timestamp.
func isTimestampForked(s *uint64, head uint64) bool {
if s == nil {
return false
}
return *s <= head
}
func configTimestampEqual(x, y *uint64) bool {
if x == nil {
return y == nil
}
if y == nil {
return x == nil
}
return *x == *y
}
// UnmarshalJSON parses the JSON-encoded data and stores the result in the
// object pointed to by c.
// This is a custom unmarshaler to handle the Precompiles field.
// Precompiles was presented as an inline object in the JSON.
// This custom unmarshaler ensures backwards compatibility with the old format.
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp := _ChainConfigExtra{}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
// At this point we have populated all fields except PrecompileUpgrade
*c = ChainConfig(tmp)
// Unmarshal inlined PrecompileUpgrade
return json.Unmarshal(data, &c.GenesisPrecompiles)
}
// MarshalJSON returns the JSON encoding of c.
// This is a custom marshaler to handle the Precompiles field.
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp, err := json.Marshal(_ChainConfigExtra(*c))
if err != nil {
return nil, err
}
// To include PrecompileUpgrades, we unmarshal the json representing c
// then directly add the corresponding keys to the json.
raw := make(map[string]json.RawMessage)
if err := json.Unmarshal(tmp, &raw); err != nil {
return nil, err
}
for key, value := range c.GenesisPrecompiles {
conf, err := json.Marshal(value)
if err != nil {
return nil, err
}
raw[key] = conf
}
return json.Marshal(raw)
}
type fork struct {
name string
block *big.Int // some go-ethereum forks use block numbers
timestamp *uint64 // Lux forks use timestamps
optional bool // if true, the fork may be nil and next fork is still allowed
}
func (c *ChainConfig) CheckConfigForkOrder() error {
if c == nil {
return nil
}
// Note: In Lux, upgrades must take place via block timestamps instead
// of block numbers since blocks are produced asynchronously. Therefore, we do
// not check block timestamp forks in the same way as block number forks since
// it would not be a meaningful comparison. Instead, we only check that the
// Lux upgrades are enabled in order.
// Note: we do not add the precompile configs here because they are optional
// and independent, i.e. the order in which they are enabled does not impact
// the correctness of the chain config.
return checkForks(c.forkOrder(), false)
}
// checkForks checks that forks are enabled in order and returns an error if not.
// `blockFork` is true if the fork is a block number fork, false if it is a timestamp fork
func checkForks(forks []fork, blockFork bool) error {
lastFork := fork{}
for _, cur := range forks {
if lastFork.name != "" {
switch {
// Non-optional forks must all be present in the chain config up to the last defined fork
case lastFork.block == nil && lastFork.timestamp == nil && (cur.block != nil || cur.timestamp != nil):
if cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at block %v",
lastFork.name, cur.name, cur.block)
} else {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at timestamp %v",
lastFork.name, cur.name, cur.timestamp)
}
// Fork (whether defined by block or timestamp) must follow the fork definition sequence
case (lastFork.block != nil && cur.block != nil) || (lastFork.timestamp != nil && cur.timestamp != nil):
if lastFork.block != nil && lastFork.block.Cmp(cur.block) > 0 {
return fmt.Errorf("unsupported fork ordering: %v enabled at block %v, but %v enabled at block %v",
lastFork.name, lastFork.block, cur.name, cur.block)
} else if lastFork.timestamp != nil && *lastFork.timestamp > *cur.timestamp {
return fmt.Errorf("unsupported fork ordering: %v enabled at timestamp %v, but %v enabled at timestamp %v",
lastFork.name, lastFork.timestamp, cur.name, cur.timestamp)
}
// Timestamp based forks can follow block based ones, but not the other way around
if lastFork.timestamp != nil && cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v used timestamp ordering, but %v reverted to block ordering",
lastFork.name, cur.name)
}
}
}
// If it was optional and not set, then ignore it
if !cur.optional || (cur.block != nil || cur.timestamp != nil) {
lastFork = cur
}
}
return nil
}
// Verify verifies chain config.
func (c *ChainConfig) Verify() error {
if err := c.FeeConfig.Verify(); err != nil {
return fmt.Errorf("invalid fee config: %w", err)
}
// Verify the precompile upgrades are internally consistent given the existing chainConfig.
if err := c.verifyPrecompileUpgrades(); err != nil {
return fmt.Errorf("invalid precompile upgrades: %w", err)
}
// Verify the state upgrades are internally consistent given the existing chainConfig.
if err := c.verifyStateUpgrades(); err != nil {
return fmt.Errorf("invalid state upgrades: %w", err)
}
// Verify the network upgrades are internally consistent given the existing chainConfig.
if err := c.verifyNetworkUpgrades(c.ConsensusCtx.NetworkUpgrades); err != nil {
return fmt.Errorf("invalid network upgrades: %w", err)
}
return nil
}
// IsPrecompileEnabled returns whether precompile with `address` is enabled at `timestamp`.
func (c *ChainConfig) IsPrecompileEnabled(address common.Address, timestamp uint64) bool {
config := c.GetActivePrecompileConfig(address, timestamp)
return config != nil && !config.IsDisabled()
}
// GetFeeConfig returns the original FeeConfig contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) GetFeeConfig() commontype.FeeConfig {
return c.FeeConfig
}
// AllowedFeeRecipients returns the original AllowedFeeRecipients parameter contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) AllowedFeeRecipients() bool {
return c.AllowFeeRecipients
}
// IsForkTransition returns true if `fork` activates during the transition from
// `parent` to `current`.
// Taking `parent` as a pointer allows for us to pass nil when checking forks
// that activate during genesis.
// Note: `parent` and `current` can be either both timestamp values, or both
// block number values, since this function works for both block number and
// timestamp activated forks.
func IsForkTransition(fork *uint64, parent *uint64, current uint64) bool {
var parentForked bool
if parent != nil {
parentForked = isTimestampForked(fork, *parent)
}
currentForked := isTimestampForked(fork, current)
return !parentForked && currentForked
}
-383
View File
@@ -1,383 +0,0 @@
// (c) 2024 Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"encoding/json"
"fmt"
"math/big"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/evm/interfaces"
"github.com/ethereum/go-ethereum/common"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/utils"
gethparams "github.com/luxfi/evm/params"
)
var (
DefaultFeeConfig = commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 2, // in seconds
MinBaseFee: big.NewInt(25_000_000_000),
TargetGas: big.NewInt(15_000_000),
BaseFeeChangeDenominator: big.NewInt(36),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(1_000_000),
BlockGasCostStep: big.NewInt(200_000),
}
EVMDefaultChainConfig = &ChainConfig{
FeeConfig: DefaultFeeConfig,
NetworkUpgrades: getDefaultNetworkUpgrades(upgrade.GetConfig(constants.MainnetID)),
GenesisPrecompiles: Precompiles{},
}
TestChainConfig = &ChainConfig{
LuxContext: LuxContext{ConsensusCtx: utils.TestConsensusContext()},
FeeConfig: DefaultFeeConfig,
NetworkUpgrades: getDefaultNetworkUpgrades(upgrade.GetConfig(constants.UnitTestID)), // This can be changed to correct network (local, test) via VM.
GenesisPrecompiles: Precompiles{},
}
TestPreEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestEVMChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestDurangoChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestEtnaChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.UnscheduledActivationTime),
}
})
TestFortunaChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
}
})
TestGraniteChainConfig = copyAndSet(TestChainConfig, func(c *ChainConfig) {
c.NetworkUpgrades = NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
FortunaTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
GraniteTimestamp: utils.TimeToNewUint64(upgrade.InitiallyActiveTime),
}
})
)
func copyAndSet(c *ChainConfig, set func(*ChainConfig)) *ChainConfig {
newConfig := *c
set(&newConfig)
return &newConfig
}
// UpgradeConfig includes the following configs that may be specified in upgradeBytes:
// - Timestamps that enable lux network upgrades,
// - Enabling or disabling precompiles as network upgrades.
type UpgradeConfig struct {
// Config for timestamps that enable network upgrades.
NetworkUpgradeOverrides *NetworkUpgrades `json:"networkUpgradeOverrides,omitempty"`
// Config for modifying state as a network upgrade.
StateUpgrades []StateUpgrade `json:"stateUpgrades,omitempty"`
// Config for enabling and disabling precompiles as network upgrades.
PrecompileUpgrades []PrecompileUpgrade `json:"precompileUpgrades,omitempty"`
}
// LuxContext provides Lux specific context directly into the EVM.
type LuxContext struct {
ConsensusCtx *interfaces.ChainContext
}
type ChainConfig struct {
NetworkUpgrades // Config for timestamps that enable network upgrades.
LuxContext `json:"-"` // Lux specific context set during VM initialization. Not serialized.
FeeConfig commontype.FeeConfig `json:"feeConfig"` // Set the configuration for the dynamic fee algorithm
AllowFeeRecipients bool `json:"allowFeeRecipients,omitempty"` // Allows fees to be collected by block builders.
GenesisPrecompiles Precompiles `json:"-"` // Config for enabling precompiles from genesis. JSON encode/decode will be handled by the custom marshaler/unmarshaler.
UpgradeConfig `json:"-"` // Config specified in upgradeBytes (lux network upgrades or enable/disabling precompiles). Not serialized.
}
func (c *ChainConfig) CheckConfigCompatible(newConfig *ChainConfig, headNumber *big.Int, headTimestamp uint64) *gethparams.ConfigCompatError {
if c == nil {
return nil
}
return c.checkConfigCompatible(newConfig, headNumber, headTimestamp)
}
func (c *ChainConfig) checkConfigCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64) *gethparams.ConfigCompatError {
if err := c.checkNetworkUpgradesCompatible(&newcfg.NetworkUpgrades, headTimestamp); err != nil {
return err
}
// Check that the precompiles on the new config are compatible with the existing precompile config.
if err := c.checkPrecompilesCompatible(newcfg.PrecompileUpgrades, headTimestamp); err != nil {
return err
}
// Check that the state upgrades on the new config are compatible with the existing state upgrade config.
if err := c.checkStateUpgradesCompatible(newcfg.StateUpgrades, headTimestamp); err != nil {
return err
}
return nil
}
func (c *ChainConfig) Description() string {
if c == nil {
return ""
}
var banner string
banner += "Lux Upgrades (timestamp based):\n"
banner += c.NetworkUpgrades.Description()
banner += "\n"
upgradeConfigBytes, err := json.Marshal(c.UpgradeConfig)
if err != nil {
upgradeConfigBytes = []byte("cannot marshal UpgradeConfig")
}
banner += fmt.Sprintf("Upgrade Config: %s", string(upgradeConfigBytes))
banner += "\n"
feeBytes, err := json.Marshal(c.FeeConfig)
if err != nil {
feeBytes = []byte("cannot marshal FeeConfig")
}
banner += fmt.Sprintf("Fee Config: %s\n", string(feeBytes))
banner += fmt.Sprintf("Allow Fee Recipients: %v\n", c.AllowFeeRecipients)
return banner
}
// isForkTimestampIncompatible returns true if a fork scheduled at timestamp s1
// cannot be rescheduled to timestamp s2 because head is already past the fork.
func isForkTimestampIncompatible(s1, s2 *uint64, head uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2)
}
// isTimestampForked returns whether a fork scheduled at timestamp s is active
// at the given head timestamp.
func isTimestampForked(s *uint64, head uint64) bool {
if s == nil {
return false
}
return *s <= head
}
func configTimestampEqual(x, y *uint64) bool {
if x == nil {
return y == nil
}
if y == nil {
return x == nil
}
return *x == *y
}
// UnmarshalJSON parses the JSON-encoded data and stores the result in the
// object pointed to by c.
// This is a custom unmarshaler to handle the Precompiles field.
// Precompiles was presented as an inline object in the JSON.
// This custom unmarshaler ensures backwards compatibility with the old format.
func (c *ChainConfig) UnmarshalJSON(data []byte) error {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp := _ChainConfigExtra{}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
// At this point we have populated all fields except PrecompileUpgrade
*c = ChainConfig(tmp)
// Unmarshal inlined PrecompileUpgrade
return json.Unmarshal(data, &c.GenesisPrecompiles)
}
// MarshalJSON returns the JSON encoding of c.
// This is a custom marshaler to handle the Precompiles field.
func (c *ChainConfig) MarshalJSON() ([]byte, error) {
// Alias ChainConfigExtra to avoid recursion
type _ChainConfigExtra ChainConfig
tmp, err := json.Marshal(_ChainConfigExtra(*c))
if err != nil {
return nil, err
}
// To include PrecompileUpgrades, we unmarshal the json representing c
// then directly add the corresponding keys to the json.
raw := make(map[string]json.RawMessage)
if err := json.Unmarshal(tmp, &raw); err != nil {
return nil, err
}
for key, value := range c.GenesisPrecompiles {
conf, err := json.Marshal(value)
if err != nil {
return nil, err
}
raw[key] = conf
}
return json.Marshal(raw)
}
type fork struct {
name string
block *big.Int // some go-ethereum forks use block numbers
timestamp *uint64 // Lux forks use timestamps
optional bool // if true, the fork may be nil and next fork is still allowed
}
func (c *ChainConfig) CheckConfigForkOrder() error {
if c == nil {
return nil
}
// Note: In Lux, upgrades must take place via block timestamps instead
// of block numbers since blocks are produced asynchronously. Therefore, we do
// not check block timestamp forks in the same way as block number forks since
// it would not be a meaningful comparison. Instead, we only check that the
// Lux upgrades are enabled in order.
// Note: we do not add the precompile configs here because they are optional
// and independent, i.e. the order in which they are enabled does not impact
// the correctness of the chain config.
return checkForks(c.forkOrder(), false)
}
// checkForks checks that forks are enabled in order and returns an error if not.
// `blockFork` is true if the fork is a block number fork, false if it is a timestamp fork
func checkForks(forks []fork, blockFork bool) error {
lastFork := fork{}
for _, cur := range forks {
if lastFork.name != "" {
switch {
// Non-optional forks must all be present in the chain config up to the last defined fork
case lastFork.block == nil && lastFork.timestamp == nil && (cur.block != nil || cur.timestamp != nil):
if cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at block %v",
lastFork.name, cur.name, cur.block)
} else {
return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at timestamp %v",
lastFork.name, cur.name, cur.timestamp)
}
// Fork (whether defined by block or timestamp) must follow the fork definition sequence
case (lastFork.block != nil && cur.block != nil) || (lastFork.timestamp != nil && cur.timestamp != nil):
if lastFork.block != nil && lastFork.block.Cmp(cur.block) > 0 {
return fmt.Errorf("unsupported fork ordering: %v enabled at block %v, but %v enabled at block %v",
lastFork.name, lastFork.block, cur.name, cur.block)
} else if lastFork.timestamp != nil && *lastFork.timestamp > *cur.timestamp {
return fmt.Errorf("unsupported fork ordering: %v enabled at timestamp %v, but %v enabled at timestamp %v",
lastFork.name, lastFork.timestamp, cur.name, cur.timestamp)
}
// Timestamp based forks can follow block based ones, but not the other way around
if lastFork.timestamp != nil && cur.block != nil {
return fmt.Errorf("unsupported fork ordering: %v used timestamp ordering, but %v reverted to block ordering",
lastFork.name, cur.name)
}
}
}
// If it was optional and not set, then ignore it
if !cur.optional || (cur.block != nil || cur.timestamp != nil) {
lastFork = cur
}
}
return nil
}
// Verify verifies chain config.
func (c *ChainConfig) Verify() error {
if err := c.FeeConfig.Verify(); err != nil {
return fmt.Errorf("invalid fee config: %w", err)
}
// Verify the precompile upgrades are internally consistent given the existing chainConfig.
if err := c.verifyPrecompileUpgrades(); err != nil {
return fmt.Errorf("invalid precompile upgrades: %w", err)
}
// Verify the state upgrades are internally consistent given the existing chainConfig.
if err := c.verifyStateUpgrades(); err != nil {
return fmt.Errorf("invalid state upgrades: %w", err)
}
// Verify the network upgrades are internally consistent given the existing chainConfig.
if err := c.verifyNetworkUpgrades(c.ConsensusCtx.NetworkUpgrades); err != nil {
return fmt.Errorf("invalid network upgrades: %w", err)
}
return nil
}
// IsPrecompileEnabled returns whether precompile with `address` is enabled at `timestamp`.
func (c *ChainConfig) IsPrecompileEnabled(address common.Address, timestamp uint64) bool {
config := c.GetActivePrecompileConfig(address, timestamp)
return config != nil && !config.IsDisabled()
}
// GetFeeConfig returns the original FeeConfig contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) GetFeeConfig() commontype.FeeConfig {
return c.FeeConfig
}
// AllowedFeeRecipients returns the original AllowedFeeRecipients parameter contained in the genesis ChainConfig.
// Implements precompile.ChainConfig interface.
func (c *ChainConfig) AllowedFeeRecipients() bool {
return c.AllowFeeRecipients
}
// IsForkTransition returns true if `fork` activates during the transition from
// `parent` to `current`.
// Taking `parent` as a pointer allows for us to pass nil when checking forks
// that activate during genesis.
// Note: `parent` and `current` can be either both timestamp values, or both
// block number values, since this function works for both block number and
// timestamp activated forks.
func IsForkTransition(fork *uint64, parent *uint64, current uint64) bool {
var parentForked bool
if parent != nil {
parentForked = isTimestampForked(fork, *parent)
}
currentForked := isTimestampForked(fork, current)
return !parentForked && currentForked
}
-185
View File
@@ -1,185 +0,0 @@
// (c) 2025 Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"math/big"
"testing"
"time"
"github.com/luxfi/node/consensus"
"github.com/luxfi/node/upgrade"
"github.com/ethereum/go-ethereum/common"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/precompile/contracts/txallowlist"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func pointer[T any](v T) *T { return &v }
func TestChainConfigDescription(t *testing.T) {
t.Parallel()
tests := map[string]struct {
config *ChainConfig
wantRegex string
}{
"nil": {},
"empty": {
config: &ChainConfig{},
wantRegex: `Lux Upgrades \(timestamp based\)\:
- EVM Timestamp: ( )+@nil( )+\(https:\/\/github\.com\/luxfi\/luxd\/releases\/tag\/v1\.10\.0\)
( - .+Timestamp: .+\n)+
Upgrade Config: {}
Fee Config: {}
Allow Fee Recipients: false
$`,
},
"set": {
config: &ChainConfig{
NetworkUpgrades: NetworkUpgrades{
EVMTimestamp: pointer(uint64(1)),
DurangoTimestamp: pointer(uint64(2)),
EtnaTimestamp: pointer(uint64(3)),
FortunaTimestamp: pointer(uint64(4)),
},
FeeConfig: commontype.FeeConfig{
GasLimit: big.NewInt(5),
TargetBlockRate: 6,
MinBaseFee: big.NewInt(7),
TargetGas: big.NewInt(8),
BaseFeeChangeDenominator: big.NewInt(9),
MinBlockGasCost: big.NewInt(10),
MaxBlockGasCost: big.NewInt(11),
BlockGasCostStep: big.NewInt(12),
},
AllowFeeRecipients: true,
UpgradeConfig: UpgradeConfig{
NetworkUpgradeOverrides: &NetworkUpgrades{
EVMTimestamp: pointer(uint64(13)),
},
StateUpgrades: []StateUpgrade{
{
BlockTimestamp: pointer(uint64(14)),
StateUpgradeAccounts: map[common.Address]StateUpgradeAccount{
common.Address{15}: {
Code: []byte{16},
},
},
},
},
},
},
wantRegex: `Lux Upgrades \(timestamp based\)\:
- EVM Timestamp: ( )+@1( )+\(https:\/\/github\.com\/luxfi\/luxd\/releases\/tag\/v1\.10\.0\)
( - .+Timestamp: .+\n)+
Upgrade Config: {"networkUpgradeOverrides":{"evmTimestamp":13},"stateUpgrades":\[{"blockTimestamp":14,"accounts":{"0x0f00000000000000000000000000000000000000":{"code":"0x10"}}}\]}
Fee Config: {"gasLimit":5,"targetBlockRate":6,"minBaseFee":7,"targetGas":8,"baseFeeChangeDenominator":9,"minBlockGasCost":10,"maxBlockGasCost":11,"blockGasCostStep":12}
Allow Fee Recipients: true
$`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := test.config.Description()
assert.Regexp(t, test.wantRegex, got, "config description mismatch")
})
}
}
func TestChainConfigVerify(t *testing.T) {
t.Parallel()
validFeeConfig := commontype.FeeConfig{
GasLimit: big.NewInt(1),
TargetBlockRate: 1,
MinBaseFee: big.NewInt(1),
TargetGas: big.NewInt(1),
BaseFeeChangeDenominator: big.NewInt(1),
MinBlockGasCost: big.NewInt(1),
MaxBlockGasCost: big.NewInt(1),
BlockGasCostStep: big.NewInt(1),
}
tests := map[string]struct {
config ChainConfig
errRegex string
}{
"invalid_feeconfig": {
config: ChainConfig{
FeeConfig: commontype.FeeConfig{
GasLimit: nil,
},
},
errRegex: "^invalid fee config: ",
},
"invalid_precompile_upgrades": {
// Also see precompile_config_test.go TestVerifyWithChainConfig* tests
config: ChainConfig{
FeeConfig: validFeeConfig,
UpgradeConfig: UpgradeConfig{
PrecompileUpgrades: []PrecompileUpgrade{
// same precompile cannot be configured twice for the same timestamp
{Config: txallowlist.NewDisableConfig(pointer(uint64(1)))},
{Config: txallowlist.NewDisableConfig(pointer(uint64(1)))},
},
},
},
errRegex: "^invalid precompile upgrades: ",
},
"invalid_state_upgrades": {
config: ChainConfig{
FeeConfig: validFeeConfig,
UpgradeConfig: UpgradeConfig{
StateUpgrades: []StateUpgrade{
{BlockTimestamp: nil},
},
},
},
errRegex: "^invalid state upgrades: ",
},
"invalid_network_upgrades": {
config: ChainConfig{
FeeConfig: validFeeConfig,
NetworkUpgrades: NetworkUpgrades{
EVMTimestamp: nil,
},
LuxContext: LuxContext{ConsensusCtx: &consensus.Context{}},
},
errRegex: "^invalid network upgrades: ",
},
"valid": {
config: ChainConfig{
FeeConfig: validFeeConfig,
NetworkUpgrades: NetworkUpgrades{
EVMTimestamp: pointer(uint64(1)),
DurangoTimestamp: pointer(uint64(2)),
EtnaTimestamp: pointer(uint64(3)),
FortunaTimestamp: pointer(uint64(4)),
},
LuxContext: LuxContext{ConsensusCtx: &consensus.Context{
NetworkUpgrades: upgrade.Config{
DurangoTime: time.Unix(2, 0),
EtnaTime: time.Unix(3, 0),
FortunaTime: time.Unix(4, 0),
},
}},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
err := test.config.Verify()
if test.errRegex == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Regexp(t, test.errRegex, err.Error())
}
})
}
}
-245
View File
@@ -1,245 +0,0 @@
// (c) 2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"fmt"
"reflect"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/evm/utils"
gethparams "github.com/luxfi/evm/params"
)
var errCannotBeNil = fmt.Errorf("timestamp cannot be nil")
// newTimestampCompatError creates a new timestamp compatibility error
func newTimestampCompatError(what string, storedtime, newtime *uint64) *gethparams.ConfigCompatError {
var rew *uint64
switch {
case storedtime == nil:
rew = newtime
case newtime == nil || *storedtime < *newtime:
rew = storedtime
default:
rew = newtime
}
var rewTime uint64
if rew != nil {
rewTime = *rew
}
return &gethparams.ConfigCompatError{
What: what,
StoredTime: storedtime,
NewTime: newtime,
RewindToTime: rewTime,
}
}
// NetworkUpgrades contains timestamps that enable network upgrades.
// Lux specific network upgrades are also included here.
// (nil = no fork, 0 = already activated)
type NetworkUpgrades struct {
// EVMTimestamp is a placeholder that activates Lux Upgrades prior to ApricotPhase6
EVMTimestamp *uint64 `json:"evmTimestamp,omitempty"`
// Durango activates the Shanghai Execution Spec Upgrade from Ethereum (https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/shanghai.md#included-eips)
// and Lux Warp Messaging.
// Note: EIP-4895 is excluded since withdrawals are not relevant to the Lux C-Chain or Subnets running the EVM.
DurangoTimestamp *uint64 `json:"durangoTimestamp,omitempty"`
// Placeholder for EtnaTimestamp
EtnaTimestamp *uint64 `json:"etnaTimestamp,omitempty"`
// Fortuna has no effect on EVM by itself, but is included for completeness.
FortunaTimestamp *uint64 `json:"fortunaTimestamp,omitempty"`
// Granite is a placeholder for the next upgrade.
GraniteTimestamp *uint64 `json:"graniteTimestamp,omitempty"`
}
func (n *NetworkUpgrades) Equal(other *NetworkUpgrades) bool {
return reflect.DeepEqual(n, other)
}
func (n *NetworkUpgrades) checkNetworkUpgradesCompatible(newcfg *NetworkUpgrades, time uint64) *gethparams.ConfigCompatError {
if isForkTimestampIncompatible(n.EVMTimestamp, newcfg.EVMTimestamp, time) {
return newTimestampCompatError("EVM fork block timestamp", n.EVMTimestamp, newcfg.EVMTimestamp)
}
if isForkTimestampIncompatible(n.DurangoTimestamp, newcfg.DurangoTimestamp, time) {
return newTimestampCompatError("Durango fork block timestamp", n.DurangoTimestamp, newcfg.DurangoTimestamp)
}
if isForkTimestampIncompatible(n.EtnaTimestamp, newcfg.EtnaTimestamp, time) {
return newTimestampCompatError("Etna fork block timestamp", n.EtnaTimestamp, newcfg.EtnaTimestamp)
}
if isForkTimestampIncompatible(n.FortunaTimestamp, newcfg.FortunaTimestamp, time) {
return newTimestampCompatError("Fortuna fork block timestamp", n.FortunaTimestamp, newcfg.FortunaTimestamp)
}
if isForkTimestampIncompatible(n.GraniteTimestamp, newcfg.GraniteTimestamp, time) {
return newTimestampCompatError("Granite fork block timestamp", n.GraniteTimestamp, newcfg.GraniteTimestamp)
}
return nil
}
func (n *NetworkUpgrades) forkOrder() []fork {
return []fork{
{name: "evmTimestamp", timestamp: n.EVMTimestamp},
{name: "durangoTimestamp", timestamp: n.DurangoTimestamp},
{name: "etnaTimestamp", timestamp: n.EtnaTimestamp},
{name: "fortunaTimestamp", timestamp: n.FortunaTimestamp, optional: true},
{name: "graniteTimestamp", timestamp: n.GraniteTimestamp},
}
}
// SetDefaults sets the default values for the network upgrades.
// This overrides deactivating the network upgrade by providing a timestamp of nil value.
func (n *NetworkUpgrades) SetDefaults(agoUpgrades upgrade.Config) {
defaults := getDefaultNetworkUpgrades(agoUpgrades)
// If the network upgrade is not set, set it to the default value.
// If the network upgrade is set to 0, we also treat it as nil and set it default.
// This is because in prior versions, upgrades were not modifiable and were directly set to their default values.
// Most of the tools and configurations just provide these as 0, so it is safer to treat 0 as nil and set to default
// to prevent premature activations of the network upgrades for live networks.
if n.EVMTimestamp == nil || *n.EVMTimestamp == 0 {
n.EVMTimestamp = defaults.EVMTimestamp
}
if n.DurangoTimestamp == nil || *n.DurangoTimestamp == 0 {
n.DurangoTimestamp = defaults.DurangoTimestamp
}
if n.EtnaTimestamp == nil || *n.EtnaTimestamp == 0 {
n.EtnaTimestamp = defaults.EtnaTimestamp
}
if n.FortunaTimestamp == nil || *n.FortunaTimestamp == 0 {
n.FortunaTimestamp = defaults.FortunaTimestamp
}
}
// verifyNetworkUpgrades checks that the network upgrades are well formed.
func (n *NetworkUpgrades) verifyNetworkUpgrades(agoUpgrades upgrade.Config) error {
defaults := getDefaultNetworkUpgrades(agoUpgrades)
if err := verifyWithDefault(n.EVMTimestamp, defaults.EVMTimestamp); err != nil {
return fmt.Errorf("EVM fork block timestamp is invalid: %w", err)
}
if err := verifyWithDefault(n.DurangoTimestamp, defaults.DurangoTimestamp); err != nil {
return fmt.Errorf("Durango fork block timestamp is invalid: %w", err)
}
if err := verifyWithDefault(n.EtnaTimestamp, defaults.EtnaTimestamp); err != nil {
return fmt.Errorf("Etna fork block timestamp is invalid: %w", err)
}
if err := verifyWithDefault(n.FortunaTimestamp, defaults.FortunaTimestamp); err != nil {
return fmt.Errorf("Fortuna fork block timestamp is invalid: %w", err)
}
if err := verifyWithDefault(n.GraniteTimestamp, defaults.GraniteTimestamp); err != nil {
return fmt.Errorf("Granite fork block timestamp is invalid: %w", err)
}
return nil
}
func (n *NetworkUpgrades) Override(o *NetworkUpgrades) {
if o.EVMTimestamp != nil {
n.EVMTimestamp = o.EVMTimestamp
}
if o.DurangoTimestamp != nil {
n.DurangoTimestamp = o.DurangoTimestamp
}
if o.EtnaTimestamp != nil {
n.EtnaTimestamp = o.EtnaTimestamp
}
if o.FortunaTimestamp != nil {
n.FortunaTimestamp = o.FortunaTimestamp
}
if o.GraniteTimestamp != nil {
n.GraniteTimestamp = o.GraniteTimestamp
}
}
// IsEVM returns whether [time] represents a block
// with a timestamp after the EVM upgrade time.
func (n NetworkUpgrades) IsEVM(time uint64) bool {
return isTimestampForked(n.EVMTimestamp, time)
}
// IsDurango returns whether [time] represents a block
// with a timestamp after the Durango upgrade time.
func (n NetworkUpgrades) IsDurango(time uint64) bool {
return isTimestampForked(n.DurangoTimestamp, time)
}
// IsEtna returns whether [time] represents a block
// with a timestamp after the Etna upgrade time.
func (n NetworkUpgrades) IsEtna(time uint64) bool {
return isTimestampForked(n.EtnaTimestamp, time)
}
// IsFortuna returns whether [time] represents a block
// with a timestamp after the Fortuna upgrade time.
func (n *NetworkUpgrades) IsFortuna(time uint64) bool {
return isTimestampForked(n.FortunaTimestamp, time)
}
// IsGranite returns whether [time] represents a block
// with a timestamp after the Granite upgrade time.
func (n *NetworkUpgrades) IsGranite(time uint64) bool {
return isTimestampForked(n.GraniteTimestamp, time)
}
func (n *NetworkUpgrades) Description() string {
var banner string
banner += fmt.Sprintf(" - EVM Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.10.0)\n", ptrToString(n.EVMTimestamp))
banner += fmt.Sprintf(" - Durango Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.11.0)\n", ptrToString(n.DurangoTimestamp))
banner += fmt.Sprintf(" - Etna Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.12.0)\n", ptrToString(n.EtnaTimestamp))
banner += fmt.Sprintf(" - Fortuna Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.13.0)\n", ptrToString(n.FortunaTimestamp))
banner += fmt.Sprintf(" - Granite Timestamp: @%-10v (https://github.com/luxfi/node/releases/tag/v1.14.0)\n", ptrToString(n.GraniteTimestamp))
return banner
}
type LuxRules struct {
IsEVM bool
IsDurango bool
IsEtna bool
IsFortuna bool
IsGranite bool
}
func (n *NetworkUpgrades) GetLuxRules(time uint64) LuxRules {
return LuxRules{
IsEVM: n.IsEVM(time),
IsDurango: n.IsDurango(time),
IsEtna: n.IsEtna(time),
IsFortuna: n.IsFortuna(time),
IsGranite: n.IsGranite(time),
}
}
// getDefaultNetworkUpgrades returns the network upgrades for the specified luxd upgrades.
// Nil values are used to indicate optional upgrades.
func getDefaultNetworkUpgrades(agoUpgrade upgrade.Config) NetworkUpgrades {
return NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(agoUpgrade.DurangoTime),
EtnaTimestamp: utils.TimeToNewUint64(agoUpgrade.EtnaTime),
FortunaTimestamp: nil, // Fortuna is optional and has no effect on EVM
GraniteTimestamp: nil, // Granite is optional and has no effect on EVM
}
}
// verifyWithDefault checks that the provided timestamp is greater than or equal to the default timestamp.
func verifyWithDefault(configTimestamp *uint64, defaultTimestamp *uint64) error {
if defaultTimestamp == nil {
return nil
}
if configTimestamp == nil {
return errCannotBeNil
}
if *configTimestamp < *defaultTimestamp {
return fmt.Errorf("provided timestamp (%d) must be greater than or equal to the default timestamp (%d)", *configTimestamp, *defaultTimestamp)
}
return nil
}
func ptrToString(val *uint64) string {
if val == nil {
return "nil"
}
return fmt.Sprintf("%d", *val)
}
-342
View File
@@ -1,342 +0,0 @@
// (c) 2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package extras
import (
"testing"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/upgrade/upgradetest"
"github.com/luxfi/node/utils/constants"
"github.com/luxfi/evm/utils"
"github.com/stretchr/testify/require"
)
func TestNetworkUpgradesEqual(t *testing.T) {
testcases := []struct {
name string
upgrades1 *NetworkUpgrades
upgrades2 *NetworkUpgrades
expected bool
}{
{
name: "EqualNetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
expected: true,
},
{
name: "NotEqualNetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(3),
},
expected: false,
},
{
name: "NilNetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: nil,
expected: false,
},
{
name: "NilNetworkUpgrade",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: nil,
},
expected: false,
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.expected, test.upgrades1.Equal(test.upgrades2))
})
}
}
func TestCheckNetworkUpgradesCompatible(t *testing.T) {
testcases := []struct {
name string
upgrades1 *NetworkUpgrades
upgrades2 *NetworkUpgrades
time uint64
valid bool
}{
{
name: "Compatible_same_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
time: 1,
valid: true,
},
{
name: "Compatible_different_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(3),
},
time: 1,
valid: true,
},
{
name: "Compatible_nil_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: nil,
},
time: 1,
valid: true,
},
{
name: "Incompatible_rewinded_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(1),
},
time: 1,
valid: false,
},
{
name: "Incompatible_fastforward_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(3),
},
time: 4,
valid: false,
},
{
name: "Incompatible_nil_NetworkUpgrades",
upgrades1: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
upgrades2: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: nil,
},
time: 2,
valid: false,
},
{
name: "Incompatible_fastforward_nil_NetworkUpgrades",
upgrades1: func() *NetworkUpgrades {
upgrades := getDefaultNetworkUpgrades(upgrade.Testnet)
return &upgrades
}(),
upgrades2: func() *NetworkUpgrades {
upgrades := getDefaultNetworkUpgrades(upgrade.Testnet)
upgrades.EtnaTimestamp = nil
return &upgrades
}(),
time: uint64(upgrade.Testnet.EtnaTime.Unix()),
valid: false,
},
{
name: "Compatible_Fortuna_fastforward_nil_NetworkUpgrades",
upgrades1: func() *NetworkUpgrades {
upgrades := getDefaultNetworkUpgrades(upgrade.Testnet)
return &upgrades
}(),
upgrades2: func() *NetworkUpgrades {
upgrades := getDefaultNetworkUpgrades(upgrade.Testnet)
upgrades.FortunaTimestamp = nil
return &upgrades
}(),
time: uint64(upgrade.Testnet.FortunaTime.Unix()),
valid: true,
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
err := test.upgrades1.checkNetworkUpgradesCompatible(test.upgrades2, test.time)
if test.valid {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
})
}
}
func TestVerifyNetworkUpgrades(t *testing.T) {
testcases := []struct {
name string
upgrades *NetworkUpgrades
avagoUpgrades upgrade.Config
valid bool
}{
{
name: "ValidNetworkUpgrades_for_latest_network",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.NewUint64(1607144400),
EtnaTimestamp: utils.NewUint64(1607144400),
},
avagoUpgrades: upgradetest.GetConfig(upgradetest.Latest),
valid: true,
},
{
name: "Invalid_Durango_nil_upgrade",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: nil,
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Invalid_EVM_non-zero",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(2),
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Invalid_Durango_before_default_upgrade",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.NewUint64(1),
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Invalid_Mainnet_Durango_reconfigured_to_Testnet",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.GetConfig(constants.TestnetID).DurangoTime),
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Valid_Testnet_Durango_reconfigured_to_Mainnet",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.GetConfig(constants.MainnetID).DurangoTime),
},
avagoUpgrades: upgrade.Testnet,
valid: false,
},
{
name: "Invalid_Etna_nil",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.Mainnet.DurangoTime),
EtnaTimestamp: nil,
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Invalid_Etna_before_Durango",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.Mainnet.DurangoTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.Mainnet.DurangoTime.Add(-1)),
},
avagoUpgrades: upgrade.Mainnet,
valid: false,
},
{
name: "Valid_Fortuna_nil",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.TimeToNewUint64(upgrade.Testnet.DurangoTime),
EtnaTimestamp: utils.TimeToNewUint64(upgrade.Testnet.EtnaTime),
FortunaTimestamp: nil,
},
avagoUpgrades: upgrade.Testnet,
valid: true,
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
err := test.upgrades.verifyNetworkUpgrades(test.avagoUpgrades)
if test.valid {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
})
}
}
func TestForkOrder(t *testing.T) {
testcases := []struct {
name string
upgrades *NetworkUpgrades
expectedErr bool
}{
{
name: "ValidNetworkUpgrades",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(0),
DurangoTimestamp: utils.NewUint64(2),
},
expectedErr: false,
},
{
name: "Invalid order",
upgrades: &NetworkUpgrades{
EVMTimestamp: utils.NewUint64(1),
DurangoTimestamp: utils.NewUint64(0),
},
expectedErr: true,
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
err := checkForks(test.upgrades.forkOrder(), false)
if test.expectedErr {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
})
}
}
+2 -2
View File
@@ -3,11 +3,11 @@
package params
// Avalanche-specific gas costs for precompiled contracts
// Lux-specific gas costs for precompiled contracts
const (
// AssetBalanceApricot is the gas cost for querying native asset balance
AssetBalanceApricot uint64 = 2474
// AssetCallApricot is the gas cost for calling native asset transfer
AssetCallApricot uint64 = 9000
)
)
-78
View File
@@ -1,78 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"context"
"errors"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/version"
)
var (
_ NetworkClient = &client{}
ErrRequestFailed = errors.New("request failed")
)
// NetworkClient defines ability to send request / response through the Network
type NetworkClient interface {
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns response bytes, the ID of the chosen peer, and ErrRequestFailed if
// the request should be retried.
SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte) ([]byte, ids.NodeID, error)
// SendAppRequest synchronously sends request to the selected nodeID
// Returns response bytes, and ErrRequestFailed if the request should be retried.
SendAppRequest(ctx context.Context, nodeID ids.NodeID, request []byte) ([]byte, error)
// TrackBandwidth should be called for each valid request with the bandwidth
// (length of response divided by request time), and with 0 if the response is invalid.
TrackBandwidth(nodeID ids.NodeID, bandwidth float64)
}
// client implements NetworkClient interface
// provides ability to send request / responses through the Network and wait for a response
// so that the caller gets the result synchronously.
type client struct {
network Network
}
// NewNetworkClient returns Client for a given network
func NewNetworkClient(network Network) NetworkClient {
return &client{
network: network,
}
}
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns response bytes, the ID of the chosen peer, and ErrRequestFailed if
// the request should be retried.
func (c *client) SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte) ([]byte, ids.NodeID, error) {
waitingHandler := newWaitingResponseHandler()
nodeID, err := c.network.SendAppRequestAny(ctx, minVersion, request, waitingHandler)
if err != nil {
return nil, nodeID, err
}
response, err := waitingHandler.WaitForResult(ctx)
return response, nodeID, err
}
// SendAppRequest synchronously sends request to the specified nodeID
// Returns response bytes and ErrRequestFailed if the request should be retried.
func (c *client) SendAppRequest(ctx context.Context, nodeID ids.NodeID, request []byte) ([]byte, error) {
waitingHandler := newWaitingResponseHandler()
if err := c.network.SendAppRequest(ctx, nodeID, request, waitingHandler); err != nil {
return nil, err
}
return waitingHandler.WaitForResult(ctx)
}
func (c *client) TrackBandwidth(nodeID ids.NodeID, bandwidth float64) {
c.network.TrackBandwidth(nodeID, bandwidth)
}
-78
View File
@@ -1,78 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"context"
"errors"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/version"
)
var (
_ NetworkClient = &client{}
ErrRequestFailed = errors.New("request failed")
)
// NetworkClient defines ability to send request / response through the Network
type NetworkClient interface {
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns response bytes, the ID of the chosen peer, and ErrRequestFailed if
// the request should be retried.
SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte) ([]byte, interfaces.NodeID, error)
// SendAppRequest synchronously sends request to the selected nodeID
// Returns response bytes, and ErrRequestFailed if the request should be retried.
SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte) ([]byte, error)
// TrackBandwidth should be called for each valid request with the bandwidth
// (length of response divided by request time), and with 0 if the response is invalid.
TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64)
}
// client implements NetworkClient interface
// provides ability to send request / responses through the Network and wait for a response
// so that the caller gets the result synchronously.
type client struct {
network Network
}
// NewNetworkClient returns Client for a given network
func NewNetworkClient(network Network) NetworkClient {
return &client{
network: network,
}
}
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns response bytes, the ID of the chosen peer, and ErrRequestFailed if
// the request should be retried.
func (c *client) SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte) ([]byte, interfaces.NodeID, error) {
waitingHandler := newWaitingResponseHandler()
nodeID, err := c.network.SendAppRequestAny(ctx, minVersion, request, waitingHandler)
if err != nil {
return nil, nodeID, err
}
response, err := waitingHandler.WaitForResult(ctx)
return response, nodeID, err
}
// SendAppRequest synchronously sends request to the specified nodeID
// Returns response bytes and ErrRequestFailed if the request should be retried.
func (c *client) SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte) ([]byte, error) {
waitingHandler := newWaitingResponseHandler()
if err := c.network.SendAppRequest(ctx, nodeID, request, waitingHandler); err != nil {
return nil, err
}
return waitingHandler.WaitForResult(ctx)
}
func (c *client) TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64) {
c.network.TrackBandwidth(nodeID, bandwidth)
}
-588
View File
@@ -1,588 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"context"
"errors"
"fmt"
"sync"
"time"
"golang.org/x/sync/semaphore"
"github.com/luxfi/evm/log"
"github.com/luxfi/node/codec"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/network/p2p"
"github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/utils"
"github.com/luxfi/evm/utils"
"github.com/luxfi/node/version"
"github.com/luxfi/evm/peer/stats"
"github.com/luxfi/evm/plugin/evm/message"
)
// Minimum amount of time to handle a request
const minRequestHandlingDuration = 100 * time.Millisecond
var (
errAcquiringSemaphore = errors.New("error acquiring semaphore")
errExpiredRequest = errors.New("expired request")
_ Network = &network{}
_ validators.Connector = &network{}
_ core.AppHandler = &network{}
)
type Network interface {
validators.Connector
core.AppHandler
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns the ID of the chosen peer, and an error if the request could not
// be sent to a peer with the desired [minVersion].
SendAppRequestAny(ctx context.Context, minVersion *version.Application, message []byte, handler message.ResponseHandler) (interfaces.NodeID, error)
// SendAppRequest sends message to given nodeID, notifying handler when there's a response or timeout
SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, message []byte, handler message.ResponseHandler) error
// Shutdown stops all peer channel listeners and marks the node to have stopped
// n.Start() can be called again but the peers will have to be reconnected
// by calling OnPeerConnected for each peer
Shutdown()
// SetRequestHandler sets the provided request handler as the request handler
SetRequestHandler(handler message.RequestHandler)
// Size returns the size of the network in number of connected peers
Size() uint32
// TrackBandwidth should be called for each valid request with the bandwidth
// (length of response divided by request time), and with 0 if the response is invalid.
TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64)
// NewClient returns a client to send messages with for the given protocol
NewClient(protocol uint64, options ...p2p.ClientOption) *p2p.Client
// AddHandler registers a server handler for an application protocol
AddHandler(protocol uint64, handler p2p.Handler) error
}
// network is an implementation of Network that processes message requests for
// each peer in linear fashion
type network struct {
lock sync.RWMutex // lock for mutating state of this Network struct
self interfaces.NodeID // NodeID of this node
requestIDGen uint32 // requestID counter used to track outbound requests
outstandingRequestHandlers map[uint32]message.ResponseHandler // maps node requestID => message.ResponseHandler
activeAppRequests *semaphore.Weighted // controls maximum number of active outbound requests
activeCrossChainRequests *semaphore.Weighted // controls maximum number of active outbound cross chain requests
p2pNetwork *p2p.Network
appSender core.AppSender // node AppSender for sending messages
codec codec.Manager // Codec used for parsing messages
crossChainCodec codec.Manager // Codec used for parsing cross chain messages
appRequestHandler message.RequestHandler // maps request type => handler
crossChainRequestHandler message.CrossChainRequestHandler // maps cross chain request type => handler
gossipHandler message.GossipHandler // maps gossip type => handler
peers *peerTracker // tracking of peers & bandwidth
appStats stats.RequestHandlerStats // Provide request handler metrics
crossChainStats stats.RequestHandlerStats // Provide cross chain request handler metrics
// Set to true when Shutdown is called, after which all operations on this
// struct are no-ops.
//
// Invariant: Even though `closed` is an atomic, `lock` is required to be
// held when sending requests to guarantee that the network isn't closed
// during these calls. This is because closing the network cancels all
// outstanding requests, which means we must guarantee never to register a
// request that will never be fulfilled or cancelled.
closed utils.Atomic[bool]
}
func NewNetwork(p2pNetwork *p2p.Network, appSender core.AppSender, codec codec.Manager, self interfaces.NodeID, maxActiveAppRequests int64) Network {
return &network{
appSender: appSender,
codec: codec,
self: self,
outstandingRequestHandlers: make(map[uint32]message.ResponseHandler),
activeAppRequests: semaphore.NewWeighted(maxActiveAppRequests),
p2pNetwork: p2pNetwork,
appRequestHandler: message.NoopRequestHandler{},
peers: NewPeerTracker(),
appStats: stats.NewRequestHandlerStats(),
}
}
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion. If minVersion is nil,
// the request will be sent to any peer regardless of their version.
// Returns the ID of the chosen peer, and an error if the request could not
// be sent to a peer with the desired [minVersion].
func (n *network) SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte, handler message.ResponseHandler) (interfaces.NodeID, error) {
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
return interfaces.EmptyIDNodeID, err
}
// Take a slot from total [activeAppRequests] and block until a slot becomes available.
if err := n.activeAppRequests.Acquire(ctx, 1); err != nil {
return interfaces.EmptyIDNodeID, errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
if nodeID, ok := n.peers.GetAnyPeer(minVersion); ok {
return nodeID, n.sendAppRequest(ctx, nodeID, request, handler)
}
n.activeAppRequests.Release(1)
return interfaces.EmptyIDNodeID, fmt.Errorf("no peers found matching version %s out of %d peers", minVersion, n.peers.Size())
}
// SendAppRequest sends request message bytes to specified nodeID, notifying the responseHandler on response or failure
func (n *network) SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte, responseHandler message.ResponseHandler) error {
if nodeID == interfaces.EmptyIDNodeID {
return fmt.Errorf("cannot send request to empty nodeID, nodeID=%s, requestLen=%d", nodeID, len(request))
}
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
return err
}
// Take a slot from total [activeAppRequests] and block until a slot becomes available.
if err := n.activeAppRequests.Acquire(ctx, 1); err != nil {
return errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
return n.sendAppRequest(ctx, nodeID, request, responseHandler)
}
// sendAppRequest sends request message bytes to specified nodeID and adds [responseHandler] to [outstandingRequestHandlers]
// so that it can be invoked when the network receives either a response or failure message.
// Assumes [nodeID] is never [self] since we guarantee [self] will not be added to the [peers] map.
// Releases active requests semaphore if there was an error in sending the request
// Returns an error if [appSender] is unable to make the request.
// Assumes write lock is held
func (n *network) sendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte, responseHandler message.ResponseHandler) error {
if n.closed.Get() {
n.activeAppRequests.Release(1)
return nil
}
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
n.activeAppRequests.Release(1)
return err
}
log.Debug("sending request to peer", "nodeID", nodeID, "requestLen", len(request))
n.peers.TrackPeer(nodeID)
requestID := n.nextRequestID()
n.outstandingRequestHandlers[requestID] = responseHandler
nodeIDs := utils.NewSet[interfaces.NodeID](1)
nodeIDs.Add(nodeID)
// Send app request to [nodeID].
// On failure, release the slot from [activeAppRequests] and delete request
// from [outstandingRequestHandlers]
//
// Cancellation is removed from this context to avoid erroring unexpectedly.
// SendAppRequest should be non-blocking and any error other than context
// cancellation is unexpected.
//
// This guarantees that the network should never receive an unexpected
// AppResponse.
ctxWithoutCancel := context.WithoutCancel(ctx)
if err := n.appSender.SendAppRequest(ctxWithoutCancel, nodeIDs, requestID, request); err != nil {
log.Error(
"request to peer failed",
"nodeID", nodeID,
"requestID", requestID,
"requestLen", len(request),
"error", err,
)
n.activeAppRequests.Release(1)
delete(n.outstandingRequestHandlers, requestID)
return err
}
log.Debug("sent request message to peer", "nodeID", nodeID, "requestID", requestID)
return nil
}
// SendCrossChainRequest sends request message bytes to specified chainID and adds [handler] to [outstandingRequestHandlers]
// so that it can be invoked when the network receives either a response or failure message.
// Returns an error if [appSender] is unable to make the request.
func (n *network) SendCrossChainRequest(ctx context.Context, chainID interfaces.ID, request []byte, handler message.ResponseHandler) error {
// Take a slot from total [activeCrossChainRequests] and block until a slot becomes available.
if err := n.activeCrossChainRequests.Acquire(ctx, 1); err != nil {
return errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
n.activeCrossChainRequests.Release(1)
return nil
}
requestID := n.nextRequestID()
n.outstandingRequestHandlers[requestID] = handler
// Send cross chain request to [chainID].
// On failure, release the slot from [activeCrossChainRequests] and delete request from [outstandingRequestHandlers].
// FIXME: SendCrossChainAppRequest is not available in luxfi/node v1.13.2
// if err := n.appSender.SendCrossChainAppRequest(ctx, chainID, requestID, request); err != nil {
// n.activeCrossChainRequests.Release(1)
// delete(n.outstandingRequestHandlers, requestID)
// return err
// }
n.activeCrossChainRequests.Release(1)
delete(n.outstandingRequestHandlers, requestID)
return errors.New("cross-chain requests not supported in current version")
}
// CrossChainAppRequest notifies the VM when another chain in the network requests for data.
// Send a CrossChainAppResponse to [chainID] in response to a valid message using the same
// [requestID] before the deadline.
func (n *network) CrossChainAppRequest(ctx context.Context, requestingChainID interfaces.ID, requestID uint32, deadline time.Time, request []byte) error {
if n.closed.Get() {
return nil
}
log.Trace("received CrossChainAppRequest from chain", "requestingChainID", requestingChainID, "requestID", requestID, "requestLen", len(request))
// FIXME: CrossChainRequest type is not available
// var req message.CrossChainRequest
// if _, err := n.crossChainCodec.Unmarshal(request, &req); err != nil {
// log.Trace("failed to unmarshal CrossChainAppRequest", "requestingChainID", requestingChainID, "requestID", requestID, "requestLen", len(request), "err", err)
// return nil
// }
// bufferedDeadline, err := calculateTimeUntilDeadline(deadline, n.crossChainStats)
// if err != nil {
// log.Trace("deadline to process CrossChainAppRequest has expired, skipping", "requestingChainID", requestingChainID, "requestID", requestID, "err", err)
// return nil
// }
// log.Trace("processing incoming CrossChainAppRequest", "requestingChainID", requestingChainID, "requestID", requestID, "req", req)
// handleCtx, cancel := context.WithDeadline(context.Background(), bufferedDeadline)
// defer cancel()
// responseBytes, err := req.Handle(handleCtx, requestingChainID, requestID, n.crossChainRequestHandler)
// switch {
// case err != nil && err != context.DeadlineExceeded:
// return err // Return a fatal error
// case responseBytes != nil:
// return n.appSender.SendCrossChainAppResponse(ctx, requestingChainID, requestID, responseBytes) // Propagate fatal error
// default:
// return nil
// }
return errors.New("cross-chain requests not supported in current version")
}
// CrossChainAppRequestFailed can be called by the node -> VM in following cases:
// - respondingChain doesn't exist
// - invalid CrossChainAppResponse from respondingChain
// - invalid CrossChainRequest was sent to respondingChain
// - request times out before a response is provided
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) CrossChainAppRequestFailed(ctx context.Context, respondingChainID interfaces.ID, requestID uint32) error {
log.Trace("received CrossChainAppRequestFailed from chain", "respondingChainID", respondingChainID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
// Can happen after the network has been closed.
log.Trace("received CrossChainAppRequestFailed to unknown request", "respondingChainID", respondingChainID, "requestID", requestID)
return nil
}
// We must release the slot
n.activeCrossChainRequests.Release(1)
return handler.OnFailure()
}
// CrossChainAppResponse is invoked when there is a
// response received from [respondingChainID] regarding a request the VM sent out
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) CrossChainAppResponse(ctx context.Context, respondingChainID interfaces.ID, requestID uint32, response []byte) error {
log.Trace("received CrossChainAppResponse from responding chain", "respondingChainID", respondingChainID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
// Can happen after the network has been closed.
log.Trace("received CrossChainAppResponse to unknown request", "respondingChainID", respondingChainID, "requestID", requestID, "responseLen", len(response))
return nil
}
// We must release the slot
n.activeCrossChainRequests.Release(1)
return handler.OnResponse(response)
}
// AppRequest is called by node -> VM when there is an incoming AppRequest from a peer
// error returned by this function is expected to be treated as fatal by the engine
// returns error if the requestHandler returns an error
// sends a response back to the sender if length of response returned by the handler is >0
// expects the deadline to not have been passed
func (n *network) AppRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, deadline time.Time, request []byte) error {
if n.closed.Get() {
return nil
}
log.Debug("received AppRequest from node", "nodeID", nodeID, "requestID", requestID, "requestLen", len(request))
if !IsNetworkRequest(requestID) {
log.Debug("forwarding AppRequest to SDK network", "nodeID", nodeID, "requestID", requestID, "requestLen", len(request))
return n.p2pNetwork.AppRequest(ctx, nodeID, requestID, deadline, request)
}
bufferedDeadline, err := calculateTimeUntilDeadline(deadline, n.appStats)
if err != nil {
log.Debug("deadline to process AppRequest has expired, skipping", "nodeID", nodeID, "requestID", requestID, "err", err)
return nil
}
var req message.Request
if _, err := n.codec.Unmarshal(request, &req); err != nil {
log.Debug("failed to unmarshal AppRequest", "nodeID", nodeID, "requestID", requestID, "err", err)
return nil
}
log.Debug("processing incoming request", "nodeID", nodeID, "requestID", requestID, "req", req)
// We make a new context here because we don't want to cancel the context
// passed into n.AppSender.SendAppResponse below
handleCtx, cancel := context.WithDeadline(context.Background(), bufferedDeadline)
defer cancel()
responseBytes, err := req.Handle(handleCtx, nodeID, requestID, n.appRequestHandler)
switch {
case err != nil && err != context.DeadlineExceeded:
return err // Return a fatal error
case responseBytes != nil:
return n.appSender.SendAppResponse(ctx, nodeID, requestID, responseBytes) // Propagate fatal error
default:
return nil
}
}
// AppResponse is invoked when there is a response received from a peer regarding a request
// Error returned by this function is expected to be treated as fatal by the engine
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) AppResponse(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, response []byte) error {
log.Debug("received AppResponse from peer", "nodeID", nodeID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
log.Debug("forwarding AppResponse to SDK network", "nodeID", nodeID, "requestID", requestID, "responseLen", len(response))
return n.p2pNetwork.AppResponse(ctx, nodeID, requestID, response)
}
// We must release the slot
n.activeAppRequests.Release(1)
return handler.OnResponse(response)
}
// AppRequestFailed can be called by the node -> VM in following cases:
// - node is benched
// - failed to send message to [nodeID] due to a network issue
// - request times out before a response is provided
// error returned by this function is expected to be treated as fatal by the engine
// returns error only when the response handler returns an error
func (n *network) AppRequestFailed(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, appErr *core.AppError) error {
log.Debug("received AppRequestFailed from peer", "nodeID", nodeID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
log.Debug("forwarding AppRequestFailed to SDK network", "nodeID", nodeID, "requestID", requestID)
return n.p2pNetwork.AppRequestFailed(ctx, nodeID, requestID, appErr)
}
// We must release the slot
n.activeAppRequests.Release(1)
return handler.OnFailure()
}
// calculateTimeUntilDeadline calculates the time until deadline and drops it if we missed he deadline to response.
// This function updates metrics for app requests.
// This is called by [AppRequest].
func calculateTimeUntilDeadline(deadline time.Time, stats stats.RequestHandlerStats) (time.Time, error) {
// calculate how much time is left until the deadline
timeTillDeadline := time.Until(deadline)
stats.UpdateTimeUntilDeadline(timeTillDeadline)
// bufferedDeadline is half the time till actual deadline so that the message has a reasonable chance
// of completing its processing and sending the response to the peer.
bufferedDeadline := time.Now().Add(timeTillDeadline / 2)
// check if we have enough time to handle this request
if time.Until(bufferedDeadline) < minRequestHandlingDuration {
// Drop the request if we already missed the deadline to respond.
stats.IncDeadlineDroppedRequest()
return time.Time{}, errExpiredRequest
}
return bufferedDeadline, nil
}
// markRequestFulfilled fetches the handler for [requestID] and marks the request with [requestID] as having been fulfilled.
// This is called by either [AppResponse] or [AppRequestFailed].
// Assumes that the write lock is not held.
func (n *network) markRequestFulfilled(requestID uint32) (message.ResponseHandler, bool) {
n.lock.Lock()
defer n.lock.Unlock()
handler, exists := n.outstandingRequestHandlers[requestID]
if !exists {
return nil, false
}
// mark message as processed
delete(n.outstandingRequestHandlers, requestID)
return handler, true
}
// Gossip sends given gossip message to peers
func (n *network) Gossip(gossip []byte) error {
if n.closed.Get() {
return nil
}
// Send gossip to all peers
sendConfig := core.SendConfig{
NodeIDs: nil,
Validators: 100, // Send to all validators
NonValidators: 100, // Send to all non-validators
Peers: 0,
}
return n.appSender.SendAppGossip(context.TODO(), sendConfig, gossip)
}
// AppGossip is called by node -> VM when there is an incoming AppGossip from a peer
// error returned by this function is expected to be treated as fatal by the engine
// returns error if request could not be parsed as message.Request or when the requestHandler returns an error
func (n *network) AppGossip(_ context.Context, nodeID interfaces.NodeID, gossipBytes []byte) error {
var gossipMsg message.GossipMessage
if _, err := n.codec.Unmarshal(gossipBytes, &gossipMsg); err != nil {
log.Trace("could not parse app gossip", "nodeID", nodeID, "gossipLen", len(gossipBytes), "err", err)
return nil
}
log.Trace("processing AppGossip from node", "nodeID", nodeID, "msg", gossipMsg)
return gossipMsg.Handle(n.gossipHandler, nodeID)
}
// Connected adds the given nodeID to the peer list so that it can receive messages
func (n *network) Connected(ctx context.Context, nodeID interfaces.NodeID, nodeVersion *version.Application) error {
log.Debug("adding new peer", "nodeID", nodeID)
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
return nil
}
if nodeID != n.self {
// The legacy peer tracker doesn't expect to be connected to itself.
n.peers.Connected(nodeID, nodeVersion)
}
return n.p2pNetwork.Connected(ctx, nodeID, nodeVersion)
}
// Disconnected removes given [nodeID] from the peer list
func (n *network) Disconnected(ctx context.Context, nodeID interfaces.NodeID) error {
log.Debug("disconnecting peer", "nodeID", nodeID)
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
return nil
}
if nodeID != n.self {
// The legacy peer tracker doesn't expect to be connected to itself.
n.peers.Disconnected(nodeID)
}
return n.p2pNetwork.Disconnected(ctx, nodeID)
}
// Shutdown disconnects all peers
func (n *network) Shutdown() {
n.lock.Lock()
defer n.lock.Unlock()
// clean up any pending requests
for requestID, handler := range n.outstandingRequestHandlers {
_ = handler.OnFailure() // make sure all waiting threads are unblocked
delete(n.outstandingRequestHandlers, requestID)
}
n.peers = NewPeerTracker() // reset peers
n.closed.Set(true) // mark network as closed
}
func (n *network) SetRequestHandler(handler message.RequestHandler) {
n.lock.Lock()
defer n.lock.Unlock()
n.appRequestHandler = handler
}
func (n *network) Size() uint32 {
n.lock.RLock()
defer n.lock.RUnlock()
return uint32(n.peers.Size())
}
func (n *network) TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64) {
n.lock.Lock()
defer n.lock.Unlock()
n.peers.TrackBandwidth(nodeID, bandwidth)
}
func (n *network) NewClient(protocol uint64, options ...p2p.ClientOption) *p2p.Client {
return n.p2pNetwork.NewClient(protocol, options...)
}
func (n *network) AddHandler(protocol uint64, handler p2p.Handler) error {
return n.p2pNetwork.AddHandler(protocol, handler)
}
// invariant: peer/network must use explicitly even request ids.
// for this reason, [n.requestID] is initialized as zero and incremented by 2.
// This is for backwards-compatibility while the SDK router exists with the
// legacy geth handlers to avoid a (very) narrow edge case where request ids
// can overlap, resulting in a dropped timeout.
func (n *network) nextRequestID() uint32 {
next := n.requestIDGen
n.requestIDGen += 2
return next
}
// IsNetworkRequest checks if the given requestID is a request for this network handler (even-numbered requestIDs)
// SDK requests are odd-numbered requestIDs
// (see invariant: https://github.com/luxfi/node/blob/v1.13.0/network/p2p/router.go#L83)
func IsNetworkRequest(requestID uint32) bool {
return requestID%2 == 0
}
-588
View File
@@ -1,588 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"context"
"errors"
"fmt"
"sync"
"time"
"golang.org/x/sync/semaphore"
"github.com/luxfi/evm/log"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/network/p2p"
"github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/utils"
"github.com/luxfi/evm/utils"
"github.com/luxfi/node/version"
"github.com/luxfi/evm/peer/stats"
"github.com/luxfi/evm/plugin/evm/message"
)
// Minimum amount of time to handle a request
const minRequestHandlingDuration = 100 * time.Millisecond
var (
errAcquiringSemaphore = errors.New("error acquiring semaphore")
errExpiredRequest = errors.New("expired request")
_ Network = &network{}
_ validators.Connector = &network{}
_ core.AppHandler = &network{}
)
type Network interface {
validators.Connector
core.AppHandler
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion.
// Returns the ID of the chosen peer, and an error if the request could not
// be sent to a peer with the desired [minVersion].
SendAppRequestAny(ctx context.Context, minVersion *version.Application, message []byte, handler message.ResponseHandler) (interfaces.NodeID, error)
// SendAppRequest sends message to given nodeID, notifying handler when there's a response or timeout
SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, message []byte, handler message.ResponseHandler) error
// Shutdown stops all peer channel listeners and marks the node to have stopped
// n.Start() can be called again but the peers will have to be reconnected
// by calling OnPeerConnected for each peer
Shutdown()
// SetRequestHandler sets the provided request handler as the request handler
SetRequestHandler(handler message.RequestHandler)
// Size returns the size of the network in number of connected peers
Size() uint32
// TrackBandwidth should be called for each valid request with the bandwidth
// (length of response divided by request time), and with 0 if the response is invalid.
TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64)
// NewClient returns a client to send messages with for the given protocol
NewClient(protocol uint64, options ...p2p.ClientOption) *p2p.Client
// AddHandler registers a server handler for an application protocol
AddHandler(protocol uint64, handler p2p.Handler) error
}
// network is an implementation of Network that processes message requests for
// each peer in linear fashion
type network struct {
lock sync.RWMutex // lock for mutating state of this Network struct
self interfaces.NodeID // NodeID of this node
requestIDGen uint32 // requestID counter used to track outbound requests
outstandingRequestHandlers map[uint32]message.ResponseHandler // maps node requestID => message.ResponseHandler
activeAppRequests *semaphore.Weighted // controls maximum number of active outbound requests
activeCrossChainRequests *semaphore.Weighted // controls maximum number of active outbound cross chain requests
p2pNetwork *p2p.Network
appSender core.AppSender // node AppSender for sending messages
codec interfaces.Codec // Codec used for parsing messages
crossChainCodec interfaces.Codec // Codec used for parsing cross chain messages
appRequestHandler message.RequestHandler // maps request type => handler
crossChainRequestHandler message.CrossChainRequestHandler // maps cross chain request type => handler
gossipHandler message.GossipHandler // maps gossip type => handler
peers *peerTracker // tracking of peers & bandwidth
appStats stats.RequestHandlerStats // Provide request handler metrics
crossChainStats stats.RequestHandlerStats // Provide cross chain request handler metrics
// Set to true when Shutdown is called, after which all operations on this
// struct are no-ops.
//
// Invariant: Even though `closed` is an atomic, `lock` is required to be
// held when sending requests to guarantee that the network isn't closed
// during these calls. This is because closing the network cancels all
// outstanding requests, which means we must guarantee never to register a
// request that will never be fulfilled or cancelled.
closed utils.Atomic[bool]
}
func NewNetwork(p2pNetwork *p2p.Network, appSender core.AppSender, codec interfaces.Codec, self interfaces.NodeID, maxActiveAppRequests int64) Network {
return &network{
appSender: appSender,
codec: codec,
self: self,
outstandingRequestHandlers: make(map[uint32]message.ResponseHandler),
activeAppRequests: semaphore.NewWeighted(maxActiveAppRequests),
p2pNetwork: p2pNetwork,
appRequestHandler: message.NoopRequestHandler{},
peers: NewPeerTracker(),
appStats: stats.NewRequestHandlerStats(),
}
}
// SendAppRequestAny synchronously sends request to an arbitrary peer with a
// node version greater than or equal to minVersion. If minVersion is nil,
// the request will be sent to any peer regardless of their version.
// Returns the ID of the chosen peer, and an error if the request could not
// be sent to a peer with the desired [minVersion].
func (n *network) SendAppRequestAny(ctx context.Context, minVersion *version.Application, request []byte, handler message.ResponseHandler) (interfaces.NodeID, error) {
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
return interfaces.EmptyIDNodeID, err
}
// Take a slot from total [activeAppRequests] and block until a slot becomes available.
if err := n.activeAppRequests.Acquire(ctx, 1); err != nil {
return interfaces.EmptyIDNodeID, errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
if nodeID, ok := n.peers.GetAnyPeer(minVersion); ok {
return nodeID, n.sendAppRequest(ctx, nodeID, request, handler)
}
n.activeAppRequests.Release(1)
return interfaces.EmptyIDNodeID, fmt.Errorf("no peers found matching version %s out of %d peers", minVersion, n.peers.Size())
}
// SendAppRequest sends request message bytes to specified nodeID, notifying the responseHandler on response or failure
func (n *network) SendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte, responseHandler message.ResponseHandler) error {
if nodeID == interfaces.EmptyIDNodeID {
return fmt.Errorf("cannot send request to empty nodeID, nodeID=%s, requestLen=%d", nodeID, len(request))
}
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
return err
}
// Take a slot from total [activeAppRequests] and block until a slot becomes available.
if err := n.activeAppRequests.Acquire(ctx, 1); err != nil {
return errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
return n.sendAppRequest(ctx, nodeID, request, responseHandler)
}
// sendAppRequest sends request message bytes to specified nodeID and adds [responseHandler] to [outstandingRequestHandlers]
// so that it can be invoked when the network receives either a response or failure message.
// Assumes [nodeID] is never [self] since we guarantee [self] will not be added to the [peers] map.
// Releases active requests semaphore if there was an error in sending the request
// Returns an error if [appSender] is unable to make the request.
// Assumes write lock is held
func (n *network) sendAppRequest(ctx context.Context, nodeID interfaces.NodeID, request []byte, responseHandler message.ResponseHandler) error {
if n.closed.Get() {
n.activeAppRequests.Release(1)
return nil
}
// If the context was cancelled, we can skip sending this request.
if err := ctx.Err(); err != nil {
n.activeAppRequests.Release(1)
return err
}
log.Debug("sending request to peer", "nodeID", nodeID, "requestLen", len(request))
n.peers.TrackPeer(nodeID)
requestID := n.nextRequestID()
n.outstandingRequestHandlers[requestID] = responseHandler
nodeIDs := utils.NewSet[interfaces.NodeID](1)
nodeIDs.Add(nodeID)
// Send app request to [nodeID].
// On failure, release the slot from [activeAppRequests] and delete request
// from [outstandingRequestHandlers]
//
// Cancellation is removed from this context to avoid erroring unexpectedly.
// SendAppRequest should be non-blocking and any error other than context
// cancellation is unexpected.
//
// This guarantees that the network should never receive an unexpected
// AppResponse.
ctxWithoutCancel := context.WithoutCancel(ctx)
if err := n.appSender.SendAppRequest(ctxWithoutCancel, nodeIDs, requestID, request); err != nil {
log.Error(
"request to peer failed",
"nodeID", nodeID,
"requestID", requestID,
"requestLen", len(request),
"error", err,
)
n.activeAppRequests.Release(1)
delete(n.outstandingRequestHandlers, requestID)
return err
}
log.Debug("sent request message to peer", "nodeID", nodeID, "requestID", requestID)
return nil
}
// SendCrossChainRequest sends request message bytes to specified chainID and adds [handler] to [outstandingRequestHandlers]
// so that it can be invoked when the network receives either a response or failure message.
// Returns an error if [appSender] is unable to make the request.
func (n *network) SendCrossChainRequest(ctx context.Context, chainID interfaces.ID, request []byte, handler message.ResponseHandler) error {
// Take a slot from total [activeCrossChainRequests] and block until a slot becomes available.
if err := n.activeCrossChainRequests.Acquire(ctx, 1); err != nil {
return errAcquiringSemaphore
}
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
n.activeCrossChainRequests.Release(1)
return nil
}
requestID := n.nextRequestID()
n.outstandingRequestHandlers[requestID] = handler
// Send cross chain request to [chainID].
// On failure, release the slot from [activeCrossChainRequests] and delete request from [outstandingRequestHandlers].
// FIXME: SendCrossChainAppRequest is not available in luxfi/node v1.13.2
// if err := n.appSender.SendCrossChainAppRequest(ctx, chainID, requestID, request); err != nil {
// n.activeCrossChainRequests.Release(1)
// delete(n.outstandingRequestHandlers, requestID)
// return err
// }
n.activeCrossChainRequests.Release(1)
delete(n.outstandingRequestHandlers, requestID)
return errors.New("cross-chain requests not supported in current version")
}
// CrossChainAppRequest notifies the VM when another chain in the network requests for data.
// Send a CrossChainAppResponse to [chainID] in response to a valid message using the same
// [requestID] before the deadline.
func (n *network) CrossChainAppRequest(ctx context.Context, requestingChainID interfaces.ID, requestID uint32, deadline time.Time, request []byte) error {
if n.closed.Get() {
return nil
}
log.Trace("received CrossChainAppRequest from chain", "requestingChainID", requestingChainID, "requestID", requestID, "requestLen", len(request))
// FIXME: CrossChainRequest type is not available
// var req message.CrossChainRequest
// if _, err := n.crossChainCodec.Unmarshal(request, &req); err != nil {
// log.Trace("failed to unmarshal CrossChainAppRequest", "requestingChainID", requestingChainID, "requestID", requestID, "requestLen", len(request), "err", err)
// return nil
// }
// bufferedDeadline, err := calculateTimeUntilDeadline(deadline, n.crossChainStats)
// if err != nil {
// log.Trace("deadline to process CrossChainAppRequest has expired, skipping", "requestingChainID", requestingChainID, "requestID", requestID, "err", err)
// return nil
// }
// log.Trace("processing incoming CrossChainAppRequest", "requestingChainID", requestingChainID, "requestID", requestID, "req", req)
// handleCtx, cancel := context.WithDeadline(context.Background(), bufferedDeadline)
// defer cancel()
// responseBytes, err := req.Handle(handleCtx, requestingChainID, requestID, n.crossChainRequestHandler)
// switch {
// case err != nil && err != context.DeadlineExceeded:
// return err // Return a fatal error
// case responseBytes != nil:
// return n.appSender.SendCrossChainAppResponse(ctx, requestingChainID, requestID, responseBytes) // Propagate fatal error
// default:
// return nil
// }
return errors.New("cross-chain requests not supported in current version")
}
// CrossChainAppRequestFailed can be called by the node -> VM in following cases:
// - respondingChain doesn't exist
// - invalid CrossChainAppResponse from respondingChain
// - invalid CrossChainRequest was sent to respondingChain
// - request times out before a response is provided
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) CrossChainAppRequestFailed(ctx context.Context, respondingChainID interfaces.ID, requestID uint32) error {
log.Trace("received CrossChainAppRequestFailed from chain", "respondingChainID", respondingChainID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
// Can happen after the network has been closed.
log.Trace("received CrossChainAppRequestFailed to unknown request", "respondingChainID", respondingChainID, "requestID", requestID)
return nil
}
// We must release the slot
n.activeCrossChainRequests.Release(1)
return handler.OnFailure()
}
// CrossChainAppResponse is invoked when there is a
// response received from [respondingChainID] regarding a request the VM sent out
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) CrossChainAppResponse(ctx context.Context, respondingChainID interfaces.ID, requestID uint32, response []byte) error {
log.Trace("received CrossChainAppResponse from responding chain", "respondingChainID", respondingChainID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
// Can happen after the network has been closed.
log.Trace("received CrossChainAppResponse to unknown request", "respondingChainID", respondingChainID, "requestID", requestID, "responseLen", len(response))
return nil
}
// We must release the slot
n.activeCrossChainRequests.Release(1)
return handler.OnResponse(response)
}
// AppRequest is called by node -> VM when there is an incoming AppRequest from a peer
// error returned by this function is expected to be treated as fatal by the engine
// returns error if the requestHandler returns an error
// sends a response back to the sender if length of response returned by the handler is >0
// expects the deadline to not have been passed
func (n *network) AppRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, deadline time.Time, request []byte) error {
if n.closed.Get() {
return nil
}
log.Debug("received AppRequest from node", "nodeID", nodeID, "requestID", requestID, "requestLen", len(request))
if !IsNetworkRequest(requestID) {
log.Debug("forwarding AppRequest to SDK network", "nodeID", nodeID, "requestID", requestID, "requestLen", len(request))
return n.p2pNetwork.AppRequest(ctx, nodeID, requestID, deadline, request)
}
bufferedDeadline, err := calculateTimeUntilDeadline(deadline, n.appStats)
if err != nil {
log.Debug("deadline to process AppRequest has expired, skipping", "nodeID", nodeID, "requestID", requestID, "err", err)
return nil
}
var req message.Request
if _, err := n.interfaces.Unmarshal(request, &req); err != nil {
log.Debug("failed to unmarshal AppRequest", "nodeID", nodeID, "requestID", requestID, "err", err)
return nil
}
log.Debug("processing incoming request", "nodeID", nodeID, "requestID", requestID, "req", req)
// We make a new context here because we don't want to cancel the context
// passed into n.AppSender.SendAppResponse below
handleCtx, cancel := context.WithDeadline(context.Background(), bufferedDeadline)
defer cancel()
responseBytes, err := req.Handle(handleCtx, nodeID, requestID, n.appRequestHandler)
switch {
case err != nil && err != context.DeadlineExceeded:
return err // Return a fatal error
case responseBytes != nil:
return n.appSender.SendAppResponse(ctx, nodeID, requestID, responseBytes) // Propagate fatal error
default:
return nil
}
}
// AppResponse is invoked when there is a response received from a peer regarding a request
// Error returned by this function is expected to be treated as fatal by the engine
// If [requestID] is not known, this function will emit a log and return a nil error.
// If the response handler returns an error it is propagated as a fatal error.
func (n *network) AppResponse(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, response []byte) error {
log.Debug("received AppResponse from peer", "nodeID", nodeID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
log.Debug("forwarding AppResponse to SDK network", "nodeID", nodeID, "requestID", requestID, "responseLen", len(response))
return n.p2pNetwork.AppResponse(ctx, nodeID, requestID, response)
}
// We must release the slot
n.activeAppRequests.Release(1)
return handler.OnResponse(response)
}
// AppRequestFailed can be called by the node -> VM in following cases:
// - node is benched
// - failed to send message to [nodeID] due to a network issue
// - request times out before a response is provided
// error returned by this function is expected to be treated as fatal by the engine
// returns error only when the response handler returns an error
func (n *network) AppRequestFailed(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, appErr *core.AppError) error {
log.Debug("received AppRequestFailed from peer", "nodeID", nodeID, "requestID", requestID)
handler, exists := n.markRequestFulfilled(requestID)
if !exists {
log.Debug("forwarding AppRequestFailed to SDK network", "nodeID", nodeID, "requestID", requestID)
return n.p2pNetwork.AppRequestFailed(ctx, nodeID, requestID, appErr)
}
// We must release the slot
n.activeAppRequests.Release(1)
return handler.OnFailure()
}
// calculateTimeUntilDeadline calculates the time until deadline and drops it if we missed he deadline to response.
// This function updates metrics for app requests.
// This is called by [AppRequest].
func calculateTimeUntilDeadline(deadline time.Time, stats stats.RequestHandlerStats) (time.Time, error) {
// calculate how much time is left until the deadline
timeTillDeadline := time.Until(deadline)
stats.UpdateTimeUntilDeadline(timeTillDeadline)
// bufferedDeadline is half the time till actual deadline so that the message has a reasonable chance
// of completing its processing and sending the response to the peer.
bufferedDeadline := time.Now().Add(timeTillDeadline / 2)
// check if we have enough time to handle this request
if time.Until(bufferedDeadline) < minRequestHandlingDuration {
// Drop the request if we already missed the deadline to respond.
stats.IncDeadlineDroppedRequest()
return time.Time{}, errExpiredRequest
}
return bufferedDeadline, nil
}
// markRequestFulfilled fetches the handler for [requestID] and marks the request with [requestID] as having been fulfilled.
// This is called by either [AppResponse] or [AppRequestFailed].
// Assumes that the write lock is not held.
func (n *network) markRequestFulfilled(requestID uint32) (message.ResponseHandler, bool) {
n.lock.Lock()
defer n.lock.Unlock()
handler, exists := n.outstandingRequestHandlers[requestID]
if !exists {
return nil, false
}
// mark message as processed
delete(n.outstandingRequestHandlers, requestID)
return handler, true
}
// Gossip sends given gossip message to peers
func (n *network) Gossip(gossip []byte) error {
if n.closed.Get() {
return nil
}
// Send gossip to all peers
sendConfig := core.SendConfig{
NodeIDs: nil,
Validators: 100, // Send to all validators
NonValidators: 100, // Send to all non-validators
Peers: 0,
}
return n.appSender.SendAppGossip(context.TODO(), sendConfig, gossip)
}
// AppGossip is called by node -> VM when there is an incoming AppGossip from a peer
// error returned by this function is expected to be treated as fatal by the engine
// returns error if request could not be parsed as message.Request or when the requestHandler returns an error
func (n *network) AppGossip(_ context.Context, nodeID interfaces.NodeID, gossipBytes []byte) error {
var gossipMsg message.GossipMessage
if _, err := n.interfaces.Unmarshal(gossipBytes, &gossipMsg); err != nil {
log.Trace("could not parse app gossip", "nodeID", nodeID, "gossipLen", len(gossipBytes), "err", err)
return nil
}
log.Trace("processing AppGossip from node", "nodeID", nodeID, "msg", gossipMsg)
return gossipMsg.Handle(n.gossipHandler, nodeID)
}
// Connected adds the given nodeID to the peer list so that it can receive messages
func (n *network) Connected(ctx context.Context, nodeID interfaces.NodeID, nodeVersion *version.Application) error {
log.Debug("adding new peer", "nodeID", nodeID)
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
return nil
}
if nodeID != n.self {
// The legacy peer tracker doesn't expect to be connected to itself.
n.peers.Connected(nodeID, nodeVersion)
}
return n.p2pNetwork.Connected(ctx, nodeID, nodeVersion)
}
// Disconnected removes given [nodeID] from the peer list
func (n *network) Disconnected(ctx context.Context, nodeID interfaces.NodeID) error {
log.Debug("disconnecting peer", "nodeID", nodeID)
n.lock.Lock()
defer n.lock.Unlock()
if n.closed.Get() {
return nil
}
if nodeID != n.self {
// The legacy peer tracker doesn't expect to be connected to itself.
n.peers.Disconnected(nodeID)
}
return n.p2pNetwork.Disconnected(ctx, nodeID)
}
// Shutdown disconnects all peers
func (n *network) Shutdown() {
n.lock.Lock()
defer n.lock.Unlock()
// clean up any pending requests
for requestID, handler := range n.outstandingRequestHandlers {
_ = handler.OnFailure() // make sure all waiting threads are unblocked
delete(n.outstandingRequestHandlers, requestID)
}
n.peers = NewPeerTracker() // reset peers
n.closed.Set(true) // mark network as closed
}
func (n *network) SetRequestHandler(handler message.RequestHandler) {
n.lock.Lock()
defer n.lock.Unlock()
n.appRequestHandler = handler
}
func (n *network) Size() uint32 {
n.lock.RLock()
defer n.lock.RUnlock()
return uint32(n.peers.Size())
}
func (n *network) TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64) {
n.lock.Lock()
defer n.lock.Unlock()
n.peers.TrackBandwidth(nodeID, bandwidth)
}
func (n *network) NewClient(protocol uint64, options ...p2p.ClientOption) *p2p.Client {
return n.p2pNetwork.NewClient(protocol, options...)
}
func (n *network) AddHandler(protocol uint64, handler p2p.Handler) error {
return n.p2pNetwork.AddHandler(protocol, handler)
}
// invariant: peer/network must use explicitly even request ids.
// for this reason, [n.requestID] is initialized as zero and incremented by 2.
// This is for backwards-compatibility while the SDK router exists with the
// legacy geth handlers to avoid a (very) narrow edge case where request ids
// can overlap, resulting in a dropped timeout.
func (n *network) nextRequestID() uint32 {
next := n.requestIDGen
n.requestIDGen += 2
return next
}
// IsNetworkRequest checks if the given requestID is a request for this network handler (even-numbered requestIDs)
// SDK requests are odd-numbered requestIDs
// (see invariant: https://github.com/luxfi/node/blob/v1.13.0/network/p2p/router.go#L83)
func IsNetworkRequest(requestID uint32) bool {
return requestID%2 == 0
}
-760
View File
@@ -1,760 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/luxfi/node/network/p2p"
"github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/utils/logging"
"github.com/luxfi/node/utils/set"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/prometheus/client_golang/prometheus"
"github.com/luxfi/evm/plugin/evm/message"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/luxfi/node/codec"
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/version"
)
var (
defaultPeerVersion = &version.Application{
Major: 1,
Minor: 0,
Patch: 0,
}
_ message.Request = &HelloRequest{}
_ = &HelloResponse{}
_ = &GreetingRequest{}
_ = &GreetingResponse{}
_ = &TestMessage{}
_ message.RequestHandler = &HelloGreetingRequestHandler{}
_ message.RequestHandler = &testRequestHandler{}
_ common.AppSender = testAppSender{}
_ p2p.Handler = &testSDKHandler{}
)
func TestNetworkDoesNotConnectToItself(t *testing.T) {
selfNodeID := ids.GenerateTestNodeID()
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
n := NewNetwork(p2pNetwork, nil, nil, selfNodeID, 1)
assert.NoError(t, n.Connected(context.Background(), selfNodeID, defaultPeerVersion))
assert.EqualValues(t, 0, n.Size())
}
func TestRequestAnyRequestsRoutingAndResponse(t *testing.T) {
callNum := uint32(0)
senderWg := &sync.WaitGroup{}
var net Network
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], requestID uint32, requestBytes []byte) error {
nodeID, _ := nodes.Pop()
senderWg.Add(1)
go func() {
defer senderWg.Done()
if err := net.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(5*time.Second), requestBytes); err != nil {
panic(err)
}
}()
return nil
},
sendAppResponseFn: func(nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
senderWg.Add(1)
go func() {
defer senderWg.Done()
if err := net.AppResponse(context.Background(), nodeID, requestID, responseBytes); err != nil {
panic(err)
}
atomic.AddUint32(&callNum, 1)
}()
return nil
},
}
codecManager := buildCodec(t, HelloRequest{}, HelloResponse{})
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net = NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 16)
net.SetRequestHandler(&HelloGreetingRequestHandler{codec: codecManager})
client := NewNetworkClient(net)
nodeID := ids.GenerateTestNodeID()
assert.NoError(t, net.Connected(context.Background(), nodeID, defaultPeerVersion))
requestMessage := HelloRequest{Message: "this is a request"}
defer net.Shutdown()
assert.NoError(t, net.Connected(context.Background(), nodeID, defaultPeerVersion))
totalRequests := 5000
numCallsPerRequest := 1 // on sending response
totalCalls := totalRequests * numCallsPerRequest
requestWg := &sync.WaitGroup{}
requestWg.Add(totalCalls)
for i := 0; i < totalCalls; i++ {
go func(wg *sync.WaitGroup) {
defer wg.Done()
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
assert.NoError(t, err)
responseBytes, _, err := client.SendAppRequestAny(context.Background(), defaultPeerVersion, requestBytes)
assert.NoError(t, err)
assert.NotNil(t, responseBytes)
var response TestMessage
if _, err = codecManager.Unmarshal(responseBytes, &response); err != nil {
panic(fmt.Errorf("unexpected error during unmarshal: %w", err))
}
assert.Equal(t, "Hi", response.Message)
}(requestWg)
}
requestWg.Wait()
senderWg.Wait()
assert.Equal(t, totalCalls, int(atomic.LoadUint32(&callNum)))
}
func TestAppRequestOnCtxCancellation(t *testing.T) {
codecManager := buildCodec(t, HelloRequest{}, HelloResponse{})
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], requestID uint32, requestBytes []byte) error {
return nil
},
sendAppResponseFn: func(nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
return nil
},
}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net := NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
net.SetRequestHandler(&HelloGreetingRequestHandler{codec: codecManager})
requestMessage := HelloRequest{Message: "this is a request"}
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
assert.NoError(t, err)
nodeID := ids.GenerateTestNodeID()
ctx, cancel := context.WithCancel(context.Background())
// cancel context prior to sending
cancel()
client := NewNetworkClient(net)
_, err = client.SendAppRequest(ctx, nodeID, requestBytes)
assert.ErrorIs(t, err, context.Canceled)
}
func TestRequestRequestsRoutingAndResponse(t *testing.T) {
callNum := uint32(0)
senderWg := &sync.WaitGroup{}
var net Network
var lock sync.Mutex
contactedNodes := make(map[ids.NodeID]struct{})
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], requestID uint32, requestBytes []byte) error {
nodeID, _ := nodes.Pop()
lock.Lock()
contactedNodes[nodeID] = struct{}{}
lock.Unlock()
senderWg.Add(1)
go func() {
defer senderWg.Done()
if err := net.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(5*time.Second), requestBytes); err != nil {
panic(err)
}
}()
return nil
},
sendAppResponseFn: func(nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
senderWg.Add(1)
go func() {
defer senderWg.Done()
if err := net.AppResponse(context.Background(), nodeID, requestID, responseBytes); err != nil {
panic(err)
}
atomic.AddUint32(&callNum, 1)
}()
return nil
},
}
codecManager := buildCodec(t, HelloRequest{}, HelloResponse{})
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net = NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 16)
net.SetRequestHandler(&HelloGreetingRequestHandler{codec: codecManager})
client := NewNetworkClient(net)
nodes := []ids.NodeID{
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
}
for _, nodeID := range nodes {
assert.NoError(t, net.Connected(context.Background(), nodeID, defaultPeerVersion))
}
requestMessage := HelloRequest{Message: "this is a request"}
defer net.Shutdown()
totalRequests := 5000
numCallsPerRequest := 1 // on sending response
totalCalls := totalRequests * numCallsPerRequest
requestWg := &sync.WaitGroup{}
requestWg.Add(totalCalls)
nodeIdx := 0
for i := 0; i < totalCalls; i++ {
nodeIdx = (nodeIdx + 1) % (len(nodes))
nodeID := nodes[nodeIdx]
go func(wg *sync.WaitGroup, nodeID ids.NodeID) {
defer wg.Done()
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
assert.NoError(t, err)
responseBytes, err := client.SendAppRequest(context.Background(), nodeID, requestBytes)
assert.NoError(t, err)
assert.NotNil(t, responseBytes)
var response TestMessage
if _, err = codecManager.Unmarshal(responseBytes, &response); err != nil {
panic(fmt.Errorf("unexpected error during unmarshal: %w", err))
}
assert.Equal(t, "Hi", response.Message)
}(requestWg, nodeID)
}
requestWg.Wait()
senderWg.Wait()
assert.Equal(t, totalCalls, int(atomic.LoadUint32(&callNum)))
for _, nodeID := range nodes {
if _, exists := contactedNodes[nodeID]; !exists {
t.Fatalf("expected nodeID %s to be contacted but was not", nodeID)
}
}
// ensure empty nodeID is not allowed
_, err = client.SendAppRequest(context.Background(), ids.EmptyNodeID, []byte("hello there"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot send request to empty nodeID")
}
func TestAppRequestOnShutdown(t *testing.T) {
var (
net Network
wg sync.WaitGroup
called bool
)
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], requestID uint32, requestBytes []byte) error {
wg.Add(1)
go func() {
called = true
// shutdown the network here to ensure any outstanding requests are handled as failed
net.Shutdown()
wg.Done()
}() // this is on a goroutine to avoid a deadlock since calling Shutdown takes the lock.
return nil
},
}
codecManager := buildCodec(t, HelloRequest{}, HelloResponse{})
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net = NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
client := NewNetworkClient(net)
nodeID := ids.GenerateTestNodeID()
require.NoError(t, net.Connected(context.Background(), nodeID, defaultPeerVersion))
requestMessage := HelloRequest{Message: "this is a request"}
require.NoError(t, net.Connected(context.Background(), nodeID, defaultPeerVersion))
wg.Add(1)
go func() {
defer wg.Done()
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
require.NoError(t, err)
responseBytes, _, err := client.SendAppRequestAny(context.Background(), defaultPeerVersion, requestBytes)
require.Error(t, err, ErrRequestFailed)
require.Nil(t, responseBytes)
}()
wg.Wait()
require.True(t, called)
}
func TestAppRequestAnyOnCtxCancellation(t *testing.T) {
codecManager := buildCodec(t, HelloRequest{}, HelloResponse{})
type reqInfo struct {
nodeID ids.NodeID
requestID uint32
}
sentAppRequest := make(chan reqInfo, 1)
sender := testAppSender{
sendAppRequestFn: func(ctx context.Context, nodes set.Set[ids.NodeID], requestID uint32, requestBytes []byte) error {
if err := ctx.Err(); err != nil {
return err
}
assert.Len(t, nodes, 1)
sentAppRequest <- reqInfo{
nodeID: nodes.List()[0],
requestID: requestID,
}
return nil
},
sendAppResponseFn: func(nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
return nil
},
}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net := NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
net.SetRequestHandler(&HelloGreetingRequestHandler{codec: codecManager})
assert.NoError(t,
net.Connected(
context.Background(),
ids.GenerateTestNodeID(),
version.CurrentApp,
),
)
requestMessage := HelloRequest{Message: "this is a request"}
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
assert.NoError(t, err)
// cancel context prior to sending
ctx, cancel := context.WithCancel(context.Background())
cancel()
client := NewNetworkClient(net)
_, _, err = client.SendAppRequestAny(ctx, defaultPeerVersion, requestBytes)
assert.ErrorIs(t, err, context.Canceled)
// Assert we didn't send anything
select {
case <-sentAppRequest:
assert.FailNow(t, "should not have sent request")
default:
}
// Cancel context after sending
assert.Empty(t, net.(*network).outstandingRequestHandlers) // no outstanding requests
ctx, cancel = context.WithCancel(context.Background())
doneChan := make(chan struct{})
go func() {
_, _, err = client.SendAppRequestAny(ctx, defaultPeerVersion, requestBytes)
assert.ErrorIs(t, err, context.Canceled)
close(doneChan)
}()
// Wait until we've "sent" the app request over the network
// before cancelling context.
sentAppRequestInfo := <-sentAppRequest
assert.Len(t, net.(*network).outstandingRequestHandlers, 1)
cancel()
<-doneChan
// Should still be able to process a response after cancelling.
assert.Len(t, net.(*network).outstandingRequestHandlers, 1) // context cancellation SendAppRequestAny failure doesn't clear
err = net.AppResponse(context.Background(), sentAppRequestInfo.nodeID, sentAppRequestInfo.requestID, []byte{})
assert.NoError(t, err)
assert.Empty(t, net.(*network).outstandingRequestHandlers) // Received response
}
func TestRequestMinVersion(t *testing.T) {
callNum := uint32(0)
nodeID := ids.GenerateTestNodeID()
codecManager := buildCodec(t, TestMessage{})
var net Network
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], reqID uint32, messageBytes []byte) error {
atomic.AddUint32(&callNum, 1)
assert.True(t, nodes.Contains(nodeID), "request nodes should contain expected nodeID")
assert.Len(t, nodes, 1, "request nodes should contain exactly one node")
go func() {
time.Sleep(200 * time.Millisecond)
atomic.AddUint32(&callNum, 1)
responseBytes, err := codecManager.Marshal(message.Version, TestMessage{Message: "this is a response"})
if err != nil {
panic(err)
}
err = net.AppResponse(context.Background(), nodeID, reqID, responseBytes)
assert.NoError(t, err)
}()
return nil
},
}
// passing nil as codec works because the net.AppRequest is never called
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net = NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
client := NewNetworkClient(net)
requestMessage := TestMessage{Message: "this is a request"}
requestBytes, err := message.RequestToBytes(codecManager, requestMessage)
assert.NoError(t, err)
assert.NoError(t,
net.Connected(
context.Background(),
nodeID,
&version.Application{
Name: version.Client,
Major: 1,
Minor: 7,
Patch: 1,
},
),
)
// ensure version does not match
responseBytes, _, err := client.SendAppRequestAny(
context.Background(),
&version.Application{
Name: version.Client,
Major: 2,
Minor: 0,
Patch: 0,
},
requestBytes,
)
assert.Equal(t, err.Error(), "no peers found matching version lux/2.0.0 out of 1 peers")
assert.Nil(t, responseBytes)
// ensure version matches and the request goes through
responseBytes, _, err = client.SendAppRequestAny(context.Background(), defaultPeerVersion, requestBytes)
assert.NoError(t, err)
var response TestMessage
if _, err = codecManager.Unmarshal(responseBytes, &response); err != nil {
t.Fatal("unexpected error during unmarshal", err)
}
assert.Equal(t, "this is a response", response.Message)
}
func TestOnRequestHonoursDeadline(t *testing.T) {
var net Network
responded := false
sender := testAppSender{
sendAppRequestFn: func(_ context.Context, nodes set.Set[ids.NodeID], reqID uint32, message []byte) error {
return nil
},
sendAppResponseFn: func(nodeID ids.NodeID, reqID uint32, message []byte) error {
responded = true
return nil
},
}
codecManager := buildCodec(t, TestMessage{})
requestBytes, err := marshalStruct(codecManager, TestMessage{Message: "hello there"})
assert.NoError(t, err)
requestHandler := &testRequestHandler{
processingDuration: 500 * time.Millisecond,
}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
net = NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
net.SetRequestHandler(requestHandler)
nodeID := ids.GenerateTestNodeID()
requestHandler.response, err = marshalStruct(codecManager, TestMessage{Message: "hi there"})
assert.NoError(t, err)
assert.NoError(t, net.AppRequest(context.Background(), nodeID, 0, time.Now().Add(1*time.Millisecond), requestBytes))
// ensure the handler didn't get called (as peer.Network would've dropped the request)
assert.EqualValues(t, requestHandler.calls, 0)
requestHandler.processingDuration = 0
err = net.AppRequest(context.Background(), nodeID, 2, time.Now().Add(250*time.Millisecond), requestBytes)
assert.NoError(t, err)
assert.True(t, responded)
assert.EqualValues(t, requestHandler.calls, 1)
}
func TestHandleInvalidMessages(t *testing.T) {
codecManager := buildCodec(t, HelloGossip{}, TestMessage{})
nodeID := ids.GenerateTestNodeID()
requestID := peertest.TestSDKRequestID
sender := &enginetest.Sender{
SendAppErrorF: func(context.Context, ids.NodeID, uint32, int32, string) error {
return nil
},
}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, sender, prometheus.NewRegistry(), "")
require.NoError(t, err)
clientNetwork := NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
clientNetwork.SetRequestHandler(&testRequestHandler{})
assert.NoError(t, clientNetwork.Connected(context.Background(), nodeID, defaultPeerVersion))
defer clientNetwork.Shutdown()
// Ensure a valid gossip message sent as any App specific message type does not trigger a fatal error
marshaller := helloGossipMarshaller{codec: codecManager}
gossipMsg, err := marshaller.MarshalGossip(&HelloGossip{Msg: "hello there!"})
assert.NoError(t, err)
// Ensure a valid request message sent as any App specific message type does not trigger a fatal error
requestMessage, err := marshalStruct(codecManager, TestMessage{Message: "Hello"})
assert.NoError(t, err)
// Ensure a random message sent as any App specific message type does not trigger a fatal error
garbageResponse := make([]byte, 10)
// Ensure a zero-length message sent as any App specific message type does not trigger a fatal error
emptyResponse := make([]byte, 0)
// Ensure a nil byte slice sent as any App specific message type does not trigger a fatal error
var nilResponse []byte
// Check for edge cases
assert.NoError(t, clientNetwork.AppGossip(context.Background(), nodeID, gossipMsg))
assert.NoError(t, clientNetwork.AppGossip(context.Background(), nodeID, requestMessage))
assert.NoError(t, clientNetwork.AppGossip(context.Background(), nodeID, garbageResponse))
assert.NoError(t, clientNetwork.AppGossip(context.Background(), nodeID, emptyResponse))
assert.NoError(t, clientNetwork.AppGossip(context.Background(), nodeID, nilResponse))
assert.NoError(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), gossipMsg))
assert.NoError(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), requestMessage))
assert.NoError(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), garbageResponse))
assert.NoError(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), emptyResponse))
assert.NoError(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), nilResponse))
assert.ErrorIs(t, p2p.ErrUnrequestedResponse, clientNetwork.AppResponse(context.Background(), nodeID, requestID, gossipMsg))
assert.ErrorIs(t, p2p.ErrUnrequestedResponse, clientNetwork.AppResponse(context.Background(), nodeID, requestID, requestMessage))
assert.ErrorIs(t, p2p.ErrUnrequestedResponse, clientNetwork.AppResponse(context.Background(), nodeID, requestID, garbageResponse))
assert.ErrorIs(t, p2p.ErrUnrequestedResponse, clientNetwork.AppResponse(context.Background(), nodeID, requestID, emptyResponse))
assert.ErrorIs(t, p2p.ErrUnrequestedResponse, clientNetwork.AppResponse(context.Background(), nodeID, requestID, nilResponse))
}
func TestNetworkPropagatesRequestHandlerError(t *testing.T) {
codecManager := buildCodec(t, TestMessage{})
nodeID := ids.GenerateTestNodeID()
requestID := peertest.TestPeerRequestID
sender := testAppSender{}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, nil, prometheus.NewRegistry(), "")
require.NoError(t, err)
clientNetwork := NewNetwork(p2pNetwork, sender, codecManager, ids.EmptyNodeID, 1)
clientNetwork.SetRequestHandler(&testRequestHandler{err: errors.New("fail")}) // Return an error from the request handler
assert.NoError(t, clientNetwork.Connected(context.Background(), nodeID, defaultPeerVersion))
defer clientNetwork.Shutdown()
// Ensure a valid request message sent as any App specific message type does not trigger a fatal error
requestMessage, err := marshalStruct(codecManager, TestMessage{Message: "Hello"})
assert.NoError(t, err)
// Check that if the request handler returns an error, it is propagated as a fatal error.
assert.Error(t, clientNetwork.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(time.Second), requestMessage))
}
func TestNetworkAppRequestAfterShutdown(t *testing.T) {
require := require.New(t)
net := NewNetwork(nil, nil, nil, ids.EmptyNodeID, 1)
net.Shutdown()
require.NoError(net.SendAppRequest(context.Background(), ids.GenerateTestNodeID(), nil, nil))
require.NoError(net.SendAppRequest(context.Background(), ids.GenerateTestNodeID(), nil, nil))
}
func TestNetworkRouting(t *testing.T) {
require := require.New(t)
sender := &testAppSender{
sendAppRequestFn: func(_ context.Context, s set.Set[ids.NodeID], u uint32, bytes []byte) error {
return nil
},
sendAppResponseFn: func(id ids.NodeID, u uint32, bytes []byte) error {
return nil
},
}
protocol := 0
requestID := peertest.TestSDKRequestID
handler := &testSDKHandler{}
p2pNetwork, err := p2p.NewNetwork(logging.NoLog{}, sender, prometheus.NewRegistry(), "")
require.NoError(err)
require.NoError(p2pNetwork.AddHandler(uint64(protocol), handler))
networkCodec := codec.NewManager(0)
network := NewNetwork(p2pNetwork, nil, networkCodec, ids.EmptyNodeID, 1)
nodeID := ids.GenerateTestNodeID()
foobar := append([]byte{byte(protocol)}, []byte("foobar")...)
// forward it to the sdk handler
require.NoError(network.AppRequest(context.Background(), nodeID, requestID, time.Now().Add(5*time.Second), foobar))
require.True(handler.appRequested)
err = network.AppResponse(context.Background(), ids.GenerateTestNodeID(), requestID, foobar)
require.ErrorIs(err, p2p.ErrUnrequestedResponse)
err = network.AppRequestFailed(context.Background(), nodeID, requestID, common.ErrTimeout)
require.ErrorIs(err, p2p.ErrUnrequestedResponse)
}
func buildCodec(t *testing.T, types ...interface{}) codec.Manager {
codecManager := codec.NewDefaultManager()
c := linearcodec.NewDefault()
for _, typ := range types {
assert.NoError(t, c.RegisterType(typ))
}
assert.NoError(t, codecManager.RegisterCodec(message.Version, c))
return codecManager
}
// marshalStruct is a helper method used to marshal an object as `interface{}`
// so that the codec is able to include the TypeID in the resulting bytes
func marshalStruct(codec codec.Manager, obj interface{}) ([]byte, error) {
return codec.Marshal(message.Version, &obj)
}
type testAppSender struct {
sendAppRequestFn func(context.Context, set.Set[ids.NodeID], uint32, []byte) error
sendAppResponseFn func(ids.NodeID, uint32, []byte) error
sendAppGossipFn func(common.SendConfig, []byte) error
}
func (t testAppSender) SendAppRequest(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, message []byte) error {
return t.sendAppRequestFn(ctx, nodeIDs, requestID, message)
}
func (t testAppSender) SendAppResponse(_ context.Context, nodeID ids.NodeID, requestID uint32, message []byte) error {
return t.sendAppResponseFn(nodeID, requestID, message)
}
func (t testAppSender) SendAppGossip(_ context.Context, config common.SendConfig, message []byte) error {
return t.sendAppGossipFn(config, message)
}
func (t testAppSender) SendAppError(ctx context.Context, nodeID ids.NodeID, requestID uint32, errorCode int32, errorMessage string) error {
panic("not implemented")
}
type HelloRequest struct {
Message string `serialize:"true"`
}
func (h HelloRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler message.RequestHandler) ([]byte, error) {
// casting is only necessary for test since RequestHandler does not implement anything at the moment
return handler.(TestRequestHandler).HandleHelloRequest(ctx, nodeID, requestID, &h)
}
func (h HelloRequest) String() string {
return fmt.Sprintf("HelloRequest(%s)", h.Message)
}
type GreetingRequest struct {
Greeting string `serialize:"true"`
}
func (g GreetingRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler message.RequestHandler) ([]byte, error) {
// casting is only necessary for test since RequestHandler does not implement anything at the moment
return handler.(TestRequestHandler).HandleGreetingRequest(ctx, nodeID, requestID, &g)
}
func (g GreetingRequest) String() string {
return fmt.Sprintf("GreetingRequest(%s)", g.Greeting)
}
type HelloResponse struct {
Response string `serialize:"true"`
}
type GreetingResponse struct {
Greet string `serialize:"true"`
}
type TestRequestHandler interface {
HandleHelloRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request *HelloRequest) ([]byte, error)
HandleGreetingRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request *GreetingRequest) ([]byte, error)
}
type HelloGreetingRequestHandler struct {
message.RequestHandler
codec codec.Manager
}
func (h *HelloGreetingRequestHandler) HandleHelloRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request *HelloRequest) ([]byte, error) {
return h.codec.Marshal(message.Version, HelloResponse{Response: "Hi"})
}
func (h *HelloGreetingRequestHandler) HandleGreetingRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request *GreetingRequest) ([]byte, error) {
return h.codec.Marshal(message.Version, GreetingResponse{Greet: "Hey there"})
}
type TestMessage struct {
Message string `serialize:"true"`
}
func (t TestMessage) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler message.RequestHandler) ([]byte, error) {
return handler.(*testRequestHandler).handleTestRequest(ctx, nodeID, requestID, &t)
}
func (t TestMessage) String() string {
return fmt.Sprintf("TestMessage(%s)", t.Message)
}
type HelloGossip struct {
Msg string `serialize:"true"`
}
func (tx *HelloGossip) GossipID() ids.ID {
return ids.FromStringOrPanic(tx.Msg)
}
type helloGossipMarshaller struct {
codec codec.Manager
}
func (g helloGossipMarshaller) MarshalGossip(tx *HelloGossip) ([]byte, error) {
return g.codec.Marshal(0, tx)
}
func (g helloGossipMarshaller) UnmarshalGossip(bytes []byte) (*HelloGossip, error) {
h := &HelloGossip{}
_, err := g.codec.Unmarshal(bytes, h)
return h, err
}
type testRequestHandler struct {
message.RequestHandler
calls uint32
processingDuration time.Duration
response []byte
err error
}
func (r *testRequestHandler) handleTestRequest(ctx context.Context, _ ids.NodeID, _ uint32, _ *TestMessage) ([]byte, error) {
r.calls++
select {
case <-time.After(r.processingDuration):
break
case <-ctx.Done():
return nil, ctx.Err()
}
return r.response, r.err
}
type testSDKHandler struct {
appRequested bool
}
func (t *testSDKHandler) AppGossip(ctx context.Context, nodeID ids.NodeID, gossipBytes []byte) {
// TODO implement me
panic("implement me")
}
func (t *testSDKHandler) AppRequest(ctx context.Context, nodeID ids.NodeID, deadline time.Time, requestBytes []byte) ([]byte, *common.AppError) {
t.appRequested = true
return nil, nil
}
-197
View File
@@ -1,197 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"math"
"math/rand"
"time"
"github.com/luxfi/evm/interfaces"
utils_math "github.com/luxfi/node/utils/math"
"github.com/luxfi/node/utils/set"
"github.com/luxfi/node/version"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
const (
bandwidthHalflife = 5 * time.Minute
// controls how eagerly we connect to new peers vs. using
// peers with known good response bandwidth.
desiredMinResponsivePeers = 20
newPeerConnectFactor = 0.1
// controls how often we prefer a random responsive peer over the most
// performant peer.
randomPeerProbability = 0.2
)
// information we track on a given peer
type peerInfo struct {
version *version.Application
bandwidth utils_math.Averager
}
// peerTracker tracks the bandwidth of responses coming from peers,
// preferring to contact peers with known good bandwidth, connecting
// to new peers with an exponentially decaying probability.
// Note: is not thread safe, caller must handle synchronization.
type peerTracker struct {
peers map[interfaces.NodeID]*peerInfo // all peers we are connected to
numTrackedPeers metrics.Gauge
trackedPeers set.Set[interfaces.NodeID] // peers that we have sent a request to
numResponsivePeers metrics.Gauge
responsivePeers set.Set[interfaces.NodeID] // peers that responded to the last request they were sent
bandwidthHeap utils_math.AveragerHeap // tracks bandwidth peers are responding with
averageBandwidthMetric metrics.GaugeFloat64
averageBandwidth utils_math.Averager
}
func NewPeerTracker() *peerTracker {
return &peerTracker{
peers: make(map[interfaces.NodeID]*peerInfo),
numTrackedPeers: metrics.NewGauge(),
trackedPeers: make(set.Set[interfaces.NodeID]),
numResponsivePeers: metrics.NewGauge(),
responsivePeers: make(set.Set[interfaces.NodeID]),
bandwidthHeap: utils_math.NewMaxAveragerHeap(),
averageBandwidthMetric: metrics.NewGaugeFloat64(),
averageBandwidth: utils_math.NewAverager(0, bandwidthHalflife, time.Now()),
}
}
// shouldTrackNewPeer returns true if we are not connected to enough peers.
// otherwise returns true probabilistically based on the number of tracked peers.
func (p *peerTracker) shouldTrackNewPeer() bool {
numResponsivePeers := p.responsivePeers.Len()
if numResponsivePeers < desiredMinResponsivePeers {
return true
}
if len(p.trackedPeers) >= len(p.peers) {
// already tracking all the peers
return false
}
newPeerProbability := math.Exp(-float64(numResponsivePeers) * newPeerConnectFactor)
return rand.Float64() < newPeerProbability
}
// getResponsivePeer returns a random [interfaces.NodeID] of a peer that has responded
// to a request.
func (p *peerTracker) getResponsivePeer() (interfaces.NodeID, utils_math.Averager, bool) {
nodeID, ok := p.responsivePeers.Peek()
if !ok {
return interfaces.NodeID{}, nil, false
}
averager, ok := p.bandwidthHeap.Remove(nodeID)
if ok {
return nodeID, averager, true
}
peer := p.peers[nodeID]
return nodeID, peer.bandwidth, true
}
func (p *peerTracker) GetAnyPeer(minVersion *version.Application) (interfaces.NodeID, bool) {
if p.shouldTrackNewPeer() {
for nodeID := range p.peers {
// if minVersion is specified and peer's version is less, skip
if minVersion != nil && p.peers[nodeID].version.Compare(minVersion) < 0 {
continue
}
// skip peers already tracked
if p.trackedPeers.Contains(nodeID) {
continue
}
log.Debug("peer tracking: connecting to new peer", "trackedPeers", len(p.trackedPeers), "nodeID", nodeID)
return nodeID, true
}
}
var (
nodeID interfaces.NodeID
ok bool
random bool
averager utils_math.Averager
)
if rand.Float64() < randomPeerProbability {
random = true
nodeID, averager, ok = p.getResponsivePeer()
} else {
nodeID, averager, ok = p.bandwidthHeap.Pop()
}
if ok {
log.Debug("peer tracking: popping peer", "nodeID", nodeID, "bandwidth", averager.Read(), "random", random)
return nodeID, true
}
// if no nodes found in the bandwidth heap, return a tracked node at random
return p.trackedPeers.Peek()
}
func (p *peerTracker) TrackPeer(nodeID interfaces.NodeID) {
p.trackedPeers.Add(nodeID)
p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
}
func (p *peerTracker) TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64) {
peer := p.peers[nodeID]
if peer == nil {
// we're not connected to this peer, nothing to do here
log.Debug("tracking bandwidth for untracked peer", "nodeID", nodeID)
return
}
now := time.Now()
if peer.bandwidth == nil {
peer.bandwidth = utils_math.NewAverager(bandwidth, bandwidthHalflife, now)
} else {
peer.bandwidth.Observe(bandwidth, now)
}
p.bandwidthHeap.Add(nodeID, peer.bandwidth)
if bandwidth == 0 {
p.responsivePeers.Remove(nodeID)
} else {
p.responsivePeers.Add(nodeID)
p.averageBandwidth.Observe(bandwidth, now)
p.averageBandwidthMetric.Update(p.averageBandwidth.Read())
}
p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
}
// Connected should be called when [nodeID] connects to this node
func (p *peerTracker) Connected(nodeID interfaces.NodeID, nodeVersion *version.Application) {
if peer := p.peers[nodeID]; peer != nil {
// Peer is already connected, update the version if it has changed.
// Log a warning message since the consensus engine should never call Connected on a peer
// that we have already marked as Connected.
if nodeVersion.Compare(peer.version) != 0 {
p.peers[nodeID] = &peerInfo{
version: nodeVersion,
bandwidth: peer.bandwidth,
}
log.Warn("updating node version of already connected peer", "nodeID", nodeID, "storedVersion", peer.version, "nodeVersion", nodeVersion)
} else {
log.Warn("ignoring peer connected event for already connected peer with identical version", "nodeID", nodeID)
}
return
}
p.peers[nodeID] = &peerInfo{
version: nodeVersion,
}
}
// Disconnected should be called when [nodeID] disconnects from this node
func (p *peerTracker) Disconnected(nodeID interfaces.NodeID) {
p.bandwidthHeap.Remove(nodeID)
p.trackedPeers.Remove(nodeID)
p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
p.responsivePeers.Remove(nodeID)
p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
delete(p.peers, nodeID)
}
// Size returns the number of peers the node is connected to
func (p *peerTracker) Size() int {
return len(p.peers)
}
-197
View File
@@ -1,197 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"math"
"math/rand"
"time"
"github.com/luxfi/evm/interfaces"
utils_math "github.com/luxfi/node/utils/math"
"github.com/luxfi/evm/utils"
"github.com/luxfi/node/version"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
const (
bandwidthHalflife = 5 * time.Minute
// controls how eagerly we connect to new peers vs. using
// peers with known good response bandwidth.
desiredMinResponsivePeers = 20
newPeerConnectFactor = 0.1
// controls how often we prefer a random responsive peer over the most
// performant peer.
randomPeerProbability = 0.2
)
// information we track on a given peer
type peerInfo struct {
version *version.Application
bandwidth utils_math.Averager
}
// peerTracker tracks the bandwidth of responses coming from peers,
// preferring to contact peers with known good bandwidth, connecting
// to new peers with an exponentially decaying probability.
// Note: is not thread safe, caller must handle synchronization.
type peerTracker struct {
peers map[interfaces.NodeID]*peerInfo // all peers we are connected to
numTrackedPeers metrics.Gauge
trackedPeers interfaces.Set[interfaces.NodeID] // peers that we have sent a request to
numResponsivePeers metrics.Gauge
responsivePeers interfaces.Set[interfaces.NodeID] // peers that responded to the last request they were sent
bandwidthHeap utils_math.AveragerHeap // tracks bandwidth peers are responding with
averageBandwidthMetric metrics.GaugeFloat64
averageBandwidth utils_math.Averager
}
func NewPeerTracker() *peerTracker {
return &peerTracker{
peers: make(map[interfaces.NodeID]*peerInfo),
numTrackedPeers: metrics.NewGauge(),
trackedPeers: make(interfaces.Set[interfaces.NodeID]),
numResponsivePeers: metrics.NewGauge(),
responsivePeers: make(interfaces.Set[interfaces.NodeID]),
bandwidthHeap: utils_math.NewMaxAveragerHeap(),
averageBandwidthMetric: metrics.NewGaugeFloat64(),
averageBandwidth: utils_math.NewAverager(0, bandwidthHalflife, time.Now()),
}
}
// shouldTrackNewPeer returns true if we are not connected to enough peers.
// otherwise returns true probabilistically based on the number of tracked peers.
func (p *peerTracker) shouldTrackNewPeer() bool {
numResponsivePeers := p.responsivePeers.Len()
if numResponsivePeers < desiredMinResponsivePeers {
return true
}
if len(p.trackedPeers) >= len(p.peers) {
// already tracking all the peers
return false
}
newPeerProbability := math.Exp(-float64(numResponsivePeers) * newPeerConnectFactor)
return rand.Float64() < newPeerProbability
}
// getResponsivePeer returns a random [interfaces.NodeID] of a peer that has responded
// to a request.
func (p *peerTracker) getResponsivePeer() (interfaces.NodeID, utils_math.Averager, bool) {
nodeID, ok := p.responsivePeers.Peek()
if !ok {
return interfaces.NodeID{}, nil, false
}
averager, ok := p.bandwidthHeap.Remove(nodeID)
if ok {
return nodeID, averager, true
}
peer := p.peers[nodeID]
return nodeID, peer.bandwidth, true
}
func (p *peerTracker) GetAnyPeer(minVersion *version.Application) (interfaces.NodeID, bool) {
if p.shouldTrackNewPeer() {
for nodeID := range p.peers {
// if minVersion is specified and peer's version is less, skip
if minVersion != nil && p.peers[nodeID].version.Compare(minVersion) < 0 {
continue
}
// skip peers already tracked
if p.trackedPeers.Contains(nodeID) {
continue
}
log.Debug("peer tracking: connecting to new peer", "trackedPeers", len(p.trackedPeers), "nodeID", nodeID)
return nodeID, true
}
}
var (
nodeID interfaces.NodeID
ok bool
random bool
averager utils_math.Averager
)
if rand.Float64() < randomPeerProbability {
random = true
nodeID, averager, ok = p.getResponsivePeer()
} else {
nodeID, averager, ok = p.bandwidthHeap.Pop()
}
if ok {
log.Debug("peer tracking: popping peer", "nodeID", nodeID, "bandwidth", averager.Read(), "random", random)
return nodeID, true
}
// if no nodes found in the bandwidth heap, return a tracked node at random
return p.trackedPeers.Peek()
}
func (p *peerTracker) TrackPeer(nodeID interfaces.NodeID) {
p.trackedPeers.Add(nodeID)
p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
}
func (p *peerTracker) TrackBandwidth(nodeID interfaces.NodeID, bandwidth float64) {
peer := p.peers[nodeID]
if peer == nil {
// we're not connected to this peer, nothing to do here
log.Debug("tracking bandwidth for untracked peer", "nodeID", nodeID)
return
}
now := time.Now()
if peer.bandwidth == nil {
peer.bandwidth = utils_math.NewAverager(bandwidth, bandwidthHalflife, now)
} else {
peer.bandwidth.Observe(bandwidth, now)
}
p.bandwidthHeap.Add(nodeID, peer.bandwidth)
if bandwidth == 0 {
p.responsivePeers.Remove(nodeID)
} else {
p.responsivePeers.Add(nodeID)
p.averageBandwidth.Observe(bandwidth, now)
p.averageBandwidthMetric.Update(p.averageBandwidth.Read())
}
p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
}
// Connected should be called when [nodeID] connects to this node
func (p *peerTracker) Connected(nodeID interfaces.NodeID, nodeVersion *version.Application) {
if peer := p.peers[nodeID]; peer != nil {
// Peer is already connected, update the version if it has changed.
// Log a warning message since the consensus engine should never call Connected on a peer
// that we have already marked as Connected.
if nodeVersion.Compare(peer.version) != 0 {
p.peers[nodeID] = &peerInfo{
version: nodeVersion,
bandwidth: peer.bandwidth,
}
log.Warn("updating node version of already connected peer", "nodeID", nodeID, "storedVersion", peer.version, "nodeVersion", nodeVersion)
} else {
log.Warn("ignoring peer connected event for already connected peer with identical version", "nodeID", nodeID)
}
return
}
p.peers[nodeID] = &peerInfo{
version: nodeVersion,
}
}
// Disconnected should be called when [nodeID] disconnects from this node
func (p *peerTracker) Disconnected(nodeID interfaces.NodeID) {
p.bandwidthHeap.Remove(nodeID)
p.trackedPeers.Remove(nodeID)
p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
p.responsivePeers.Remove(nodeID)
p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
delete(p.peers, nodeID)
}
// Size returns the number of peers the node is connected to
func (p *peerTracker) Size() int {
return len(p.peers)
}
-88
View File
@@ -1,88 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package peer
import (
"testing"
"github.com/luxfi/node/ids"
"github.com/stretchr/testify/require"
)
func TestPeerTracker(t *testing.T) {
require := require.New(t)
p := NewPeerTracker()
// Connect some peers
numExtraPeers := 10
numPeers := desiredMinResponsivePeers + numExtraPeers
peerIDs := make([]ids.NodeID, numPeers)
for i := range peerIDs {
peerIDs[i] = ids.GenerateTestNodeID()
p.Connected(peerIDs[i], defaultPeerVersion)
}
responsivePeers := make(map[ids.NodeID]bool)
// Expect requests to go to new peers until we have desiredMinResponsivePeers responsive peers.
for i := 0; i < desiredMinResponsivePeers+numExtraPeers/2; i++ {
peer, ok := p.GetAnyPeer(nil)
require.True(ok)
require.NotNil(peer)
_, exists := responsivePeers[peer]
require.Falsef(exists, "expected connecting to a new peer, but got the same peer twice: peer %s iteration %d", peer, i)
responsivePeers[peer] = true
p.TrackPeer(peer) // mark the peer as having a message sent to it
}
// Mark some peers as responsive and others as not responsive
i := 0
for peer := range responsivePeers {
if i < desiredMinResponsivePeers {
p.TrackBandwidth(peer, 10)
} else {
responsivePeers[peer] = false // remember which peers were not responsive
p.TrackBandwidth(peer, 0)
}
i++
}
// Expect requests to go to responsive or new peers, so long as they are available
numRequests := 50
for i := 0; i < numRequests; i++ {
peer, ok := p.GetAnyPeer(nil)
require.True(ok)
require.NotNil(peer)
responsive, ok := responsivePeers[peer]
if ok {
require.Truef(responsive, "expected connecting to a responsive peer, but got a peer that was not responsive: peer %s iteration %d", peer, i)
p.TrackBandwidth(peer, 10)
} else {
responsivePeers[peer] = false // remember that we connected to this peer
p.TrackPeer(peer) // mark the peer as having a message sent to it
p.TrackBandwidth(peer, 0) // mark the peer as non-responsive
}
}
// Disconnect from peers that were previously responsive and ones we didn't connect to yet.
for _, peer := range peerIDs {
responsive, ok := responsivePeers[peer]
if ok && responsive || !ok {
p.Disconnected(peer)
}
}
// Requests should fall back on non-responsive peers when no other choice is left
peer, ok := p.GetAnyPeer(nil)
require.True(ok)
require.NotNil(peer)
responsive, ok := responsivePeers[peer]
require.True(ok)
require.Falsef(responsive, "expected connecting to a non-responsive peer, but got a peer that was responsive: peer %s", peer)
}
-82
View File
@@ -1,82 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"fmt"
"net/http"
"github.com/luxfi/node/api"
"github.com/luxfi/node/utils/profiler"
"github.com/luxfi/evm/log"
)
// Admin is the API service for admin API calls
type Admin struct {
vm *VM
profiler profiler.Profiler
}
func NewAdminService(vm *VM, performanceDir string) *Admin {
return &Admin{
vm: vm,
profiler: profiler.New(performanceDir),
}
}
// StartCPUProfiler starts a cpu profile writing to the specified file
func (p *Admin) StartCPUProfiler(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
log.Info("Admin: StartCPUProfiler called")
p.vm.vmLock.Lock()
defer p.vm.vmLock.Unlock()
return p.profiler.StartCPUProfiler()
}
// StopCPUProfiler stops the cpu profile
func (p *Admin) StopCPUProfiler(r *http.Request, _ *struct{}, _ *api.EmptyReply) error {
log.Info("Admin: StopCPUProfiler called")
p.vm.vmLock.Lock()
defer p.vm.vmLock.Unlock()
return p.profiler.StopCPUProfiler()
}
// MemoryProfile runs a memory profile writing to the specified file
func (p *Admin) MemoryProfile(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
log.Info("Admin: MemoryProfile called")
p.vm.vmLock.Lock()
defer p.vm.vmLock.Unlock()
return p.profiler.MemoryProfile()
}
// LockProfile runs a mutex profile writing to the specified file
func (p *Admin) LockProfile(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
log.Info("Admin: LockProfile called")
p.vm.vmLock.Lock()
defer p.vm.vmLock.Unlock()
return p.profiler.LockProfile()
}
func (p *Admin) SetLogLevel(_ *http.Request, args *client.SetLogLevelArgs, reply *api.EmptyReply) error {
log.Info("EVM: SetLogLevel called", "logLevel", args.Level)
p.vm.vmLock.Lock()
defer p.vm.vmLock.Unlock()
if err := p.vm.logger.SetLogLevel(args.Level); err != nil {
return fmt.Errorf("failed to parse log level: %w ", err)
}
return nil
}
func (p *Admin) GetVMConfig(_ *http.Request, _ *struct{}, reply *client.ConfigReply) error {
reply.Config = &p.vm.config
return nil
}
+2 -2
View File
@@ -17,7 +17,7 @@ import (
"github.com/luxfi/node/chains/atomic"
"github.com/luxfi/node/ids"
"github.com/luxfi/evm/consensus"
avalancheutils "github.com/luxfi/node/utils"
luxutils "github.com/luxfi/node/utils"
"github.com/luxfi/node/utils/constants"
"github.com/luxfi/node/utils/crypto/secp256k1"
"github.com/luxfi/node/utils/math"
@@ -125,7 +125,7 @@ func (utx *UnsignedExportTx) Verify(
if !lux.IsSortedTransferableOutputs(utx.ExportedOutputs, Codec) {
return ErrOutputsNotSorted
}
if rules.IsApricotPhase1 && !avalancheutils.IsSortedAndUnique(utx.Ins) {
if rules.IsApricotPhase1 && !luxutils.IsSortedAndUnique(utx.Ins) {
return ErrInputsNotSortedUnique
}
+7 -7
View File
@@ -11,7 +11,7 @@ import (
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/node/utils"
avalancheatomic "github.com/luxfi/node/chains/atomic"
luxatomic "github.com/luxfi/node/chains/atomic"
"github.com/luxfi/node/ids"
"github.com/luxfi/evm/consensus"
"github.com/luxfi/node/utils/set"
@@ -39,7 +39,7 @@ func init() {
type TestUnsignedTx struct {
GasUsedV uint64 `serialize:"true"`
AcceptRequestsBlockchainIDV ids.ID `serialize:"true"`
AcceptRequestsV *avalancheatomic.Requests `serialize:"true"`
AcceptRequestsV *luxatomic.Requests `serialize:"true"`
VerifyV error
IDV ids.ID `serialize:"true" json:"id"`
BurnedV uint64 `serialize:"true"`
@@ -59,7 +59,7 @@ func (t *TestUnsignedTx) GasUsed(fixedFee bool) (uint64, error) { return t.GasUs
func (t *TestUnsignedTx) Verify(ctx *consensus.Context, rules params.Rules) error { return t.VerifyV }
// AtomicOps implements the UnsignedAtomicTx interface
func (t *TestUnsignedTx) AtomicOps() (ids.ID, *avalancheatomic.Requests, error) {
func (t *TestUnsignedTx) AtomicOps() (ids.ID, *luxatomic.Requests, error) {
return t.AcceptRequestsBlockchainIDV, t.AcceptRequestsV, nil
}
@@ -100,7 +100,7 @@ func GenerateTestImportTxWithGas(gasUsed uint64, burned uint64) *Tx {
GasUsedV: gasUsed,
BurnedV: burned,
AcceptRequestsBlockchainIDV: TestBlockchainID,
AcceptRequestsV: &avalancheatomic.Requests{
AcceptRequestsV: &luxatomic.Requests{
RemoveRequests: [][]byte{
utils.RandomBytes(32),
utils.RandomBytes(32),
@@ -115,7 +115,7 @@ func GenerateTestImportTx() *Tx {
UnsignedAtomicTx: &TestUnsignedTx{
IDV: ids.GenerateTestID(),
AcceptRequestsBlockchainIDV: TestBlockchainID,
AcceptRequestsV: &avalancheatomic.Requests{
AcceptRequestsV: &luxatomic.Requests{
RemoveRequests: [][]byte{
utils.RandomBytes(32),
utils.RandomBytes(32),
@@ -130,8 +130,8 @@ func GenerateTestExportTx() *Tx {
UnsignedAtomicTx: &TestUnsignedTx{
IDV: ids.GenerateTestID(),
AcceptRequestsBlockchainIDV: TestBlockchainID,
AcceptRequestsV: &avalancheatomic.Requests{
PutRequests: []*avalancheatomic.Element{
AcceptRequestsV: &luxatomic.Requests{
PutRequests: []*luxatomic.Element{
{
Key: utils.RandomBytes(16),
Value: utils.RandomBytes(24),
-272
View File
@@ -1,272 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/geth/rlp"
"github.com/luxfi/evm/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/predicate"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus/choices"
"github.com/luxfi/node/consensus/linear"
"github.com/luxfi/node/consensus/engine/linear/block"
)
var (
_ chain.Block = (*Block)(nil)
_ block.WithVerifyContext = (*Block)(nil)
)
// Block implements the chain.Block interface
type Block struct {
id ids.ID
ethBlock *types.Block
vm *VM
}
// newBlock returns a new Block wrapping the ethBlock type and implementing the chain.Block interface
func (vm *VM) newBlock(ethBlock *types.Block) *Block {
return &Block{
id: ids.ID(ethBlock.Hash()),
ethBlock: ethBlock,
vm: vm,
}
}
// ID implements the chain.Block interface
func (b *Block) ID() ids.ID { return b.id }
// Accept implements the chain.Block interface
func (b *Block) Accept(context.Context) error {
vm := b.vm
// Although returning an error from Accept is considered fatal, it is good
// practice to cleanup the batch we were modifying in the case of an error.
defer vm.versiondb.Abort()
blkID := b.ID()
log.Debug("accepting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
// Call Accept for relevant precompile logs. Note we do this prior to
// calling Accept on the blockChain so any side effects (eg warp signatures)
// take place before the accepted log is emitted to subscribers. Use of the
// sharedMemoryWriter ensures shared memory requests generated by
// precompiles are committed atomically with the vm's lastAcceptedKey.
rules := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp())
sharedMemoryWriter := NewSharedMemoryWriter()
if err := b.handlePrecompileAccept(&rules, sharedMemoryWriter); err != nil {
return err
}
if err := vm.blockChain.Accept(b.ethBlock); err != nil {
return fmt.Errorf("chain could not accept %s: %w", blkID, err)
}
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
return fmt.Errorf("failed to put %s as the last accepted block: %w", blkID, err)
}
return b.vm.versiondb.Commit()
}
// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements
// contract.Accepter
func (b *Block) handlePrecompileAccept(rules extras.Rules) error {
// Short circuit early if there are no precompile accepters to execute
if len(rules.AccepterPrecompiles) == 0 {
return nil
}
// Read receipts from disk
receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.ethBlock.Time(), b.vm.chainConfig)
// If there are no receipts, ReadReceipts may be nil, so we check the length and confirm the ReceiptHash
// is empty to ensure that missing receipts results in an error on accept.
if len(receipts) == 0 && b.ethBlock.ReceiptHash() != types.EmptyRootHash {
return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64())
}
acceptCtx := &precompileconfig.AcceptContext{
ConsensusCtx: b.vm.ctx,
Warp: b.vm.warpBackend,
}
for _, receipt := range receipts {
for logIdx, log := range receipt.Logs {
accepter, ok := rules.AccepterPrecompiles[log.Address]
if !ok {
continue
}
if err := accepter.Accept(acceptCtx, log.BlockHash, log.BlockNumber, log.TxHash, logIdx, log.Topics, log.Data); err != nil {
return err
}
}
}
return nil
}
// Reject implements the chain.Block interface
func (b *Block) Reject(context.Context) error {
blkID := b.ID()
log.Debug("rejecting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
return b.vm.blockChain.Reject(b.ethBlock)
}
// Parent implements the chain.Block interface
func (b *Block) Parent() ids.ID {
return ids.ID(b.ethBlock.ParentHash())
}
// Height implements the chain.Block interface
func (b *Block) Height() uint64 {
return b.ethBlock.NumberU64()
}
// Timestamp implements the chain.Block interface
func (b *Block) Timestamp() time.Time {
return time.Unix(int64(b.ethBlock.Time()), 0)
}
// syntacticVerify verifies that a *Block is well-formed.
func (b *Block) syntacticVerify() error {
if b == nil || b.ethBlock == nil {
return errInvalidBlock
}
header := b.ethBlock.Header()
rules := b.vm.chainConfig.LuxRules(header.Number, header.Time)
return b.vm.syntacticBlockValidator.SyntacticVerify(b, rules)
}
// Verify implements the chain.Block interface
func (b *Block) Verify(context.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.ctx,
ProposerVMBlockCtx: nil,
}, true)
}
// ShouldVerifyWithContext implements the block.WithVerifyContext interface
func (b *Block) ShouldVerifyWithContext(context.Context) (bool, error) {
predicates := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp()).Predicaters
// Short circuit early if there are no predicates to verify
if len(predicates) == 0 {
return false, nil
}
// Check if any of the transactions in the block specify a precompile that enforces a predicate, which requires
// the ProposerVMBlockCtx.
for _, tx := range b.ethBlock.Transactions() {
for _, accessTuple := range tx.AccessList() {
if _, ok := predicates[accessTuple.Address]; ok {
log.Debug("Block verification requires proposerVM context", "block", b.ID(), "height", b.Height())
return true, nil
}
}
}
log.Debug("Block verification does not require proposerVM context", "block", b.ID(), "height", b.Height())
return false, nil
}
// VerifyWithContext implements the block.WithVerifyContext interface
func (b *Block) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.ctx,
ProposerVMBlockCtx: proposerVMBlockCtx,
}, true)
}
// Verify the block is valid.
// Enforces that the predicates are valid within [predicateContext].
// Writes the block details to disk and the state to the trie manager iff writes=true.
func (b *Block) verify(predicateContext *precompileconfig.PredicateContext, writes bool) error {
if predicateContext.ProposerVMBlockCtx != nil {
log.Debug("Verifying block with context", "block", b.ID(), "height", b.Height())
} else {
log.Debug("Verifying block without context", "block", b.ID(), "height", b.Height())
}
if err := b.syntacticVerify(); err != nil {
return fmt.Errorf("syntactic block verification failed: %w", err)
}
// Only enforce predicates if the chain has already bootstrapped.
// If the chain is still bootstrapping, we can assume that all blocks we are verifying have
// been accepted by the network (so the predicate was validated by the network when the
// block was originally verified).
if b.vm.bootstrapped.Get() {
if err := b.verifyPredicates(predicateContext); err != nil {
return fmt.Errorf("failed to verify predicates: %w", err)
}
}
// The engine may call VerifyWithContext multiple times on the same block with different contexts.
// Since the engine will only call Accept/Reject once, we should only call InsertBlockManual once.
// Additionally, if a block is already in processing, then it has already passed verification and
// at this point we have checked the predicates are still valid in the different context so we
// can return nil.
if b.vm.State.IsProcessing(b.id) {
return nil
}
return b.vm.blockChain.InsertBlockManual(b.ethBlock, writes)
}
// verifyPredicates verifies the predicates in the block are valid according to predicateContext.
func (b *Block) verifyPredicates(predicateContext *precompileconfig.PredicateContext) error {
rules := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp())
switch {
case !rulesExtra.IsDurango && rulesExtra.PredicatersExist():
return errors.New("cannot enable predicates before Durango activation")
case !rulesExtra.IsDurango:
return nil
}
predicateResults := predicate.NewResults()
for _, tx := range b.ethBlock.Transactions() {
results, err := core.CheckPredicates(rules, predicateContext, tx)
if err != nil {
return err
}
predicateResults.SetTxResults(tx.Hash(), results)
}
// TODO: document required gas constraints to ensure marshalling predicate results does not error
predicateResultsBytes, err := predicateResults.Bytes()
if err != nil {
return fmt.Errorf("failed to marshal predicate results: %w", err)
}
extraData := b.ethBlock.Extra()
headerPredicateResultsBytes := header.PredicateBytesFromExtra(extraData)
if !bytes.Equal(headerPredicateResultsBytes, predicateResultsBytes) {
return fmt.Errorf("%w (remote: %x local: %x)", errInvalidHeaderPredicateResults, headerPredicateResultsBytes, predicateResultsBytes)
}
return nil
}
// Bytes implements the chain.Block interface
func (b *Block) Bytes() []byte {
res, err := rlp.EncodeToBytes(b.ethBlock)
if err != nil {
panic(err)
}
return res
}
func (b *Block) String() string { return fmt.Sprintf("EVM block, ID = %s", b.ID()) }
-272
View File
@@ -1,272 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/geth/rlp"
"github.com/luxfi/evm/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/predicate"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/choices"
"github.com/luxfi/node/consensus/linear"
"github.com/luxfi/node/consensus/engine/linear/block"
)
var (
_ chain.Block = (*Block)(nil)
_ block.WithVerifyContext = (*Block)(nil)
)
// Block implements the chain.Block interface
type Block struct {
id interfaces.ID
ethBlock *types.Block
vm *VM
}
// newBlock returns a new Block wrapping the ethBlock type and implementing the chain.Block interface
func (vm *VM) newBlock(ethBlock *types.Block) *Block {
return &Block{
id: interfaces.ID(ethBlock.Hash()),
ethBlock: ethBlock,
vm: vm,
}
}
// ID implements the chain.Block interface
func (b *Block) ID() interfaces.ID { return b.id }
// Accept implements the chain.Block interface
func (b *Block) Accept(context.Context) error {
vm := b.vm
// Although returning an error from Accept is considered fatal, it is good
// practice to cleanup the batch we were modifying in the case of an error.
defer vm.versiondb.Abort()
blkID := b.ID()
log.Debug("accepting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
// Call Accept for relevant precompile logs. Note we do this prior to
// calling Accept on the blockChain so any side effects (eg warp signatures)
// take place before the accepted log is emitted to subscribers. Use of the
// sharedMemoryWriter ensures shared memory requests generated by
// precompiles are committed atomically with the vm's lastAcceptedKey.
rules := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp())
sharedMemoryWriter := NewSharedMemoryWriter()
if err := b.handlePrecompileAccept(&rules, sharedMemoryWriter); err != nil {
return err
}
if err := vm.blockChain.Accept(b.ethBlock); err != nil {
return fmt.Errorf("chain could not accept %s: %w", blkID, err)
}
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
return fmt.Errorf("failed to put %s as the last accepted block: %w", blkID, err)
}
return b.vm.versiondb.Commit()
}
// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements
// contract.Accepter
func (b *Block) handlePrecompileAccept(rules extras.Rules) error {
// Short circuit early if there are no precompile accepters to execute
if len(rules.AccepterPrecompiles) == 0 {
return nil
}
// Read receipts from disk
receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.ethBlock.Time(), b.vm.chainConfig)
// If there are no receipts, ReadReceipts may be nil, so we check the length and confirm the ReceiptHash
// is empty to ensure that missing receipts results in an error on accept.
if len(receipts) == 0 && b.ethBlock.ReceiptHash() != types.EmptyRootHash {
return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64())
}
acceptCtx := &precompileconfig.AcceptContext{
ConsensusCtx: b.vm.ctx,
Warp: b.vm.warpBackend,
}
for _, receipt := range receipts {
for logIdx, log := range receipt.Logs {
accepter, ok := rules.AccepterPrecompiles[log.Address]
if !ok {
continue
}
if err := accepter.Accept(acceptCtx, log.BlockHash, log.BlockNumber, log.TxHash, logIdx, log.Topics, log.Data); err != nil {
return err
}
}
}
return nil
}
// Reject implements the chain.Block interface
func (b *Block) Reject(context.Context) error {
blkID := b.ID()
log.Debug("rejecting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
return b.vm.blockChain.Reject(b.ethBlock)
}
// Parent implements the chain.Block interface
func (b *Block) Parent() interfaces.ID {
return interfaces.ID(b.ethBlock.ParentHash())
}
// Height implements the chain.Block interface
func (b *Block) Height() uint64 {
return b.ethBlock.NumberU64()
}
// Timestamp implements the chain.Block interface
func (b *Block) Timestamp() time.Time {
return time.Unix(int64(b.ethBlock.Time()), 0)
}
// syntacticVerify verifies that a *Block is well-formed.
func (b *Block) syntacticVerify() error {
if b == nil || b.ethBlock == nil {
return errInvalidBlock
}
header := b.ethBlock.Header()
rules := b.vm.chainConfig.LuxRules(header.Number, header.Time)
return b.vm.syntacticBlockValidator.SyntacticVerify(b, rules)
}
// Verify implements the chain.Block interface
func (b *Block) Verify(context.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.ctx,
ProposerVMBlockCtx: nil,
}, true)
}
// ShouldVerifyWithContext implements the block.WithVerifyContext interface
func (b *Block) ShouldVerifyWithContext(context.Context) (bool, error) {
predicates := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp()).Predicaters
// Short circuit early if there are no predicates to verify
if len(predicates) == 0 {
return false, nil
}
// Check if any of the transactions in the block specify a precompile that enforces a predicate, which requires
// the ProposerVMBlockCtx.
for _, tx := range b.ethBlock.Transactions() {
for _, accessTuple := range tx.AccessList() {
if _, ok := predicates[accessTuple.Address]; ok {
log.Debug("Block verification requires proposerVM context", "block", b.ID(), "height", b.Height())
return true, nil
}
}
}
log.Debug("Block verification does not require proposerVM context", "block", b.ID(), "height", b.Height())
return false, nil
}
// VerifyWithContext implements the block.WithVerifyContext interface
func (b *Block) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.ctx,
ProposerVMBlockCtx: proposerVMBlockCtx,
}, true)
}
// Verify the block is valid.
// Enforces that the predicates are valid within [predicateContext].
// Writes the block details to disk and the state to the trie manager iff writes=true.
func (b *Block) verify(predicateContext *precompileconfig.PredicateContext, writes bool) error {
if predicateContext.ProposerVMBlockCtx != nil {
log.Debug("Verifying block with context", "block", b.ID(), "height", b.Height())
} else {
log.Debug("Verifying block without context", "block", b.ID(), "height", b.Height())
}
if err := b.syntacticVerify(); err != nil {
return fmt.Errorf("syntactic block verification failed: %w", err)
}
// Only enforce predicates if the chain has already bootstrapped.
// If the chain is still bootstrapping, we can assume that all blocks we are verifying have
// been accepted by the network (so the predicate was validated by the network when the
// block was originally verified).
if b.vm.bootstrapped.Get() {
if err := b.verifyPredicates(predicateContext); err != nil {
return fmt.Errorf("failed to verify predicates: %w", err)
}
}
// The engine may call VerifyWithContext multiple times on the same block with different contexts.
// Since the engine will only call Accept/Reject once, we should only call InsertBlockManual once.
// Additionally, if a block is already in processing, then it has already passed verification and
// at this point we have checked the predicates are still valid in the different context so we
// can return nil.
if b.vm.State.IsProcessing(b.id) {
return nil
}
return b.vm.blockChain.InsertBlockManual(b.ethBlock, writes)
}
// verifyPredicates verifies the predicates in the block are valid according to predicateContext.
func (b *Block) verifyPredicates(predicateContext *precompileconfig.PredicateContext) error {
rules := b.vm.chainConfig.LuxRules(b.ethBlock.Number(), b.ethBlock.Timestamp())
switch {
case !rulesExtra.IsDurango && rulesExtra.PredicatersExist():
return errors.New("cannot enable predicates before Durango activation")
case !rulesExtra.IsDurango:
return nil
}
predicateResults := predicate.NewResults()
for _, tx := range b.ethBlock.Transactions() {
results, err := core.CheckPredicates(rules, predicateContext, tx)
if err != nil {
return err
}
predicateResults.SetTxResults(tx.Hash(), results)
}
// TODO: document required gas constraints to ensure marshalling predicate results does not error
predicateResultsBytes, err := predicateResults.Bytes()
if err != nil {
return fmt.Errorf("failed to marshal predicate results: %w", err)
}
extraData := b.ethBlock.Extra()
headerPredicateResultsBytes := header.PredicateBytesFromExtra(extraData)
if !bytes.Equal(headerPredicateResultsBytes, predicateResultsBytes) {
return fmt.Errorf("%w (remote: %x local: %x)", errInvalidHeaderPredicateResults, headerPredicateResultsBytes, predicateResultsBytes)
}
return nil
}
// Bytes implements the chain.Block interface
func (b *Block) Bytes() []byte {
res, err := rlp.EncodeToBytes(b.ethBlock)
if err != nil {
panic(err)
}
return res
}
func (b *Block) String() string { return fmt.Sprintf("EVM block, ID = %s", b.ID()) }
-162
View File
@@ -1,162 +0,0 @@
// (c) 2019-2021, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"sync"
"time"
"github.com/luxfi/node/utils/timer"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/txpool"
"github.com/luxfi/evm/params"
"github.com/luxfi/node/consensus"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/ethereum/go-ethereum/log"
)
const (
// Minimum amount of time to wait after building a block before attempting to build a block
// a second time without changing the contents of the mempool.
minBlockBuildingRetryDelay = 500 * time.Millisecond
)
type blockBuilder struct {
ctx *consensus.Context
chainConfig *params.ChainConfig
txPool *txpool.TxPool
shutdownChan <-chan struct{}
shutdownWg *sync.WaitGroup
// A message is sent on this channel when a new block
// is ready to be build. This notifies the consensus common.
notifyBuildBlockChan chan<- commonEng.Message
// [buildBlockLock] must be held when accessing [buildSent]
buildBlockLock sync.Mutex
// buildSent is true iff we have sent a PendingTxs message to the consensus message and
// are still waiting for buildBlock to be called.
buildSent bool
// buildBlockTimer is a timer used to delay retrying block building a minimum amount of time
// with the same contents of the mempool.
// If the mempool receives a new transaction, the block builder will send a new notification to
// the engine and cancel the timer.
buildBlockTimer *timer.Timer
}
func (vm *VM) NewBlockBuilder(notifyBuildBlockChan chan<- commonEng.Message) *blockBuilder {
b := &blockBuilder{
ctx: vm.ctx,
chainConfig: vm.chainConfig,
txPool: vm.txPool,
shutdownChan: vm.shutdownChan,
shutdownWg: &vm.shutdownWg,
notifyBuildBlockChan: notifyBuildBlockChan,
}
b.handleBlockBuilding()
return b
}
// handleBlockBuilding dispatches a timer used to delay block building retry attempts when the contents
// of the mempool has not been changed since the last attempt.
func (b *blockBuilder) handleBlockBuilding() {
b.buildBlockTimer = timer.NewTimer(b.buildBlockTimerCallback)
go b.ctx.Log.RecoverAndPanic(b.buildBlockTimer.Dispatch)
}
// buildBlockTimerCallback is the timer callback that will send a PendingTxs notification
// to the consensus engine if there are transactions in the mempool.
func (b *blockBuilder) buildBlockTimerCallback() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// If there are still transactions in the mempool, send another notification to
// the engine to retry BuildBlock.
if b.needToBuild() {
b.markBuilding()
}
}
// handleGenerateBlock is called from the VM immediately after BuildBlock.
func (b *blockBuilder) handleGenerateBlock() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// Reset buildSent now that the engine has called BuildBlock.
b.buildSent = false
// Set a timer to check if calling build block a second time is needed.
b.buildBlockTimer.SetTimeoutIn(minBlockBuildingRetryDelay)
}
// needToBuild returns true if there are outstanding transactions to be issued
// into a block.
func (b *blockBuilder) needToBuild() bool {
size := b.txPool.PendingSize(txpool.PendingFilter{
MinTip: uint256.MustFromBig(b.txPool.GasTip()),
})
return size > 0
}
// markBuilding adds a PendingTxs message to the toEngine channel.
// markBuilding assumes the [buildBlockLock] is held.
func (b *blockBuilder) markBuilding() {
// If the engine has not called BuildBlock, no need to send another message.
if b.buildSent {
return
}
b.buildBlockTimer.Cancel() // Cancel any future attempt from the timer to send a PendingTxs message
select {
case b.notifyBuildBlockChan <- commonEng.PendingTxs:
b.buildSent = true
default:
log.Error("Failed to push PendingTxs notification to the consensus common.")
}
}
// signalTxsReady sends a PendingTxs notification to the consensus common.
// If BuildBlock has not been called since the last PendingTxs message was sent,
// signalTxsReady will not send a duplicate.
func (b *blockBuilder) signalTxsReady() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// We take a naive approach here and signal the engine that we should build
// a block as soon as we receive at least one new transaction.
//
// In the future, we may wish to add optimization here to only signal the
// engine if the sum of the projected tips in the mempool satisfies the
// required block fee.
b.markBuilding()
}
// awaitSubmittedTxs waits for new transactions to be submitted
// and notifies the VM when the tx pool has transactions to be
// put into a new block.
func (b *blockBuilder) awaitSubmittedTxs() {
// txSubmitChan is invoked when new transactions are issued as well as on re-orgs which
// may orphan transactions that were previously in a preferred block.
txSubmitChan := make(chan core.NewTxsEvent)
b.txPool.SubscribeTransactions(txSubmitChan, true)
b.shutdownWg.Add(1)
go b.ctx.Log.RecoverAndPanic(func() {
defer b.shutdownWg.Done()
for {
select {
case <-txSubmitChan:
log.Trace("New tx detected, trying to generate a block")
b.signalTxsReady()
case <-b.shutdownChan:
b.buildBlockTimer.Stop()
return
}
}
})
}
-162
View File
@@ -1,162 +0,0 @@
// (c) 2019-2021, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"sync"
"time"
"github.com/luxfi/node/utils/timer"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/txpool"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/interfaces"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/ethereum/go-ethereum/log"
)
const (
// Minimum amount of time to wait after building a block before attempting to build a block
// a second time without changing the contents of the mempool.
minBlockBuildingRetryDelay = 500 * time.Millisecond
)
type blockBuilder struct {
ctx *interfaces.ChainContext
chainConfig *params.ChainConfig
txPool *txpool.TxPool
shutdownChan <-chan struct{}
shutdownWg *sync.WaitGroup
// A message is sent on this channel when a new block
// is ready to be build. This notifies the consensus common.
notifyBuildBlockChan chan<- commonEng.Message
// [buildBlockLock] must be held when accessing [buildSent]
buildBlockLock sync.Mutex
// buildSent is true iff we have sent a PendingTxs message to the consensus message and
// are still waiting for buildBlock to be called.
buildSent bool
// buildBlockTimer is a timer used to delay retrying block building a minimum amount of time
// with the same contents of the mempool.
// If the mempool receives a new transaction, the block builder will send a new notification to
// the engine and cancel the timer.
buildBlockTimer *timer.Timer
}
func (vm *VM) NewBlockBuilder(notifyBuildBlockChan chan<- commonEng.Message) *blockBuilder {
b := &blockBuilder{
ctx: vm.ctx,
chainConfig: vm.chainConfig,
txPool: vm.txPool,
shutdownChan: vm.shutdownChan,
shutdownWg: &vm.shutdownWg,
notifyBuildBlockChan: notifyBuildBlockChan,
}
b.handleBlockBuilding()
return b
}
// handleBlockBuilding dispatches a timer used to delay block building retry attempts when the contents
// of the mempool has not been changed since the last attempt.
func (b *blockBuilder) handleBlockBuilding() {
b.buildBlockTimer = timer.NewTimer(b.buildBlockTimerCallback)
go b.ctx.Log.RecoverAndPanic(b.buildBlockTimer.Dispatch)
}
// buildBlockTimerCallback is the timer callback that will send a PendingTxs notification
// to the consensus engine if there are transactions in the mempool.
func (b *blockBuilder) buildBlockTimerCallback() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// If there are still transactions in the mempool, send another notification to
// the engine to retry BuildBlock.
if b.needToBuild() {
b.markBuilding()
}
}
// handleGenerateBlock is called from the VM immediately after BuildBlock.
func (b *blockBuilder) handleGenerateBlock() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// Reset buildSent now that the engine has called BuildBlock.
b.buildSent = false
// Set a timer to check if calling build block a second time is needed.
b.buildBlockTimer.SetTimeoutIn(minBlockBuildingRetryDelay)
}
// needToBuild returns true if there are outstanding transactions to be issued
// into a block.
func (b *blockBuilder) needToBuild() bool {
size := b.txPool.PendingSize(txpool.PendingFilter{
MinTip: uint256.MustFromBig(b.txPool.GasTip()),
})
return size > 0
}
// markBuilding adds a PendingTxs message to the toEngine channel.
// markBuilding assumes the [buildBlockLock] is held.
func (b *blockBuilder) markBuilding() {
// If the engine has not called BuildBlock, no need to send another message.
if b.buildSent {
return
}
b.buildBlockTimer.Cancel() // Cancel any future attempt from the timer to send a PendingTxs message
select {
case b.notifyBuildBlockChan <- commonEng.PendingTxs:
b.buildSent = true
default:
log.Error("Failed to push PendingTxs notification to the consensus common.")
}
}
// signalTxsReady sends a PendingTxs notification to the consensus common.
// If BuildBlock has not been called since the last PendingTxs message was sent,
// signalTxsReady will not send a duplicate.
func (b *blockBuilder) signalTxsReady() {
b.buildBlockLock.Lock()
defer b.buildBlockLock.Unlock()
// We take a naive approach here and signal the engine that we should build
// a block as soon as we receive at least one new transaction.
//
// In the future, we may wish to add optimization here to only signal the
// engine if the sum of the projected tips in the mempool satisfies the
// required block fee.
b.markBuilding()
}
// awaitSubmittedTxs waits for new transactions to be submitted
// and notifies the VM when the tx pool has transactions to be
// put into a new block.
func (b *blockBuilder) awaitSubmittedTxs() {
// txSubmitChan is invoked when new transactions are issued as well as on re-orgs which
// may orphan transactions that were previously in a preferred block.
txSubmitChan := make(chan core.NewTxsEvent)
b.txPool.SubscribeTransactions(txSubmitChan, true)
b.shutdownWg.Add(1)
go b.ctx.Log.RecoverAndPanic(func() {
defer b.shutdownWg.Done()
for {
select {
case <-txSubmitChan:
log.Trace("New tx detected, trying to generate a block")
b.signalTxsReady()
case <-b.shutdownChan:
b.buildBlockTimer.Stop()
return
}
}
})
}
-97
View File
@@ -1,97 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"math/big"
"testing"
"github.com/luxfi/node/database/memdb"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestHandlePrecompileAccept(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
db := rawdb.NewMemoryDatabase()
vm := &VM{
chaindb: db,
chainConfig: params.TestChainConfig,
}
precompileAddr := common.Address{0x05}
otherAddr := common.Address{0x06}
// Prepare a receipt with 3 logs, two of which are from the precompile
receipt := &types.Receipt{
Logs: []*types.Log{
{
Address: precompileAddr,
Topics: []common.Hash{{0x01}, {0x02}, {0x03}},
Data: []byte("log1"),
},
{
Address: otherAddr,
Topics: []common.Hash{{0x01}, {0x02}, {0x04}},
Data: []byte("log2"),
},
{
Address: precompileAddr,
Topics: []common.Hash{{0x01}, {0x02}, {0x05}},
Data: []byte("log3"),
},
},
}
ethBlock := types.NewBlock(
&types.Header{Number: big.NewInt(1)},
[]*types.Transaction{types.NewTx(&types.LegacyTx{})},
nil,
[]*types.Receipt{receipt},
trie.NewStackTrie(nil),
)
// Write the block to the db
rawdb.WriteBlock(db, ethBlock)
rawdb.WriteReceipts(db, ethBlock.Hash(), ethBlock.NumberU64(), []*types.Receipt{receipt})
// Set up the mock with the expected calls to Accept
txIndex := 0
mockAccepter := precompileconfig.NewMockAccepter(ctrl)
gomock.InOrder(
mockAccepter.EXPECT().Accept(
gomock.Not(gomock.Nil()), // acceptCtx
ethBlock.Hash(), // blockHash
ethBlock.NumberU64(), // blockNumber
ethBlock.Transactions()[txIndex].Hash(), // txHash
0, // logIndex
receipt.Logs[0].Topics, // topics
receipt.Logs[0].Data, // logData
),
mockAccepter.EXPECT().Accept(
gomock.Not(gomock.Nil()), // acceptCtx
ethBlock.Hash(), // blockHash
ethBlock.NumberU64(), // blockNumber
ethBlock.Transactions()[txIndex].Hash(), // txHash
2, // logIndex
receipt.Logs[2].Topics, // topics
receipt.Logs[2].Data, // logData
),
)
// Call handlePrecompileAccept
blk := vm.newBlock(ethBlock)
rules := extras.Rules{
AccepterPrecompiles: map[common.Address]precompileconfig.Accepter{
precompileAddr: mockAccepter,
},
}
require.NoError(blk.handlePrecompileAccept(rules))
}
-57
View File
@@ -1,57 +0,0 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// blockgascost implements the block gas cost logic
package blockgascost
import (
"math"
safemath "github.com/luxfi/node/utils/math"
"github.com/luxfi/evm/commontype"
)
// BlockGasCost calculates the required block gas cost.
//
// cost = parentCost + step * (TargetBlockRate - timeElapsed)
//
// The returned cost is clamped to [MinBlockGasCost, MaxBlockGasCost].
func BlockGasCost(
feeConfig commontype.FeeConfig,
parentCost uint64,
step uint64,
timeElapsed uint64,
) uint64 {
deviation := safemath.AbsDiff(feeConfig.TargetBlockRate, timeElapsed)
change, err := safemath.Mul(step, deviation)
if err != nil {
change = math.MaxUint64
}
var (
minBlockGasCost uint64 = feeConfig.MinBlockGasCost.Uint64()
maxBlockGasCost uint64 = feeConfig.MaxBlockGasCost.Uint64()
op = safemath.Add[uint64]
defaultCost uint64 = feeConfig.MaxBlockGasCost.Uint64()
)
if timeElapsed > feeConfig.TargetBlockRate {
op = safemath.Sub
defaultCost = minBlockGasCost
}
cost, err := op(parentCost, change)
if err != nil {
cost = defaultCost
}
switch {
case cost < minBlockGasCost:
// This is technically dead code because [MinBlockGasCost] is 0, but it
// makes the code more clear.
return minBlockGasCost
case cost > maxBlockGasCost:
return maxBlockGasCost
default:
return cost
}
}
-121
View File
@@ -1,121 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package client
import (
"context"
"fmt"
"log/slog"
"github.com/luxfi/node/api"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/utils/rpc"
"github.com/luxfi/evm/plugin/evm/config"
)
// Interface compliance
var _ Client = (*client)(nil)
type CurrentValidator struct {
ValidationID ids.ID `json:"validationID"`
NodeID ids.NodeID `json:"nodeID"`
Weight uint64 `json:"weight"`
StartTimestamp uint64 `json:"startTimestamp"`
IsActive bool `json:"isActive"`
IsL1Validator bool `json:"isL1Validator"`
IsConnected bool `json:"isConnected"`
UptimePercentage float32 `json:"uptimePercentage"`
UptimeSeconds uint64 `json:"uptimeSeconds"`
}
// Client interface for interacting with EVM [chain]
type Client interface {
StartCPUProfiler(ctx context.Context, options ...rpc.Option) error
StopCPUProfiler(ctx context.Context, options ...rpc.Option) error
MemoryProfile(ctx context.Context, options ...rpc.Option) error
LockProfile(ctx context.Context, options ...rpc.Option) error
SetLogLevel(ctx context.Context, level slog.Level, options ...rpc.Option) error
GetVMConfig(ctx context.Context, options ...rpc.Option) (*config.Config, error)
GetCurrentValidators(ctx context.Context, nodeIDs []ids.NodeID, options ...rpc.Option) ([]CurrentValidator, error)
}
// Client implementation for interacting with EVM [chain]
type client struct {
adminRequester rpc.EndpointRequester
validatorsRequester rpc.EndpointRequester
}
// NewClient returns a Client for interacting with EVM [chain]
func NewClient(uri, chain string) Client {
requestUri := fmt.Sprintf("%s/ext/bc/%s", uri, chain)
return NewClientWithURL(requestUri)
}
// NewClientWithURL returns a Client for interacting with EVM [chain]
func NewClientWithURL(url string) Client {
return &client{
adminRequester: rpc.NewEndpointRequester(
url + "/admin",
),
validatorsRequester: rpc.NewEndpointRequester(
url + "/validators",
),
}
}
func (c *client) StartCPUProfiler(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.startCPUProfiler", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) StopCPUProfiler(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.stopCPUProfiler", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) MemoryProfile(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.memoryProfile", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) LockProfile(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.lockProfile", struct{}{}, &api.EmptyReply{}, options...)
}
type SetLogLevelArgs struct {
Level string `json:"level"`
}
// SetLogLevel dynamically sets the log level for the C Chain
func (c *client) SetLogLevel(ctx context.Context, level slog.Level, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.setLogLevel", &SetLogLevelArgs{
Level: level.String(),
}, &api.EmptyReply{}, options...)
}
type ConfigReply struct {
Config *config.Config `json:"config"`
}
// GetVMConfig returns the current config of the VM
func (c *client) GetVMConfig(ctx context.Context, options ...rpc.Option) (*config.Config, error) {
res := &ConfigReply{}
err := c.adminRequester.SendRequest(ctx, "admin.getVMConfig", struct{}{}, res, options...)
return res.Config, err
}
type GetCurrentValidatorsRequest struct {
NodeIDs []ids.NodeID `json:"nodeIDs"`
}
type GetCurrentValidatorsResponse struct {
Validators []CurrentValidator `json:"validators"`
}
// GetCurrentValidators returns the current validators
func (c *client) GetCurrentValidators(ctx context.Context, nodeIDs []ids.NodeID, options ...rpc.Option) ([]CurrentValidator, error) {
res := &GetCurrentValidatorsResponse{}
err := c.validatorsRequester.SendRequest(ctx, "validators.getCurrentValidators", &GetCurrentValidatorsRequest{
NodeIDs: nodeIDs,
}, res, options...)
return res.Validators, err
}
-121
View File
@@ -1,121 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package client
import (
"context"
"fmt"
"log/slog"
"github.com/luxfi/node/api"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/rpc"
"github.com/luxfi/evm/plugin/evm/config"
)
// Interface compliance
var _ Client = (*client)(nil)
type CurrentValidator struct {
ValidationID interfaces.ID `json:"validationID"`
NodeID interfaces.NodeID `json:"nodeID"`
Weight uint64 `json:"weight"`
StartTimestamp uint64 `json:"startTimestamp"`
IsActive bool `json:"isActive"`
IsL1Validator bool `json:"isL1Validator"`
IsConnected bool `json:"isConnected"`
UptimePercentage float32 `json:"uptimePercentage"`
UptimeSeconds uint64 `json:"uptimeSeconds"`
}
// Client interface for interacting with EVM [chain]
type Client interface {
StartCPUProfiler(ctx context.Context, options ...rpc.Option) error
StopCPUProfiler(ctx context.Context, options ...rpc.Option) error
MemoryProfile(ctx context.Context, options ...rpc.Option) error
LockProfile(ctx context.Context, options ...rpc.Option) error
SetLogLevel(ctx context.Context, level slog.Level, options ...rpc.Option) error
GetVMConfig(ctx context.Context, options ...rpc.Option) (*config.Config, error)
GetCurrentValidators(ctx context.Context, nodeIDs []interfaces.NodeID, options ...rpc.Option) ([]CurrentValidator, error)
}
// Client implementation for interacting with EVM [chain]
type client struct {
adminRequester rpc.EndpointRequester
validatorsRequester rpc.EndpointRequester
}
// NewClient returns a Client for interacting with EVM [chain]
func NewClient(uri, chain string) Client {
requestUri := fmt.Sprintf("%s/ext/bc/%s", uri, chain)
return NewClientWithURL(requestUri)
}
// NewClientWithURL returns a Client for interacting with EVM [chain]
func NewClientWithURL(url string) Client {
return &client{
adminRequester: rpc.NewEndpointRequester(
url + "/admin",
),
validatorsRequester: rpc.NewEndpointRequester(
url + "/validators",
),
}
}
func (c *client) StartCPUProfiler(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.startCPUProfiler", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) StopCPUProfiler(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.stopCPUProfiler", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) MemoryProfile(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.memoryProfile", struct{}{}, &api.EmptyReply{}, options...)
}
func (c *client) LockProfile(ctx context.Context, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.lockProfile", struct{}{}, &api.EmptyReply{}, options...)
}
type SetLogLevelArgs struct {
Level string `json:"level"`
}
// SetLogLevel dynamically sets the log level for the C Chain
func (c *client) SetLogLevel(ctx context.Context, level slog.Level, options ...rpc.Option) error {
return c.adminRequester.SendRequest(ctx, "admin.setLogLevel", &SetLogLevelArgs{
Level: level.String(),
}, &api.EmptyReply{}, options...)
}
type ConfigReply struct {
Config *config.Config `json:"config"`
}
// GetVMConfig returns the current config of the VM
func (c *client) GetVMConfig(ctx context.Context, options ...rpc.Option) (*config.Config, error) {
res := &ConfigReply{}
err := c.adminRequester.SendRequest(ctx, "admin.getVMConfig", struct{}{}, res, options...)
return res.Config, err
}
type GetCurrentValidatorsRequest struct {
NodeIDs []interfaces.NodeID `json:"nodeIDs"`
}
type GetCurrentValidatorsResponse struct {
Validators []CurrentValidator `json:"validators"`
}
// GetCurrentValidators returns the current validators
func (c *client) GetCurrentValidators(ctx context.Context, nodeIDs []interfaces.NodeID, options ...rpc.Option) ([]CurrentValidator, error) {
res := &GetCurrentValidatorsResponse{}
err := c.validatorsRequester.SendRequest(ctx, "validators.getCurrentValidators", &GetCurrentValidatorsRequest{
NodeIDs: nodeIDs,
}, res, options...)
return res.Validators, err
}
-314
View File
@@ -1,314 +0,0 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/json"
"fmt"
"time"
"github.com/luxfi/evm/core/txpool"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/spf13/cast"
)
const (
defaultAcceptorQueueLimit = 64 // Provides 2 minutes of buffer (2s block target) for a commit delay
defaultPruningEnabled = true
defaultCommitInterval = 4096
defaultTrieCleanCache = 512
defaultTrieDirtyCache = 512
defaultTrieDirtyCommitTarget = 20
defaultSnapshotCache = 256
defaultSyncableCommitInterval = defaultCommitInterval * 4
defaultSnapshotWait = false
defaultRpcGasCap = 50_000_000 // Default to 50M Gas Limit
defaultRpcTxFeeCap = 100 // 100 LUX
defaultMetricsExpensiveEnabled = true
defaultApiMaxDuration = 0 // Default to no maximum API call duration
defaultWsCpuRefillRate = 0 // Default to no maximum WS CPU usage
defaultWsCpuMaxStored = 0 // Default to no maximum WS CPU usage
defaultMaxBlocksPerRequest = 0 // Default to no maximum on the number of blocks per getLogs request
defaultContinuousProfilerFrequency = 15 * time.Minute
defaultContinuousProfilerMaxFiles = 5
defaultRegossipFrequency = 1 * time.Minute
defaultRegossipMaxTxs = 16
defaultRegossipTxsPerAddress = 1
defaultPriorityRegossipFrequency = 1 * time.Second
defaultPriorityRegossipMaxTxs = 32
defaultPriorityRegossipTxsPerAddress = 16
defaultOfflinePruningBloomFilterSize uint64 = 512 // Default size (MB) for the offline pruner to use
defaultLogLevel = "info"
defaultLogJSONFormat = false
defaultMaxOutboundActiveRequests = 16
defaultMaxOutboundActiveCrossChainRequests = 64
defaultPopulateMissingTriesParallelism = 1024
defaultStateSyncServerTrieCache = 64 // MB
defaultAcceptedCacheSize = 32 // blocks
// defaultStateSyncMinBlocks is the minimum number of blocks the blockchain
// should be ahead of local last accepted to perform state sync.
// This constant is chosen so normal bootstrapping is preferred when it would
// be faster than state sync.
// time assumptions:
// - normal bootstrap processing time: ~14 blocks / second
// - state sync time: ~6 hrs.
defaultStateSyncMinBlocks = 300_000
defaultStateSyncRequestSize = 1024 // the number of key/values to ask peers for per request
)
var (
defaultEnabledAPIs = []string{
"eth",
"eth-filter",
"net",
"web3",
"internal-eth",
"internal-blockchain",
"internal-transaction",
}
defaultAllowUnprotectedTxHashes = []common.Hash{
common.HexToHash("0xfefb2da535e927b85fe68eb81cb2e4a5827c905f78381a01ef2322aa9b0aee8e"), // EIP-1820: https://eips.ethereum.org/EIPS/eip-1820
}
)
type Duration struct {
time.Duration
}
// Config ...
type Config struct {
// Airdrop
AirdropFile string `json:"airdrop"`
// Lux EVM APIs
ChainAPIEnabled bool `json:"chain-api-enabled"`
WarpAPIEnabled bool `json:"warp-api-enabled"`
AdminAPIEnabled bool `json:"admin-api-enabled"`
AdminAPIDir string `json:"admin-api-dir"`
// EnabledEthAPIs is a list of Ethereum services that should be enabled
// If none is specified, then we use the default list [defaultEnabledAPIs]
EnabledEthAPIs []string `json:"eth-apis"`
// Continuous Profiler
ContinuousProfilerDir string `json:"continuous-profiler-dir"` // If set to non-empty string creates a continuous profiler
ContinuousProfilerFrequency Duration `json:"continuous-profiler-frequency"` // Frequency to run continuous profiler if enabled
ContinuousProfilerMaxFiles int `json:"continuous-profiler-max-files"` // Maximum number of files to maintain
// Gas/Price Caps
RPCGasCap uint64 `json:"rpc-gas-cap"`
RPCTxFeeCap float64 `json:"rpc-tx-fee-cap"`
// Cache settings
TrieCleanCache int `json:"trie-clean-cache"` // Size of the trie clean cache (MB)
TrieCleanJournal string `json:"trie-clean-journal"` // Directory to use to save the trie clean cache (must be populated to enable journaling the trie clean cache)
TrieCleanRejournal Duration `json:"trie-clean-rejournal"` // Frequency to re-journal the trie clean cache to disk (minimum 1 minute, must be populated to enable journaling the trie clean cache)
TrieDirtyCache int `json:"trie-dirty-cache"` // Size of the trie dirty cache (MB)
TrieDirtyCommitTarget int `json:"trie-dirty-commit-target"` // Memory limit to target in the dirty cache before performing a commit (MB)
SnapshotCache int `json:"snapshot-cache"` // Size of the snapshot disk layer clean cache (MB)
// Eth Settings
Preimages bool `json:"preimages-enabled"`
SnapshotWait bool `json:"snapshot-wait"`
SnapshotVerify bool `json:"snapshot-verification-enabled"`
// Pruning Settings
Pruning bool `json:"pruning-enabled"` // If enabled, trie roots are only persisted every 4096 blocks
AcceptorQueueLimit int `json:"accepted-queue-limit"` // Maximum blocks to queue before blocking during acceptance
CommitInterval uint64 `json:"commit-interval"` // Specifies the commit interval at which to persist EVM and atomic tries.
AllowMissingTries bool `json:"allow-missing-tries"` // If enabled, warnings preventing an incomplete trie index are suppressed
PopulateMissingTries *uint64 `json:"populate-missing-tries,omitempty"` // Sets the starting point for re-populating missing tries. Disables re-generation if nil.
PopulateMissingTriesParallelism int `json:"populate-missing-tries-parallelism"` // Number of concurrent readers to use when re-populating missing tries on startup.
PruneWarpDB bool `json:"prune-warp-db-enabled"` // Determines if the warpDB should be cleared on startup
// Metric Settings
MetricsExpensiveEnabled bool `json:"metrics-expensive-enabled"` // Debug-level metrics that might impact runtime performance
// API Settings
LocalTxsEnabled bool `json:"local-txs-enabled"`
TxPoolJournal string `json:"tx-pool-journal"`
TxPoolRejournal Duration `json:"tx-pool-rejournal"`
TxPoolPriceLimit uint64 `json:"tx-pool-price-limit"`
TxPoolPriceBump uint64 `json:"tx-pool-price-bump"`
TxPoolAccountSlots uint64 `json:"tx-pool-account-slots"`
TxPoolGlobalSlots uint64 `json:"tx-pool-global-slots"`
TxPoolAccountQueue uint64 `json:"tx-pool-account-queue"`
TxPoolGlobalQueue uint64 `json:"tx-pool-global-queue"`
APIMaxDuration Duration `json:"api-max-duration"`
WSCPURefillRate Duration `json:"ws-cpu-refill-rate"`
WSCPUMaxStored Duration `json:"ws-cpu-max-stored"`
MaxBlocksPerRequest int64 `json:"api-max-blocks-per-request"`
AllowUnfinalizedQueries bool `json:"allow-unfinalized-queries"`
AllowUnprotectedTxs bool `json:"allow-unprotected-txs"`
AllowUnprotectedTxHashes []common.Hash `json:"allow-unprotected-tx-hashes"`
// Keystore Settings
KeystoreDirectory string `json:"keystore-directory"` // both absolute and relative supported
KeystoreExternalSigner string `json:"keystore-external-signer"`
KeystoreInsecureUnlockAllowed bool `json:"keystore-insecure-unlock-allowed"`
// Gossip Settings
RemoteGossipOnlyEnabled bool `json:"remote-gossip-only-enabled"`
RegossipFrequency Duration `json:"regossip-frequency"`
RegossipMaxTxs int `json:"regossip-max-txs"`
RegossipTxsPerAddress int `json:"regossip-txs-per-address"`
PriorityRegossipFrequency Duration `json:"priority-regossip-frequency"`
PriorityRegossipMaxTxs int `json:"priority-regossip-max-txs"`
PriorityRegossipTxsPerAddress int `json:"priority-regossip-txs-per-address"`
PriorityRegossipAddresses []common.Address `json:"priority-regossip-addresses"`
// Log
LogLevel string `json:"log-level"`
LogJSONFormat bool `json:"log-json-format"`
// Address for Tx Fees (must be empty if not supported by blockchain)
FeeRecipient string `json:"feeRecipient"`
// Offline Pruning Settings
OfflinePruning bool `json:"offline-pruning-enabled"`
OfflinePruningBloomFilterSize uint64 `json:"offline-pruning-bloom-filter-size"`
OfflinePruningDataDirectory string `json:"offline-pruning-data-directory"`
// VM2VM network
MaxOutboundActiveRequests int64 `json:"max-outbound-active-requests"`
MaxOutboundActiveCrossChainRequests int64 `json:"max-outbound-active-cross-chain-requests"`
// Sync settings
StateSyncEnabled bool `json:"state-sync-enabled"`
StateSyncSkipResume bool `json:"state-sync-skip-resume"` // Forces state sync to use the highest available summary block
StateSyncServerTrieCache int `json:"state-sync-server-trie-cache"`
StateSyncIDs string `json:"state-sync-ids"`
StateSyncCommitInterval uint64 `json:"state-sync-commit-interval"`
StateSyncMinBlocks uint64 `json:"state-sync-min-blocks"`
StateSyncRequestSize uint16 `json:"state-sync-request-size"`
// Database Settings
InspectDatabase bool `json:"inspect-database"` // Inspects the database on startup if enabled.
// SkipUpgradeCheck disables checking that upgrades must take place before the last
// accepted block. Skipping this check is useful when a node operator does not update
// their node before the network upgrade and their node accepts blocks that have
// identical state with the pre-upgrade ruleset.
SkipUpgradeCheck bool `json:"skip-upgrade-check"`
// AcceptedCacheSize is the depth to keep in the accepted headers cache and the
// accepted logs cache at the accepted tip.
//
// This is particularly useful for improving the performance of eth_getLogs
// on RPC nodes.
AcceptedCacheSize int `json:"accepted-cache-size"`
// TxLookupLimit is the maximum number of blocks from head whose tx indices
// are reserved:
// * 0: means no limit
// * N: means N block limit [HEAD-N+1, HEAD] and delete extra indexes
TxLookupLimit uint64 `json:"tx-lookup-limit"`
// WarpOffChainMessages encodes off-chain messages (unrelated to any on-chain event ie. block or AddressedCall)
// that the node should be willing to sign.
// Note: only supports AddressedCall payloads as defined here:
// https://github.com/luxfi/node/tree/7623ffd4be915a5185c9ed5e11fa9be15a6e1f00/vms/platformvm/warp/payload#addressedcall
WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"`
}
// EthAPIs returns an array of strings representing the Eth APIs that should be enabled
func (c Config) EthAPIs() []string {
return c.EnabledEthAPIs
}
func (c Config) EthBackendSettings() eth.Settings {
return eth.Settings{MaxBlocksPerRequest: c.MaxBlocksPerRequest}
}
func (c *Config) SetDefaults() {
c.EnabledEthAPIs = defaultEnabledAPIs
c.RPCGasCap = defaultRpcGasCap
c.RPCTxFeeCap = defaultRpcTxFeeCap
c.MetricsExpensiveEnabled = defaultMetricsExpensiveEnabled
c.TxPoolJournal = txpool.DefaultConfig.Journal
c.TxPoolRejournal = Duration{txpool.DefaultConfig.Rejournal}
c.TxPoolPriceLimit = txpool.DefaultConfig.PriceLimit
c.TxPoolPriceBump = txpool.DefaultConfig.PriceBump
c.TxPoolAccountSlots = txpool.DefaultConfig.AccountSlots
c.TxPoolGlobalSlots = txpool.DefaultConfig.GlobalSlots
c.TxPoolAccountQueue = txpool.DefaultConfig.AccountQueue
c.TxPoolGlobalQueue = txpool.DefaultConfig.GlobalQueue
c.APIMaxDuration.Duration = defaultApiMaxDuration
c.WSCPURefillRate.Duration = defaultWsCpuRefillRate
c.WSCPUMaxStored.Duration = defaultWsCpuMaxStored
c.MaxBlocksPerRequest = defaultMaxBlocksPerRequest
c.ContinuousProfilerFrequency.Duration = defaultContinuousProfilerFrequency
c.ContinuousProfilerMaxFiles = defaultContinuousProfilerMaxFiles
c.Pruning = defaultPruningEnabled
c.TrieCleanCache = defaultTrieCleanCache
c.TrieDirtyCache = defaultTrieDirtyCache
c.TrieDirtyCommitTarget = defaultTrieDirtyCommitTarget
c.SnapshotCache = defaultSnapshotCache
c.AcceptorQueueLimit = defaultAcceptorQueueLimit
c.CommitInterval = defaultCommitInterval
c.SnapshotWait = defaultSnapshotWait
c.RegossipFrequency.Duration = defaultRegossipFrequency
c.RegossipMaxTxs = defaultRegossipMaxTxs
c.RegossipTxsPerAddress = defaultRegossipTxsPerAddress
c.PriorityRegossipFrequency.Duration = defaultPriorityRegossipFrequency
c.PriorityRegossipMaxTxs = defaultPriorityRegossipMaxTxs
c.PriorityRegossipTxsPerAddress = defaultPriorityRegossipTxsPerAddress
c.OfflinePruningBloomFilterSize = defaultOfflinePruningBloomFilterSize
c.LogLevel = defaultLogLevel
c.LogJSONFormat = defaultLogJSONFormat
c.MaxOutboundActiveRequests = defaultMaxOutboundActiveRequests
c.MaxOutboundActiveCrossChainRequests = defaultMaxOutboundActiveCrossChainRequests
c.PopulateMissingTriesParallelism = defaultPopulateMissingTriesParallelism
c.StateSyncServerTrieCache = defaultStateSyncServerTrieCache
c.StateSyncCommitInterval = defaultSyncableCommitInterval
c.StateSyncMinBlocks = defaultStateSyncMinBlocks
c.StateSyncRequestSize = defaultStateSyncRequestSize
c.AllowUnprotectedTxHashes = defaultAllowUnprotectedTxHashes
c.AcceptedCacheSize = defaultAcceptedCacheSize
}
func (d *Duration) UnmarshalJSON(data []byte) (err error) {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
d.Duration, err = cast.ToDurationE(v)
return err
}
// String implements the stringer interface.
func (d Duration) String() string {
return d.Duration.String()
}
// String implements the stringer interface.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}
// Validate returns an error if this is an invalid config.
func (c *Config) Validate() error {
if c.PopulateMissingTries != nil && (c.OfflinePruning || c.Pruning) {
return fmt.Errorf("cannot enable populate missing tries while offline pruning (enabled: %t)/pruning (enabled: %t) are enabled", c.OfflinePruning, c.Pruning)
}
if c.PopulateMissingTries != nil && c.PopulateMissingTriesParallelism < 1 {
return fmt.Errorf("cannot enable populate missing tries without at least one reader (parallelism: %d)", c.PopulateMissingTriesParallelism)
}
if !c.Pruning && c.OfflinePruning {
return fmt.Errorf("cannot run offline pruning while pruning is disabled")
}
// If pruning is enabled, the commit interval must be non-zero so the node commits state tries every CommitInterval blocks.
if c.Pruning && c.CommitInterval == 0 {
return fmt.Errorf("cannot use commit interval of 0 with pruning enabled")
}
return nil
}
-385
View File
@@ -1,385 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package config
import (
"encoding/json"
"fmt"
"time"
"github.com/luxfi/node/database/pebbledb"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/spf13/cast"
)
const (
defaultAcceptorQueueLimit = 64 // Provides 2 minutes of buffer (2s block target) for a commit delay
defaultPruningEnabled = true
defaultCommitInterval = 4096
defaultTrieCleanCache = 512
defaultTrieDirtyCache = 512
defaultTrieDirtyCommitTarget = 20
defaultTriePrefetcherParallelism = 16
defaultSnapshotCache = 256
defaultSyncableCommitInterval = defaultCommitInterval * 4
defaultSnapshotWait = false
defaultRpcGasCap = 50_000_000 // Default to 50M Gas Limit
defaultRpcTxFeeCap = 100 // 100 LUX
defaultMetricsExpensiveEnabled = true
defaultApiMaxDuration = 0 // Default to no maximum API call duration
defaultWsCpuRefillRate = 0 // Default to no maximum WS CPU usage
defaultWsCpuMaxStored = 0 // Default to no maximum WS CPU usage
defaultMaxBlocksPerRequest = 0 // Default to no maximum on the number of blocks per getLogs request
defaultContinuousProfilerFrequency = 15 * time.Minute
defaultContinuousProfilerMaxFiles = 5
defaultPushGossipPercentStake = .9
defaultPushGossipNumValidators = 100
defaultPushGossipNumPeers = 0
defaultPushRegossipNumValidators = 10
defaultPushRegossipNumPeers = 0
defaultPushGossipFrequency = 100 * time.Millisecond
defaultPullGossipFrequency = 1 * time.Second
defaultRegossipFrequency = 30 * time.Second
defaultOfflinePruningBloomFilterSize uint64 = 512 // Default size (MB) for the offline pruner to use
defaultLogLevel = "info"
defaultLogJSONFormat = false
defaultMaxOutboundActiveRequests = 16
defaultPopulateMissingTriesParallelism = 1024
defaultStateSyncServerTrieCache = 64 // MB
defaultAcceptedCacheSize = 32 // blocks
// defaultStateSyncMinBlocks is the minimum number of blocks the blockchain
// should be ahead of local last accepted to perform state sync.
// This constant is chosen so normal bootstrapping is preferred when it would
// be faster than state sync.
// time assumptions:
// - normal bootstrap processing time: ~14 blocks / second
// - state sync time: ~6 hrs.
defaultStateSyncMinBlocks = 300_000
defaultStateSyncRequestSize = 1024 // the number of key/values to ask peers for per request
defaultDBType = pebbledb.Name
defaultValidatorAPIEnabled = true
estimatedBlockAcceptPeriod = 2 * time.Second
defaultHistoricalProofQueryWindow = uint64(24 * time.Hour / estimatedBlockAcceptPeriod)
)
type PBool bool
var (
defaultEnabledAPIs = []string{
"eth",
"eth-filter",
"net",
"web3",
"internal-eth",
"internal-blockchain",
"internal-transaction",
}
defaultAllowUnprotectedTxHashes = []common.Hash{
common.HexToHash("0xfefb2da535e927b85fe68eb81cb2e4a5827c905f78381a01ef2322aa9b0aee8e"), // EIP-1820: https://eips.ethereum.org/EIPS/eip-1820
}
)
type Duration struct {
time.Duration
}
// Config ...
type Config struct {
// Airdrop
AirdropFile string `json:"airdrop"`
// Lux EVM APIs
ChainAPIEnabled bool `json:"chain-api-enabled"`
ValidatorsAPIEnabled bool `json:"validators-api-enabled"`
AdminAPIEnabled bool `json:"admin-api-enabled"`
AdminAPIDir string `json:"admin-api-dir"`
WarpAPIEnabled bool `json:"warp-api-enabled"`
// EnabledEthAPIs is a list of Ethereum services that should be enabled
// If none is specified, then we use the default list [defaultEnabledAPIs]
EnabledEthAPIs []string `json:"eth-apis"`
// Continuous Profiler
ContinuousProfilerDir string `json:"continuous-profiler-dir"` // If set to non-empty string creates a continuous profiler
ContinuousProfilerFrequency Duration `json:"continuous-profiler-frequency"` // Frequency to run continuous profiler if enabled
ContinuousProfilerMaxFiles int `json:"continuous-profiler-max-files"` // Maximum number of files to maintain
// API Gas/Price Caps
RPCGasCap uint64 `json:"rpc-gas-cap"`
RPCTxFeeCap float64 `json:"rpc-tx-fee-cap"`
// Cache settings
TrieCleanCache int `json:"trie-clean-cache"` // Size of the trie clean cache (MB)
TrieDirtyCache int `json:"trie-dirty-cache"` // Size of the trie dirty cache (MB)
TrieDirtyCommitTarget int `json:"trie-dirty-commit-target"` // Memory limit to target in the dirty cache before performing a commit (MB)
TriePrefetcherParallelism int `json:"trie-prefetcher-parallelism"` // Max concurrent disk reads trie prefetcher should perform at once
SnapshotCache int `json:"snapshot-cache"` // Size of the snapshot disk layer clean cache (MB)
// Eth Settings
Preimages bool `json:"preimages-enabled"`
SnapshotWait bool `json:"snapshot-wait"`
SnapshotVerify bool `json:"snapshot-verification-enabled"`
// Pruning Settings
Pruning bool `json:"pruning-enabled"` // If enabled, trie roots are only persisted every 4096 blocks
AcceptorQueueLimit int `json:"accepted-queue-limit"` // Maximum blocks to queue before blocking during acceptance
CommitInterval uint64 `json:"commit-interval"` // Specifies the commit interval at which to persist EVM and atomic tries.
AllowMissingTries bool `json:"allow-missing-tries"` // If enabled, warnings preventing an incomplete trie index are suppressed
PopulateMissingTries *uint64 `json:"populate-missing-tries,omitempty"` // Sets the starting point for re-populating missing tries. Disables re-generation if nil.
PopulateMissingTriesParallelism int `json:"populate-missing-tries-parallelism"` // Number of concurrent readers to use when re-populating missing tries on startup.
PruneWarpDB bool `json:"prune-warp-db-enabled"` // Determines if the warpDB should be cleared on startup
// HistoricalProofQueryWindow is, when running in archive mode only, the number of blocks before the
// last accepted block to be accepted for proof state queries.
HistoricalProofQueryWindow uint64 `json:"historical-proof-query-window,omitempty"`
// Metric Settings
MetricsExpensiveEnabled bool `json:"metrics-expensive-enabled"` // Debug-level metrics that might impact runtime performance
// API Settings
LocalTxsEnabled bool `json:"local-txs-enabled"`
TxPoolPriceLimit uint64 `json:"tx-pool-price-limit"`
TxPoolPriceBump uint64 `json:"tx-pool-price-bump"`
TxPoolAccountSlots uint64 `json:"tx-pool-account-slots"`
TxPoolGlobalSlots uint64 `json:"tx-pool-global-slots"`
TxPoolAccountQueue uint64 `json:"tx-pool-account-queue"`
TxPoolGlobalQueue uint64 `json:"tx-pool-global-queue"`
TxPoolLifetime Duration `json:"tx-pool-lifetime"`
APIMaxDuration Duration `json:"api-max-duration"`
WSCPURefillRate Duration `json:"ws-cpu-refill-rate"`
WSCPUMaxStored Duration `json:"ws-cpu-max-stored"`
MaxBlocksPerRequest int64 `json:"api-max-blocks-per-request"`
AllowUnfinalizedQueries bool `json:"allow-unfinalized-queries"`
AllowUnprotectedTxs bool `json:"allow-unprotected-txs"`
AllowUnprotectedTxHashes []common.Hash `json:"allow-unprotected-tx-hashes"`
// Keystore Settings
KeystoreDirectory string `json:"keystore-directory"` // both absolute and relative supported
KeystoreExternalSigner string `json:"keystore-external-signer"`
KeystoreInsecureUnlockAllowed bool `json:"keystore-insecure-unlock-allowed"`
// Gossip Settings
PushGossipPercentStake float64 `json:"push-gossip-percent-stake"`
PushGossipNumValidators int `json:"push-gossip-num-validators"`
PushGossipNumPeers int `json:"push-gossip-num-peers"`
PushRegossipNumValidators int `json:"push-regossip-num-validators"`
PushRegossipNumPeers int `json:"push-regossip-num-peers"`
PushGossipFrequency Duration `json:"push-gossip-frequency"`
PullGossipFrequency Duration `json:"pull-gossip-frequency"`
RegossipFrequency Duration `json:"regossip-frequency"`
PriorityRegossipAddresses []common.Address `json:"priority-regossip-addresses"`
// Log
LogLevel string `json:"log-level"`
LogJSONFormat bool `json:"log-json-format"`
// Address for Tx Fees (must be empty if not supported by blockchain)
FeeRecipient string `json:"feeRecipient"`
// Offline Pruning Settings
OfflinePruning bool `json:"offline-pruning-enabled"`
OfflinePruningBloomFilterSize uint64 `json:"offline-pruning-bloom-filter-size"`
OfflinePruningDataDirectory string `json:"offline-pruning-data-directory"`
// VM2VM network
MaxOutboundActiveRequests int64 `json:"max-outbound-active-requests"`
// Sync settings
StateSyncEnabled bool `json:"state-sync-enabled"`
StateSyncSkipResume bool `json:"state-sync-skip-resume"` // Forces state sync to use the highest available summary block
StateSyncServerTrieCache int `json:"state-sync-server-trie-cache"`
StateSyncIDs string `json:"state-sync-ids"`
StateSyncCommitInterval uint64 `json:"state-sync-commit-interval"`
StateSyncMinBlocks uint64 `json:"state-sync-min-blocks"`
StateSyncRequestSize uint16 `json:"state-sync-request-size"`
// Database Settings
InspectDatabase bool `json:"inspect-database"` // Inspects the database on startup if enabled.
// SkipUpgradeCheck disables checking that upgrades must take place before the last
// accepted block. Skipping this check is useful when a node operator does not update
// their node before the network upgrade and their node accepts blocks that have
// identical state with the pre-upgrade ruleset.
SkipUpgradeCheck bool `json:"skip-upgrade-check"`
// AcceptedCacheSize is the depth to keep in the accepted headers cache and the
// accepted logs cache at the accepted tip.
//
// This is particularly useful for improving the performance of eth_getLogs
// on RPC nodes.
AcceptedCacheSize int `json:"accepted-cache-size"`
// TransactionHistory is the maximum number of blocks from head whose tx indices
// are reserved:
// * 0: means no limit
// * N: means N block limit [HEAD-N+1, HEAD] and delete extra indexes
TransactionHistory uint64 `json:"transaction-history"`
// Deprecated, use 'TransactionHistory' instead.
TxLookupLimit uint64 `json:"tx-lookup-limit"`
// SkipTxIndexing skips indexing transactions.
// This is useful for validators that don't need to index transactions.
// TxLookupLimit can be still used to control unindexing old transactions.
SkipTxIndexing bool `json:"skip-tx-indexing"`
// WarpOffChainMessages encodes off-chain messages (unrelated to any on-chain event ie. block or AddressedCall)
// that the node should be willing to sign.
// Note: only supports AddressedCall payloads as defined here:
// https://github.com/luxfi/node/tree/7623ffd4be915a5185c9ed5e11fa9be15a6e1f00/vms/platformvm/warp/payload#addressedcall
WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"`
// RPC settings
HttpBodyLimit uint64 `json:"http-body-limit"`
// Database settings
UseStandaloneDatabase *PBool `json:"use-standalone-database"`
DatabaseConfigContent string `json:"database-config"`
DatabaseConfigFile string `json:"database-config-file"`
DatabaseType string `json:"database-type"`
DatabasePath string `json:"database-path"`
DatabaseReadOnly bool `json:"database-read-only"`
}
// TxPoolConfig contains the transaction pool config to be passed
// to [Config.SetDefaults].
type TxPoolConfig struct {
PriceLimit uint64
PriceBump uint64
AccountSlots uint64
GlobalSlots uint64
AccountQueue uint64
GlobalQueue uint64
Lifetime time.Duration
}
// EthAPIs returns an array of strings representing the Eth APIs that should be enabled
func (c Config) EthAPIs() []string {
return c.EnabledEthAPIs
}
func (c *Config) SetDefaults(txPoolConfig TxPoolConfig) {
c.EnabledEthAPIs = defaultEnabledAPIs
c.RPCGasCap = defaultRpcGasCap
c.RPCTxFeeCap = defaultRpcTxFeeCap
c.MetricsExpensiveEnabled = defaultMetricsExpensiveEnabled
// TxPool settings
c.TxPoolPriceLimit = txPoolConfig.PriceLimit
c.TxPoolPriceBump = txPoolConfig.PriceBump
c.TxPoolAccountSlots = txPoolConfig.AccountSlots
c.TxPoolGlobalSlots = txPoolConfig.GlobalSlots
c.TxPoolAccountQueue = txPoolConfig.AccountQueue
c.TxPoolGlobalQueue = txPoolConfig.GlobalQueue
c.TxPoolLifetime.Duration = txPoolConfig.Lifetime
c.APIMaxDuration.Duration = defaultApiMaxDuration
c.WSCPURefillRate.Duration = defaultWsCpuRefillRate
c.WSCPUMaxStored.Duration = defaultWsCpuMaxStored
c.MaxBlocksPerRequest = defaultMaxBlocksPerRequest
c.ContinuousProfilerFrequency.Duration = defaultContinuousProfilerFrequency
c.ContinuousProfilerMaxFiles = defaultContinuousProfilerMaxFiles
c.Pruning = defaultPruningEnabled
c.TrieCleanCache = defaultTrieCleanCache
c.TrieDirtyCache = defaultTrieDirtyCache
c.TrieDirtyCommitTarget = defaultTrieDirtyCommitTarget
c.TriePrefetcherParallelism = defaultTriePrefetcherParallelism
c.SnapshotCache = defaultSnapshotCache
c.AcceptorQueueLimit = defaultAcceptorQueueLimit
c.CommitInterval = defaultCommitInterval
c.SnapshotWait = defaultSnapshotWait
c.PushGossipPercentStake = defaultPushGossipPercentStake
c.PushGossipNumValidators = defaultPushGossipNumValidators
c.PushGossipNumPeers = defaultPushGossipNumPeers
c.PushRegossipNumValidators = defaultPushRegossipNumValidators
c.PushRegossipNumPeers = defaultPushRegossipNumPeers
c.PushGossipFrequency.Duration = defaultPushGossipFrequency
c.PullGossipFrequency.Duration = defaultPullGossipFrequency
c.RegossipFrequency.Duration = defaultRegossipFrequency
c.OfflinePruningBloomFilterSize = defaultOfflinePruningBloomFilterSize
c.LogLevel = defaultLogLevel
c.LogJSONFormat = defaultLogJSONFormat
c.MaxOutboundActiveRequests = defaultMaxOutboundActiveRequests
c.PopulateMissingTriesParallelism = defaultPopulateMissingTriesParallelism
c.StateSyncServerTrieCache = defaultStateSyncServerTrieCache
c.StateSyncCommitInterval = defaultSyncableCommitInterval
c.StateSyncMinBlocks = defaultStateSyncMinBlocks
c.StateSyncRequestSize = defaultStateSyncRequestSize
c.AllowUnprotectedTxHashes = defaultAllowUnprotectedTxHashes
c.AcceptedCacheSize = defaultAcceptedCacheSize
c.DatabaseType = defaultDBType
c.ValidatorsAPIEnabled = defaultValidatorAPIEnabled
c.HistoricalProofQueryWindow = defaultHistoricalProofQueryWindow
}
func (d *Duration) UnmarshalJSON(data []byte) (err error) {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
d.Duration, err = cast.ToDurationE(v)
return err
}
// String implements the stringer interface.
func (d Duration) String() string {
return d.Duration.String()
}
// String implements the stringer interface.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}
// Validate returns an error if this is an invalid config.
func (c *Config) Validate() error {
if c.PopulateMissingTries != nil && (c.OfflinePruning || c.Pruning) {
return fmt.Errorf("cannot enable populate missing tries while offline pruning (enabled: %t)/pruning (enabled: %t) are enabled", c.OfflinePruning, c.Pruning)
}
if c.PopulateMissingTries != nil && c.PopulateMissingTriesParallelism < 1 {
return fmt.Errorf("cannot enable populate missing tries without at least one reader (parallelism: %d)", c.PopulateMissingTriesParallelism)
}
if !c.Pruning && c.OfflinePruning {
return fmt.Errorf("cannot run offline pruning while pruning is disabled")
}
// If pruning is enabled, the commit interval must be non-zero so the node commits state tries every CommitInterval blocks.
if c.Pruning && c.CommitInterval == 0 {
return fmt.Errorf("cannot use commit interval of 0 with pruning enabled")
}
if c.PushGossipPercentStake < 0 || c.PushGossipPercentStake > 1 {
return fmt.Errorf("push-gossip-percent-stake is %f but must be in the range [0, 1]", c.PushGossipPercentStake)
}
return nil
}
func (c *Config) Deprecate() string {
msg := ""
// Deprecate the old config options and set the new ones.
if c.TxLookupLimit != 0 {
msg += "tx-lookup-limit is deprecated, use transaction-history instead. "
c.TransactionHistory = c.TxLookupLimit
}
return msg
}
func (p *PBool) String() string {
if p == nil {
return "nil"
}
return fmt.Sprintf("%t", *p)
}
func (p *PBool) Bool() bool {
if p == nil {
return false
}
return bool(*p)
}
@@ -1,222 +0,0 @@
// (c) 2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package customrawdb
import (
"encoding/binary"
"github.com/luxfi/node/utils/wrappers"
"github.com/ethereum/go-ethereum/common"
ethrawdb "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/luxfi/evm/geth/ethdb"
"github.com/ethereum/go-ethereum/log"
)
// ReadSyncRoot reads the root corresponding to the main trie of an in-progress
// sync and returns common.Hash{} if no in-progress sync was found.
func ReadSyncRoot(db ethdb.KeyValueReader) (common.Hash, error) {
has, err := db.Has(syncRootKey)
if err != nil || !has {
return common.Hash{}, err
}
root, err := db.Get(syncRootKey)
if err != nil {
return common.Hash{}, err
}
return common.BytesToHash(root), nil
}
// WriteSyncRoot writes root as the root of the main trie of the in-progress sync.
func WriteSyncRoot(db ethdb.KeyValueWriter, root common.Hash) error {
return db.Put(syncRootKey, root[:])
}
// AddCodeToFetch adds a marker that we need to fetch the code for `hash`.
func AddCodeToFetch(db ethdb.KeyValueWriter, hash common.Hash) {
if err := db.Put(codeToFetchKey(hash), nil); err != nil {
log.Crit("Failed to put code to fetch", "codeHash", hash, "err", err)
}
}
// DeleteCodeToFetch removes the marker that the code corresponding to `hash` needs to be fetched.
func DeleteCodeToFetch(db ethdb.KeyValueWriter, hash common.Hash) {
if err := db.Delete(codeToFetchKey(hash)); err != nil {
log.Crit("Failed to delete code to fetch", "codeHash", hash, "err", err)
}
}
// NewCodeToFetchIterator returns a KeyLength iterator over all code
// hashes that are pending syncing. It is the caller's responsibility to
// unpack the key and call Release on the returned iterator.
func NewCodeToFetchIterator(db ethdb.Iteratee) ethdb.Iterator {
return ethrawdb.NewKeyLengthIterator(
db.NewIterator(CodeToFetchPrefix, nil),
codeToFetchKeyLength,
)
}
func codeToFetchKey(hash common.Hash) []byte {
codeToFetchKey := make([]byte, codeToFetchKeyLength)
copy(codeToFetchKey, CodeToFetchPrefix)
copy(codeToFetchKey[len(CodeToFetchPrefix):], hash[:])
return codeToFetchKey
}
// NewSyncSegmentsIterator returns a KeyLength iterator over all trie segments
// added for root. It is the caller's responsibility to unpack the key and call
// Release on the returned iterator.
func NewSyncSegmentsIterator(db ethdb.Iteratee, root common.Hash) ethdb.Iterator {
segmentsPrefix := make([]byte, len(syncSegmentsPrefix)+common.HashLength)
copy(segmentsPrefix, syncSegmentsPrefix)
copy(segmentsPrefix[len(syncSegmentsPrefix):], root[:])
return ethrawdb.NewKeyLengthIterator(
db.NewIterator(segmentsPrefix, nil),
syncSegmentsKeyLength,
)
}
// WriteSyncSegment adds a trie segment for root at the given start position.
func WriteSyncSegment(db ethdb.KeyValueWriter, root common.Hash, start common.Hash) error {
return db.Put(packSyncSegmentKey(root, start), []byte{0x01})
}
// ClearSyncSegments removes segment markers for root from db
func ClearSyncSegments(db ethdb.KeyValueStore, root common.Hash) error {
segmentsPrefix := make([]byte, len(syncSegmentsPrefix)+common.HashLength)
copy(segmentsPrefix, syncSegmentsPrefix)
copy(segmentsPrefix[len(syncSegmentsPrefix):], root[:])
return clearPrefix(db, segmentsPrefix, syncSegmentsKeyLength)
}
// ClearAllSyncSegments removes all segment markers from db
func ClearAllSyncSegments(db ethdb.KeyValueStore) error {
return clearPrefix(db, syncSegmentsPrefix, syncSegmentsKeyLength)
}
// UnpackSyncSegmentKey returns the root and start position for a trie segment
// key returned from NewSyncSegmentsIterator.
func UnpackSyncSegmentKey(keyBytes []byte) (common.Hash, []byte) {
keyBytes = keyBytes[len(syncSegmentsPrefix):] // skip prefix
root := common.BytesToHash(keyBytes[:common.HashLength])
start := keyBytes[common.HashLength:]
return root, start
}
// packSyncSegmentKey packs root and account into a key for storage in db.
func packSyncSegmentKey(root common.Hash, start common.Hash) []byte {
bytes := make([]byte, syncSegmentsKeyLength)
copy(bytes, syncSegmentsPrefix)
copy(bytes[len(syncSegmentsPrefix):], root[:])
copy(bytes[len(syncSegmentsPrefix)+common.HashLength:], start.Bytes())
return bytes
}
// NewSyncStorageTriesIterator returns a KeyLength iterator over all storage tries
// added for syncing (beginning at seek). It is the caller's responsibility to unpack
// the key and call Release on the returned iterator.
func NewSyncStorageTriesIterator(db ethdb.Iteratee, seek []byte) ethdb.Iterator {
return ethrawdb.NewKeyLengthIterator(db.NewIterator(syncStorageTriesPrefix, seek), syncStorageTriesKeyLength)
}
// WriteSyncStorageTrie adds a storage trie for account (with the given root) to be synced.
func WriteSyncStorageTrie(db ethdb.KeyValueWriter, root common.Hash, account common.Hash) error {
return db.Put(packSyncStorageTrieKey(root, account), []byte{0x01})
}
// ClearSyncStorageTrie removes all storage trie accounts (with the given root) from db.
// Intended for use when the trie with root has completed syncing.
func ClearSyncStorageTrie(db ethdb.KeyValueStore, root common.Hash) error {
accountsPrefix := make([]byte, len(syncStorageTriesPrefix)+common.HashLength)
copy(accountsPrefix, syncStorageTriesPrefix)
copy(accountsPrefix[len(syncStorageTriesPrefix):], root[:])
return clearPrefix(db, accountsPrefix, syncStorageTriesKeyLength)
}
// ClearAllSyncStorageTries removes all storage tries added for syncing from db
func ClearAllSyncStorageTries(db ethdb.KeyValueStore) error {
return clearPrefix(db, syncStorageTriesPrefix, syncStorageTriesKeyLength)
}
// UnpackSyncStorageTrieKey returns the root and account for a storage trie
// key returned from NewSyncStorageTriesIterator.
func UnpackSyncStorageTrieKey(keyBytes []byte) (common.Hash, common.Hash) {
keyBytes = keyBytes[len(syncStorageTriesPrefix):] // skip prefix
root := common.BytesToHash(keyBytes[:common.HashLength])
account := common.BytesToHash(keyBytes[common.HashLength:])
return root, account
}
// packSyncStorageTrieKey packs root and account into a key for storage in db.
func packSyncStorageTrieKey(root common.Hash, account common.Hash) []byte {
bytes := make([]byte, 0, syncStorageTriesKeyLength)
bytes = append(bytes, syncStorageTriesPrefix...)
bytes = append(bytes, root[:]...)
bytes = append(bytes, account[:]...)
return bytes
}
// WriteSyncPerformed logs an entry in `db` indicating the VM state synced to `blockNumber`.
func WriteSyncPerformed(db ethdb.KeyValueWriter, blockNumber uint64) error {
syncPerformedPrefixLen := len(syncPerformedPrefix)
bytes := make([]byte, syncPerformedPrefixLen+wrappers.LongLen)
copy(bytes[:syncPerformedPrefixLen], syncPerformedPrefix)
binary.BigEndian.PutUint64(bytes[syncPerformedPrefixLen:], blockNumber)
return db.Put(bytes, []byte{0x01})
}
// NewSyncPerformedIterator returns an iterator over all block numbers the VM
// has state synced to.
func NewSyncPerformedIterator(db ethdb.Iteratee) ethdb.Iterator {
return ethrawdb.NewKeyLengthIterator(db.NewIterator(syncPerformedPrefix, nil), syncPerformedKeyLength)
}
// UnpackSyncPerformedKey returns the block number from keys the iterator returned
// from NewSyncPerformedIterator.
func UnpackSyncPerformedKey(key []byte) uint64 {
return binary.BigEndian.Uint64(key[len(syncPerformedPrefix):])
}
// GetLatestSyncPerformed returns the latest block number state synced performed to.
func GetLatestSyncPerformed(db ethdb.Iteratee) uint64 {
it := NewSyncPerformedIterator(db)
defer it.Release()
var latestSyncPerformed uint64
for it.Next() {
syncPerformed := UnpackSyncPerformedKey(it.Key())
if syncPerformed > latestSyncPerformed {
latestSyncPerformed = syncPerformed
}
}
return latestSyncPerformed
}
// clearPrefix removes all keys in db that begin with prefix and match an
// expected key length. `keyLen` must include the length of the prefix.
func clearPrefix(db ethdb.KeyValueStore, prefix []byte, keyLen int) error {
it := db.NewIterator(prefix, nil)
defer it.Release()
batch := db.NewBatch()
for it.Next() {
key := common.CopyBytes(it.Key())
if len(key) != keyLen {
continue
}
if err := batch.Delete(key); err != nil {
return err
}
if batch.ValueSize() > ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
return err
}
batch.Reset()
}
}
if err := it.Error(); err != nil {
return err
}
return batch.Write()
}
-60
View File
@@ -1,60 +0,0 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package customrawdb
import (
"github.com/luxfi/node/utils/wrappers"
"github.com/ethereum/go-ethereum/common"
)
var (
// snapshotBlockHashKey tracks the block hash of the last snapshot.
snapshotBlockHashKey = []byte("SnapshotBlockHash")
// offlinePruningKey tracks runs of offline pruning
offlinePruningKey = []byte("OfflinePruning")
// populateMissingTriesKey tracks runs of trie backfills
populateMissingTriesKey = []byte("PopulateMissingTries")
// pruningDisabledKey tracks whether the node has ever run in archival mode
// to ensure that a user does not accidentally corrupt an archival node.
pruningDisabledKey = []byte("PruningDisabled")
// acceptorTipKey tracks the tip of the last accepted block that has been fully processed.
acceptorTipKey = []byte("AcceptorTipKey")
// upgradeConfigPrefix prefixes upgrade bytes passed to the chain
upgradeConfigPrefix = []byte("upgrade-config-")
)
// State sync progress keys and prefixes
var (
// syncRootKey indicates the root of the main account trie currently being synced
syncRootKey = []byte("sync_root")
// syncStorageTriesPrefix is the prefix for storage tries that need to be fetched.
// syncStorageTriesPrefix + trie root + account hash: indicates a storage trie must be fetched for the account
syncStorageTriesPrefix = []byte("sync_storage")
// syncSegmentsPrefix is the prefix for segments.
// syncSegmentsPrefix + trie root + 32-byte start key: indicates the trie at root has a segment starting at the specified key
syncSegmentsPrefix = []byte("sync_segments")
// CodeToFetchPrefix is the prefix for code hashes that need to be fetched.
// CodeToFetchPrefix + code hash -> empty value tracks the outstanding code hashes we need to fetch.
CodeToFetchPrefix = []byte("CP")
)
// State sync progress key lengths
var (
syncStorageTriesKeyLength = len(syncStorageTriesPrefix) + 2*common.HashLength
syncSegmentsKeyLength = len(syncSegmentsPrefix) + 2*common.HashLength
codeToFetchKeyLength = len(CodeToFetchPrefix) + common.HashLength
)
// State sync metadata
var (
syncPerformedPrefix = []byte("sync_performed")
// syncPerformedKeyLength is the length of the key for the sync performed metadata key,
// and is equal to [syncPerformedPrefix] + block number as uint64.
syncPerformedKeyLength = len(syncPerformedPrefix) + wrappers.LongLen
)
// upgradeConfigKey = upgradeConfigPrefix + hash
func upgradeConfigKey(hash common.Hash) []byte {
return append(upgradeConfigPrefix, hash.Bytes()...)
}
@@ -1,87 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package database
import (
"errors"
"github.com/luxfi/node/database"
"github.com/luxfi/evm/geth/ethdb"
)
var (
_ ethdb.KeyValueStore = &ethDbWrapper{}
ErrSnapshotNotSupported = errors.New("snapshot is not supported")
)
// ethDbWrapper implements ethdb.Database
type ethDbWrapper struct{ database.Database }
func WrapDatabase(db database.Database) ethdb.KeyValueStore { return ethDbWrapper{db} }
// Stat implements ethdb.Database
func (db ethDbWrapper) Stat() (string, error) { return "", database.ErrNotFound }
// DeleteRange implements ethdb.KeyValueStore
func (db ethDbWrapper) DeleteRange(start []byte, end []byte) error {
// Not supported in lux database
return nil
}
// SyncKeyValue implements ethdb.KeyValueStore
func (db ethDbWrapper) SyncKeyValue() error {
// Not supported in lux database
return nil
}
// NewBatch implements ethdb.Database
func (db ethDbWrapper) NewBatch() ethdb.Batch { return wrappedBatch{db.Database.NewBatch()} }
// NewBatchWithSize implements ethdb.Database
// TODO: propagate size through luxd Database interface
func (db ethDbWrapper) NewBatchWithSize(size int) ethdb.Batch {
return wrappedBatch{db.Database.NewBatch()}
}
// NewSnapshot is not implemented
// func (db ethDbWrapper) NewSnapshot() (ethdb.Snapshot, error) {
// return nil, ErrSnapshotNotSupported
// }
// NewIterator implements ethdb.Database
//
// Note: This method assumes that the prefix is NOT part of the start, so there's
// no need for the caller to prepend the prefix to the start.
func (db ethDbWrapper) NewIterator(prefix []byte, start []byte) ethdb.Iterator {
// luxd's database implementation assumes that the prefix is part of the
// start, so it is added here (if it is provided).
if len(prefix) > 0 {
newStart := make([]byte, len(prefix)+len(start))
copy(newStart, prefix)
copy(newStart[len(prefix):], start)
start = newStart
}
return db.Database.NewIteratorWithStartAndPrefix(start, prefix)
}
// NewIteratorWithStart implements ethdb.Database
func (db ethDbWrapper) NewIteratorWithStart(start []byte) ethdb.Iterator {
return db.Database.NewIteratorWithStart(start)
}
// wrappedBatch implements ethdb.wrappedBatch
type wrappedBatch struct{ database.Batch }
// ValueSize implements ethdb.Batch
func (batch wrappedBatch) ValueSize() int { return batch.Batch.Size() }
// Replay implements ethdb.Batch
func (batch wrappedBatch) Replay(w ethdb.KeyValueWriter) error { return batch.Batch.Replay(w) }
// DeleteRange implements ethdb.Batch
func (batch wrappedBatch) DeleteRange(start []byte, end []byte) error {
// Not supported in lux database
return nil
}
-24
View File
@@ -1,24 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"github.com/luxfi/node/ids"
"github.com/luxfi/node/utils/logging"
"github.com/luxfi/node/vms"
)
var (
// ID this VM should be referenced by
IDStr = "subnetevm"
ID = ids.ID{'s', 'u', 'b', 'n', 'e', 't', 'e', 'v', 'm'}
_ vms.Factory = &Factory{}
)
type Factory struct{}
func (*Factory) New(logging.Logger) (interface{}, error) {
return &VM{}, nil
}
-24
View File
@@ -1,24 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/logging"
"github.com/luxfi/node/vms"
)
var (
// ID this VM should be referenced by
IDStr = "subnetevm"
ID = interfaces.ID{'s', 'u', 'b', 'n', 'e', 't', 'e', 'v', 'm'}
_ vms.Factory = &Factory{}
)
type Factory struct{}
func (*Factory) New(logging.Logger) (interface{}, error) {
return &VM{}, nil
}
-118
View File
@@ -1,118 +0,0 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package header
import (
"errors"
"fmt"
"github.com/luxfi/node/utils/math"
"github.com/ethereum/go-ethereum/core/types"
ethparams "github.com/luxfi/evm/params"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/params/extras"
)
var (
errInvalidGasUsed = errors.New("invalid gas used")
errInvalidGasLimit = errors.New("invalid gas limit")
)
type CalculateGasLimitFunc func(parentGasUsed, parentGasLimit, gasFloor, gasCeil uint64) uint64
// GasLimit takes the previous header and the timestamp of its child block and
// calculates the gas limit for the child block.
func GasLimit(
config *extras.ChainConfig,
feeConfig commontype.FeeConfig,
parent *types.Header,
timestamp uint64,
) (uint64, error) {
switch {
case config.IsEVM(timestamp):
return feeConfig.GasLimit.Uint64(), nil
default:
// since all chains have activated EVM,
// this code is not used in production. To avoid a dependency on the
// `core` package, this code is modified to just return the parent gas
// limit; which was valid to do prior to EVM.
return parent.GasLimit, nil
}
}
// VerifyGasUsed verifies that the gas used is less than or equal to the gas
// limit.
func VerifyGasUsed(
config *extras.ChainConfig,
feeConfig commontype.FeeConfig,
parent *types.Header,
header *types.Header,
) error {
gasUsed := header.GasUsed
capacity, err := GasCapacity(config, feeConfig, parent, header.Time)
if err != nil {
return fmt.Errorf("calculating gas capacity: %w", err)
}
if gasUsed > capacity {
return fmt.Errorf("%w: have %d, capacity %d",
errInvalidGasUsed,
gasUsed,
capacity,
)
}
return nil
}
// VerifyGasLimit verifies that the gas limit for the header is valid.
func VerifyGasLimit(
config *extras.ChainConfig,
feeConfig commontype.FeeConfig,
parent *types.Header,
header *types.Header,
) error {
switch {
case config.IsEVM(header.Time):
expectedGasLimit := feeConfig.GasLimit.Uint64()
if header.GasLimit != expectedGasLimit {
return fmt.Errorf("%w: expected to be %d in EVM, but found %d",
errInvalidGasLimit,
expectedGasLimit,
header.GasLimit,
)
}
default:
if header.GasLimit < ethparams.MinGasLimit || header.GasLimit > ethparams.MaxGasLimit {
return fmt.Errorf("%w: %d not in range [%d, %d]",
errInvalidGasLimit,
header.GasLimit,
ethparams.MinGasLimit,
ethparams.MaxGasLimit,
)
}
// Verify that the gas limit remains within allowed bounds
diff := math.AbsDiff(parent.GasLimit, header.GasLimit)
limit := parent.GasLimit / ethparams.GasLimitBoundDivisor
if diff >= limit {
return fmt.Errorf("%w: have %d, want %d += %d",
errInvalidGasLimit,
header.GasLimit,
parent.GasLimit,
limit,
)
}
}
return nil
}
// GasCapacity takes the previous header and the timestamp of its child block
// and calculates the available gas that can be consumed in the child block.
func GasCapacity(
config *extras.ChainConfig,
feeConfig commontype.FeeConfig,
parent *types.Header,
timestamp uint64,
) (uint64, error) {
return GasLimit(config, feeConfig, parent, timestamp)
}
-43
View File
@@ -1,43 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/node/ids"
"github.com/ethereum/go-ethereum/common"
)
var (
_ Request = BlockRequest{}
)
// BlockRequest is a request to retrieve Parents number of blocks starting from Hash from newest-oldest manner
type BlockRequest struct {
Hash common.Hash `serialize:"true"`
Height uint64 `serialize:"true"`
Parents uint16 `serialize:"true"`
}
func (b BlockRequest) String() string {
return fmt.Sprintf(
"BlockRequest(Hash=%s, Height=%d, Parents=%d)",
b.Hash, b.Height, b.Parents,
)
}
func (b BlockRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleBlockRequest(ctx, nodeID, requestID, b)
}
// BlockResponse is a response to a BlockRequest
// Blocks is slice of RLP encoded blocks starting with the block
// requested in BlockRequest.Hash. The next block is the parent, etc.
// handler: handlers.BlockRequestHandler
type BlockResponse struct {
Blocks [][]byte `serialize:"true"`
}
-46
View File
@@ -1,46 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"strings"
"github.com/luxfi/node/ids"
"github.com/ethereum/go-ethereum/common"
)
var _ Request = CodeRequest{}
// CodeRequest is a request to retrieve a contract code with specified Hash
type CodeRequest struct {
// Hashes is a list of contract code hashes
Hashes []common.Hash `serialize:"true"`
}
func (c CodeRequest) String() string {
hashStrs := make([]string, len(c.Hashes))
for i, hash := range c.Hashes {
hashStrs[i] = hash.String()
}
return fmt.Sprintf("CodeRequest(Hashes=%s)", strings.Join(hashStrs, ", "))
}
func (c CodeRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleCodeRequest(ctx, nodeID, requestID, c)
}
func NewCodeRequest(hashes []common.Hash) CodeRequest {
return CodeRequest{
Hashes: hashes,
}
}
// CodeResponse is a response to a CodeRequest
// crypto.Keccak256Hash of each element in Data is expected to equal
// the corresponding element in CodeRequest.Hashes
// handler: handlers.CodeRequestHandler
type CodeResponse struct {
Data [][]byte `serialize:"true"`
}
-53
View File
@@ -1,53 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/wrappers"
)
const (
Version = uint16(0)
maxMessageSize = 2*units.MiB - 64*units.KiB // Subtract 64 KiB from p2p network cap to leave room for encoding overhead from Lux
)
var (
Codec interfaces.Codec
)
func init() {
Codec = codec.NewManager(maxMessageSize)
c := linearcodec.NewDefault()
// Skip registration to keep registeredTypes unchanged after legacy gossip deprecation
c.SkipRegistrations(1)
errs := wrappers.Errs{}
errs.Add(
// Types for state sync frontier consensus
c.RegisterType(SyncSummary{}),
// state sync types
c.RegisterType(BlockRequest{}),
c.RegisterType(BlockResponse{}),
c.RegisterType(LeafsRequest{}),
c.RegisterType(LeafsResponse{}),
c.RegisterType(CodeRequest{}),
c.RegisterType(CodeResponse{}),
// Warp request types
c.RegisterType(MessageSignatureRequest{}),
c.RegisterType(BlockSignatureRequest{}),
c.RegisterType(SignatureResponse{}),
Codec.RegisterCodec(Version, c),
)
if errs.Errored() {
panic(errs.Err)
}
}
-53
View File
@@ -1,53 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
)
const (
Version = uint16(0)
maxMessageSize = 2*units.MiB - 64*units.KiB // Subtract 64 KiB from p2p network cap to leave room for encoding overhead from Lux
)
var (
Codec interfaces.Codec
)
func init() {
Codec = codec.NewManager(maxMessageSize)
c := linearcodec.NewDefault()
// Skip registration to keep registeredTypes unchanged after legacy gossip deprecation
c.SkipRegistrations(1)
errs := wrappers.Errs{}
errs.Add(
// Types for state sync frontier consensus
c.RegisterType(SyncSummary{}),
// state sync types
c.RegisterType(BlockRequest{}),
c.RegisterType(BlockResponse{}),
c.RegisterType(LeafsRequest{}),
c.RegisterType(LeafsResponse{}),
c.RegisterType(CodeRequest{}),
c.RegisterType(CodeResponse{}),
// Warp request types
c.RegisterType(MessageSignatureRequest{}),
c.RegisterType(BlockSignatureRequest{}),
c.RegisterType(SignatureResponse{}),
Codec.RegisterCodec(Version, c),
)
if errs.Errored() {
panic(errs.Err)
}
}
-73
View File
@@ -1,73 +0,0 @@
// (c) 2019-2021, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"github.com/luxfi/node/ids"
)
var (
_ RequestHandler = NoopRequestHandler{}
)
// RequestHandler interface handles incoming requests from peers
// Must have methods in format of handleType(context.Context, ids.NodeID, uint32, request Type) error
// so that the Request object of relevant Type can invoke its respective handle method
// on this struct.
// Also see GossipHandler for implementation style.
type RequestHandler interface {
HandleStateTrieLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error)
HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request BlockRequest) ([]byte, error)
HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error)
HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest MessageSignatureRequest) ([]byte, error)
HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest BlockSignatureRequest) ([]byte, error)
}
// ResponseHandler handles response for a sent request
// Only one of OnResponse or OnFailure is called for a given requestID, not both
type ResponseHandler interface {
// OnResponse is invoked when the peer responded to a request
OnResponse(response []byte) error
// OnFailure is invoked when there was a failure in processing a request
OnFailure() error
}
// CrossChainRequestHandler interface handles incoming cross chain requests
type CrossChainRequestHandler interface {
HandleCrossChainRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request []byte) ([]byte, error)
}
// GossipHandler interface handles incoming gossip messages
type GossipHandler interface {
HandleGossip(ctx context.Context, nodeID ids.NodeID, gossipBytes []byte)
}
// GossipMessage is a marker interface for gossip messages
type GossipMessage interface {
// Handle is called to process this gossip message
Handle(handler GossipHandler, nodeID ids.NodeID) error
}
type NoopRequestHandler struct{}
func (NoopRequestHandler) HandleStateTrieLeafsRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, leafsRequest LeafsRequest) ([]byte, error) {
return nil, nil
}
func (NoopRequestHandler) HandleBlockRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, request BlockRequest) ([]byte, error) {
return nil, nil
}
func (NoopRequestHandler) HandleCodeRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, codeRequest CodeRequest) ([]byte, error) {
return nil, nil
}
func (NoopRequestHandler) HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest MessageSignatureRequest) ([]byte, error) {
return nil, nil
}
func (NoopRequestHandler) HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest BlockSignatureRequest) ([]byte, error) {
return nil, nil
}
-63
View File
@@ -1,63 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/node/ids"
"github.com/ethereum/go-ethereum/common"
)
const MaxCodeHashesPerRequest = 5
var _ Request = LeafsRequest{}
// LeafsRequest is a request to receive trie leaves at specified Root within Start and End byte range
// Limit outlines maximum number of leaves to returns starting at Start
type LeafsRequest struct {
Root common.Hash `serialize:"true"`
Account common.Hash `serialize:"true"`
Start []byte `serialize:"true"`
End []byte `serialize:"true"`
Limit uint16 `serialize:"true"`
}
func (l LeafsRequest) String() string {
return fmt.Sprintf(
"LeafsRequest(Root=%s, Account=%s, Start=%s, End %s, Limit=%d)",
l.Root, l.Account, common.Bytes2Hex(l.Start), common.Bytes2Hex(l.End), l.Limit,
)
}
func (l LeafsRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleStateTrieLeafsRequest(ctx, nodeID, requestID, l)
}
// LeafsResponse is a response to a LeafsRequest
// Keys must be within LeafsRequest.Start and LeafsRequest.End and sorted in lexicographical order.
//
// ProofVals must be non-empty and contain a valid range proof unless the key-value pairs in the
// response are the entire trie.
// If the key-value pairs make up the entire trie, ProofVals should be empty since the root will be
// sufficient to prove that the leaves are included in the trie.
//
// More is a flag set in the client after verifying the response, which indicates if the last key-value
// pair in the response has any more elements to its right within the trie.
type LeafsResponse struct {
// Keys and Vals provides the key-value pairs in the trie in the response.
Keys [][]byte `serialize:"true"`
Vals [][]byte `serialize:"true"`
// More indicates if there are more leaves to the right of the last value in this response.
//
// This is not serialized since it is set in the client after verifying the response via
// VerifyRangeProof and determining if there are in fact more leaves to the right of the
// last value in this response.
More bool
// ProofVals contain the edge merkle-proofs for the range of keys included in the response.
// The keys for the proof are simply the keccak256 hashes of the values, so they are not included in the response to save bandwidth.
ProofVals [][]byte `serialize:"true"`
}
@@ -1,178 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"bytes"
"context"
"encoding/base64"
"math/rand"
"testing"
"github.com/luxfi/node/ids"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
)
// TestMarshalLeafsRequest asserts that the structure or serialization logic hasn't changed, primarily to
// ensure compatibility with the network.
func TestMarshalLeafsRequest(t *testing.T) {
// generate some random code data
// set random seed for deterministic random
rand.Seed(1)
startBytes := make([]byte, common.HashLength)
endBytes := make([]byte, common.HashLength)
_, err := rand.Read(startBytes)
assert.NoError(t, err)
_, err = rand.Read(endBytes)
assert.NoError(t, err)
leafsRequest := LeafsRequest{
Root: common.BytesToHash([]byte("im ROOTing for ya")),
Start: startBytes,
End: endBytes,
Limit: 1024,
}
base64LeafsRequest := "AAAAAAAAAAAAAAAAAAAAAABpbSBST09UaW5nIGZvciB5YQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFL9/AchgmVPFj9fD5piHXKVZsdNEAN8TXu7BAfR4sZJAAAAIIGFWthoHQ2G0ekeABZ5OctmlNLEIqzSCKAHKTlIf2mZBAA="
leafsRequestBytes, err := Codec.Marshal(Version, leafsRequest)
assert.NoError(t, err)
assert.Equal(t, base64LeafsRequest, base64.StdEncoding.EncodeToString(leafsRequestBytes))
var l LeafsRequest
_, err = Codec.Unmarshal(leafsRequestBytes, &l)
assert.NoError(t, err)
assert.Equal(t, leafsRequest.Root, l.Root)
assert.Equal(t, leafsRequest.Start, l.Start)
assert.Equal(t, leafsRequest.End, l.End)
assert.Equal(t, leafsRequest.Limit, l.Limit)
}
// TestMarshalLeafsResponse asserts that the structure or serialization logic hasn't changed, primarily to
// ensure compatibility with the network.
func TestMarshalLeafsResponse(t *testing.T) {
// generate some random code data
// set random seed for deterministic random
rand.Seed(1)
keysBytes := make([][]byte, 16)
valsBytes := make([][]byte, 16)
for i := range keysBytes {
keysBytes[i] = make([]byte, common.HashLength)
valsBytes[i] = make([]byte, rand.Intn(8)+8) // min 8 bytes, max 16 bytes
_, err := rand.Read(keysBytes[i])
assert.NoError(t, err)
_, err = rand.Read(valsBytes[i])
assert.NoError(t, err)
}
nextKey := make([]byte, common.HashLength)
_, err := rand.Read(nextKey)
assert.NoError(t, err)
proofVals := make([][]byte, 4)
for i := range proofVals {
proofVals[i] = make([]byte, rand.Intn(8)+8) // min 8 bytes, max 16 bytes
_, err = rand.Read(proofVals[i])
assert.NoError(t, err)
}
leafsResponse := LeafsResponse{
Keys: keysBytes,
Vals: valsBytes,
More: true,
ProofVals: proofVals,
}
base64LeafsResponse := "AAAAAAAQAAAAIE8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NAAAAIGagByk5SH9pmeudGKRHhARdh/PGfPInRumVr1olNnlRAAAAIK2zfFghtmgLTnyLdjobHUnUlVyEhiFjJSU/7HON16niAAAAIIYVu9oIMfUFmHWSHmaKW98sf8SERZLSVyvNBmjS1sUvAAAAIHHb2Wiw9xcu2FeUuzWLDDtSXaF4b5//CUJ52xlE69ehAAAAIPhMiSs77qX090OR9EXRWv1ClAQDdPaSS5jL+HE/jZYtAAAAIMr8yuOmvI+effHZKTM/+ZOTO+pvWzr23gN0NmxHGeQ6AAAAIBZZpE856x5YScYHfbtXIvVxeiiaJm+XZHmBmY6+qJwLAAAAIHOq53hmZ/fpNs1PJKv334ZrqlYDg2etYUXeHuj0qLCZAAAAIHiN5WOvpGfUnexqQOmh0AfwM8KCMGG90Oqln45NpkMBAAAAIKAQ13yW6oCnpmX2BvamO389/SVnwYl55NYPJmhtm/L7AAAAIAfuKbpk+Eq0PKDG5rkcH9O+iZBDQXnTr0SRo2kBLbktAAAAILsXyQKL6ZFOt2ScbJNHgAl50YMDVvKlTD3qsqS0R11jAAAAIOqxOTXzHYRIRRfpJK73iuFRwAdVklg2twdYhWUMMOwpAAAAIHnqPf5BNqv3UrO4Jx0D6USzyds2a3UEX479adIq5UEZAAAAIDLWEMqsbjP+qjJjo5lDcCS6nJsUZ4onTwGpEK4pX277AAAAEAAAAAmG0ekeABZ5OcsAAAAMuqL/bNRxxIPxX7kLAAAACov5IRGcFg8HAkQAAAAIUFTi0INr+EwAAAAOnQ97usvgJVqlt9RL7EAAAAAJfI0BkZLCQiTiAAAACxsGfYm8fwHx9XOYAAAADUs3OXARXoLtb0ElyPoAAAAKPr34iDoK2L6cOQAAAAoFIg0LKWiLc0uOAAAACCbJAf81TN4WAAAADBhPw50XNP9XFkKJUwAAAAuvvo+1aYfHf1gYUgAAAAqjcDk0v1CijaECAAAADkfLVT12lCZ670686kBrAAAADf5fWr9EzN4mO1YGYz4AAAAEAAAADlcyXwVWMEo+Pq4Uwo0MAAAADeo50qHks46vP0TGxu8AAAAOg2Ly9WQIVMFd/KyqiiwAAAAL7M5aOpS00zilFD4="
leafsResponseBytes, err := Codec.Marshal(Version, leafsResponse)
assert.NoError(t, err)
assert.Equal(t, base64LeafsResponse, base64.StdEncoding.EncodeToString(leafsResponseBytes))
var l LeafsResponse
_, err = Codec.Unmarshal(leafsResponseBytes, &l)
assert.NoError(t, err)
assert.Equal(t, leafsResponse.Keys, l.Keys)
assert.Equal(t, leafsResponse.Vals, l.Vals)
assert.False(t, l.More) // make sure it is not serialized
assert.Equal(t, leafsResponse.ProofVals, l.ProofVals)
}
func TestLeafsRequestValidation(t *testing.T) {
mockRequestHandler := &mockHandler{}
tests := map[string]struct {
request LeafsRequest
assertResponse func(t *testing.T)
}{
"node type StateTrieNode": {
request: LeafsRequest{
Root: common.BytesToHash([]byte("some hash goes here")),
Start: bytes.Repeat([]byte{0x00}, common.HashLength),
End: bytes.Repeat([]byte{0xff}, common.HashLength),
Limit: 10,
},
assertResponse: func(t *testing.T) {
assert.True(t, mockRequestHandler.handleStateTrieCalled)
assert.False(t, mockRequestHandler.handleBlockRequestCalled)
assert.False(t, mockRequestHandler.handleCodeRequestCalled)
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, _ = test.request.Handle(context.Background(), ids.GenerateTestNodeID(), 1, mockRequestHandler)
test.assertResponse(t)
mockRequestHandler.reset()
})
}
}
var _ RequestHandler = &mockHandler{}
type mockHandler struct {
handleStateTrieCalled,
handleBlockRequestCalled,
handleCodeRequestCalled,
handleMessageSignatureCalled,
handleBlockSignatureCalled bool
}
func (m *mockHandler) HandleStateTrieLeafsRequest(context.Context, ids.NodeID, uint32, LeafsRequest) ([]byte, error) {
m.handleStateTrieCalled = true
return nil, nil
}
func (m *mockHandler) HandleBlockRequest(context.Context, ids.NodeID, uint32, BlockRequest) ([]byte, error) {
m.handleBlockRequestCalled = true
return nil, nil
}
func (m *mockHandler) HandleCodeRequest(context.Context, ids.NodeID, uint32, CodeRequest) ([]byte, error) {
m.handleCodeRequestCalled = true
return nil, nil
}
func (m *mockHandler) HandleMessageSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest MessageSignatureRequest) ([]byte, error) {
m.handleMessageSignatureCalled = true
return nil, nil
}
func (m *mockHandler) HandleBlockSignatureRequest(ctx context.Context, nodeID ids.NodeID, requestID uint32, signatureRequest BlockSignatureRequest) ([]byte, error) {
m.handleBlockSignatureCalled = true
return nil, nil
}
func (m *mockHandler) reset() {
m.handleStateTrieCalled = false
m.handleBlockRequestCalled = false
m.handleCodeRequestCalled = false
}
-27
View File
@@ -1,27 +0,0 @@
// (c) 2019-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/node/codec"
"github.com/luxfi/evm/interfaces"
)
// Request represents a Network request type
type Request interface {
// Requests should implement String() for logging.
fmt.Stringer
// Handle allows `Request` to call respective methods on handler to handle
// this particular request type
Handle(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, handler RequestHandler) ([]byte, error)
}
// RequestToBytes marshals the given request object into bytes
func RequestToBytes(codec codec.Manager, request Request) ([]byte, error) {
return codec.Marshal(Version, &request)
}
@@ -1,49 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/utils/crypto/bls"
)
var (
_ Request = MessageSignatureRequest{}
_ Request = BlockSignatureRequest{}
)
// MessageSignatureRequest is used to request a warp message's signature.
type MessageSignatureRequest struct {
MessageID ids.ID `serialize:"true"`
}
func (s MessageSignatureRequest) String() string {
return fmt.Sprintf("MessageSignatureRequest(MessageID=%s)", s.MessageID.String())
}
func (s MessageSignatureRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleMessageSignatureRequest(ctx, nodeID, requestID, s)
}
// BlockSignatureRequest is used to request a warp message's signature.
type BlockSignatureRequest struct {
BlockID ids.ID `serialize:"true"`
}
func (s BlockSignatureRequest) String() string {
return fmt.Sprintf("BlockSignatureRequest(BlockID=%s)", s.BlockID.String())
}
func (s BlockSignatureRequest) Handle(ctx context.Context, nodeID ids.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleBlockSignatureRequest(ctx, nodeID, requestID, s)
}
// SignatureResponse is the response to a BlockSignatureRequest or MessageSignatureRequest.
// The response contains a BLS signature of the requested message, signed by the responding node's BLS private key.
type SignatureResponse struct {
Signature [bls.SignatureLen]byte `serialize:"true"`
}
@@ -1,49 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/crypto/bls"
)
var (
_ Request = MessageSignatureRequest{}
_ Request = BlockSignatureRequest{}
)
// MessageSignatureRequest is used to request a warp message's signature.
type MessageSignatureRequest struct {
MessageID interfaces.ID `serialize:"true"`
}
func (s MessageSignatureRequest) String() string {
return fmt.Sprintf("MessageSignatureRequest(MessageID=%s)", s.MessageID.String())
}
func (s MessageSignatureRequest) Handle(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleMessageSignatureRequest(ctx, nodeID, requestID, s)
}
// BlockSignatureRequest is used to request a warp message's signature.
type BlockSignatureRequest struct {
BlockID interfaces.ID `serialize:"true"`
}
func (s BlockSignatureRequest) String() string {
return fmt.Sprintf("BlockSignatureRequest(BlockID=%s)", s.BlockID.String())
}
func (s BlockSignatureRequest) Handle(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, handler RequestHandler) ([]byte, error) {
return handler.HandleBlockSignatureRequest(ctx, nodeID, requestID, s)
}
// SignatureResponse is the response to a BlockSignatureRequest or MessageSignatureRequest.
// The response contains a BLS signature of the requested message, signed by the responding node's BLS private key.
type SignatureResponse struct {
Signature [bls.SignatureLen]byte `serialize:"true"`
}
@@ -1,73 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"encoding/base64"
"encoding/hex"
"testing"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/utils/crypto/bls"
"github.com/stretchr/testify/require"
)
// TestMarshalMessageSignatureRequest asserts that the structure or serialization logic hasn't changed, primarily to
// ensure compatibility with the network.
func TestMarshalMessageSignatureRequest(t *testing.T) {
signatureRequest := MessageSignatureRequest{
MessageID: ids.ID{68, 79, 70, 65, 72, 73, 64, 107},
}
base64MessageSignatureRequest := "AABET0ZBSElAawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
signatureRequestBytes, err := Codec.Marshal(Version, signatureRequest)
require.NoError(t, err)
require.Equal(t, base64MessageSignatureRequest, base64.StdEncoding.EncodeToString(signatureRequestBytes))
var s MessageSignatureRequest
_, err = Codec.Unmarshal(signatureRequestBytes, &s)
require.NoError(t, err)
require.Equal(t, signatureRequest.MessageID, s.MessageID)
}
// TestMarshalBlockSignatureRequest asserts that the structure or serialization logic hasn't changed, primarily to
// ensure compatibility with the network.
func TestMarshalBlockSignatureRequest(t *testing.T) {
signatureRequest := BlockSignatureRequest{
BlockID: ids.ID{68, 79, 70, 65, 72, 73, 64, 107},
}
base64BlockSignatureRequest := "AABET0ZBSElAawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
signatureRequestBytes, err := Codec.Marshal(Version, signatureRequest)
require.NoError(t, err)
require.Equal(t, base64BlockSignatureRequest, base64.StdEncoding.EncodeToString(signatureRequestBytes))
var s BlockSignatureRequest
_, err = Codec.Unmarshal(signatureRequestBytes, &s)
require.NoError(t, err)
require.Equal(t, signatureRequest.BlockID, s.BlockID)
}
// TestMarshalSignatureResponse asserts that the structure or serialization logic hasn't changed, primarily to
// ensure compatibility with the network.
func TestMarshalSignatureResponse(t *testing.T) {
var signature [bls.SignatureLen]byte
sig, err := hex.DecodeString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
require.NoError(t, err, "failed to decode string to hex")
copy(signature[:], sig)
signatureResponse := SignatureResponse{
Signature: signature,
}
base64SignatureResponse := "AAABI0VniavN7wEjRWeJq83vASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8="
signatureResponseBytes, err := Codec.Marshal(Version, signatureResponse)
require.NoError(t, err)
require.Equal(t, base64SignatureResponse, base64.StdEncoding.EncodeToString(signatureResponseBytes))
var s SignatureResponse
_, err = Codec.Unmarshal(signatureResponseBytes, &s)
require.NoError(t, err)
require.Equal(t, signatureResponse.Signature, s.Signature)
}
-89
View File
@@ -1,89 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/node/ids"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/luxfi/node/consensus/engine/linear/block"
)
var _ block.StateSummary = &SyncSummary{}
// SyncSummary provides the information necessary to sync a node starting
// at the given block.
type SyncSummary struct {
BlockNumber uint64 `serialize:"true"`
BlockHash common.Hash `serialize:"true"`
BlockRoot common.Hash `serialize:"true"`
summaryID ids.ID
bytes []byte
acceptImpl func(SyncSummary) (block.StateSyncMode, error)
}
func NewSyncSummaryFromBytes(summaryBytes []byte, acceptImpl func(SyncSummary) (block.StateSyncMode, error)) (SyncSummary, error) {
summary := SyncSummary{}
if codecVersion, err := Codec.Unmarshal(summaryBytes, &summary); err != nil {
return SyncSummary{}, err
} else if codecVersion != Version {
return SyncSummary{}, fmt.Errorf("failed to parse syncable summary due to unexpected codec version (%d != %d)", codecVersion, Version)
}
summary.bytes = summaryBytes
summaryID, err := ids.ToID(crypto.Keccak256(summaryBytes))
if err != nil {
return SyncSummary{}, err
}
summary.summaryID = summaryID
summary.acceptImpl = acceptImpl
return summary, nil
}
func NewSyncSummary(blockHash common.Hash, blockNumber uint64, blockRoot common.Hash) (SyncSummary, error) {
summary := SyncSummary{
BlockNumber: blockNumber,
BlockHash: blockHash,
BlockRoot: blockRoot,
}
bytes, err := Codec.Marshal(Version, &summary)
if err != nil {
return SyncSummary{}, err
}
summary.bytes = bytes
summaryID, err := ids.ToID(crypto.Keccak256(bytes))
if err != nil {
return SyncSummary{}, err
}
summary.summaryID = summaryID
return summary, nil
}
func (s SyncSummary) Bytes() []byte {
return s.bytes
}
func (s SyncSummary) Height() uint64 {
return s.BlockNumber
}
func (s SyncSummary) ID() ids.ID {
return s.summaryID
}
func (s SyncSummary) String() string {
return fmt.Sprintf("SyncSummary(BlockHash=%s, BlockNumber=%d, BlockRoot=%s)", s.BlockHash, s.BlockNumber, s.BlockRoot)
}
func (s SyncSummary) Accept(context.Context) (block.StateSyncMode, error) {
if s.acceptImpl == nil {
return block.StateSyncSkipped, fmt.Errorf("accept implementation not specified for summary: %s", s)
}
return s.acceptImpl(s)
}
-89
View File
@@ -1,89 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package message
import (
"context"
"fmt"
"github.com/luxfi/evm/interfaces"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/luxfi/node/consensus/engine/linear/block"
)
var _ block.StateSummary = &SyncSummary{}
// SyncSummary provides the information necessary to sync a node starting
// at the given block.
type SyncSummary struct {
BlockNumber uint64 `serialize:"true"`
BlockHash common.Hash `serialize:"true"`
BlockRoot common.Hash `serialize:"true"`
summaryID interfaces.ID
bytes []byte
acceptImpl func(SyncSummary) (block.StateSyncMode, error)
}
func NewSyncSummaryFromBytes(summaryBytes []byte, acceptImpl func(SyncSummary) (block.StateSyncMode, error)) (SyncSummary, error) {
summary := SyncSummary{}
if codecVersion, err := Codec.Unmarshal(summaryBytes, &summary); err != nil {
return SyncSummary{}, err
} else if codecVersion != Version {
return SyncSummary{}, fmt.Errorf("failed to parse syncable summary due to unexpected codec version (%d != %d)", codecVersion, Version)
}
summary.bytes = summaryBytes
summaryID, err := ids.ToID(crypto.Keccak256(summaryBytes))
if err != nil {
return SyncSummary{}, err
}
summary.summaryID = summaryID
summary.acceptImpl = acceptImpl
return summary, nil
}
func NewSyncSummary(blockHash common.Hash, blockNumber uint64, blockRoot common.Hash) (SyncSummary, error) {
summary := SyncSummary{
BlockNumber: blockNumber,
BlockHash: blockHash,
BlockRoot: blockRoot,
}
bytes, err := Codec.Marshal(Version, &summary)
if err != nil {
return SyncSummary{}, err
}
summary.bytes = bytes
summaryID, err := ids.ToID(crypto.Keccak256(bytes))
if err != nil {
return SyncSummary{}, err
}
summary.summaryID = summaryID
return summary, nil
}
func (s SyncSummary) Bytes() []byte {
return s.bytes
}
func (s SyncSummary) Height() uint64 {
return s.BlockNumber
}
func (s SyncSummary) ID() interfaces.ID {
return s.summaryID
}
func (s SyncSummary) String() string {
return fmt.Sprintf("SyncSummary(BlockHash=%s, BlockNumber=%d, BlockRoot=%s)", s.BlockHash, s.BlockNumber, s.BlockRoot)
}
func (s SyncSummary) Accept(context.Context) (block.StateSyncMode, error) {
if s.acceptImpl == nil {
return block.StateSyncSkipped, fmt.Errorf("accept implementation not specified for summary: %s", s)
}
return s.acceptImpl(s)
}
-64
View File
@@ -1,64 +0,0 @@
// (c) 2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"github.com/luxfi/node/codec"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/geth/ethdb"
"github.com/ethereum/go-ethereum/metrics"
"github.com/luxfi/evm/plugin/evm/message"
syncHandlers "github.com/luxfi/evm/sync/handlers"
syncStats "github.com/luxfi/evm/sync/handlers/stats"
"github.com/ethereum/go-ethereum/trie"
"github.com/luxfi/evm/warp"
warpHandlers "github.com/luxfi/evm/warp/handlers"
)
var _ message.RequestHandler = &networkHandler{}
type networkHandler struct {
stateTrieLeafsRequestHandler *syncHandlers.LeafsRequestHandler
blockRequestHandler *syncHandlers.BlockRequestHandler
codeRequestHandler *syncHandlers.CodeRequestHandler
signatureRequestHandler *warpHandlers.SignatureRequestHandler
}
// newNetworkHandler constructs the handler for serving network requests.
func newNetworkHandler(
provider syncHandlers.SyncDataProvider,
diskDB ethdb.KeyValueReader,
evmTrieDB *triedb.Database,
warpBackend warp.Backend,
networkCodec codec.Manager,
) message.RequestHandler {
syncStats := syncStats.NewHandlerStats(metrics.Enabled)
return &networkHandler{
stateTrieLeafsRequestHandler: syncHandlers.NewLeafsRequestHandler(evmTrieDB, provider, networkCodec, syncStats),
blockRequestHandler: syncHandlers.NewBlockRequestHandler(provider, networkCodec, syncStats),
codeRequestHandler: syncHandlers.NewCodeRequestHandler(diskDB, networkCodec, syncStats),
signatureRequestHandler: warpHandlers.NewSignatureRequestHandler(warpBackend, networkCodec),
}
}
func (n networkHandler) HandleStateTrieLeafsRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, leafsRequest message.LeafsRequest) ([]byte, error) {
return n.stateTrieLeafsRequestHandler.OnLeafsRequest(ctx, nodeID, requestID, leafsRequest)
}
func (n networkHandler) HandleBlockRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, blockRequest message.BlockRequest) ([]byte, error) {
return n.blockRequestHandler.OnBlockRequest(ctx, nodeID, requestID, blockRequest)
}
func (n networkHandler) HandleCodeRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, codeRequest message.CodeRequest) ([]byte, error) {
return n.codeRequestHandler.OnCodeRequest(ctx, nodeID, requestID, codeRequest)
}
func (n networkHandler) HandleMessageSignatureRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, messageSignatureRequest message.MessageSignatureRequest) ([]byte, error) {
return n.signatureRequestHandler.OnMessageSignatureRequest(ctx, nodeID, requestID, messageSignatureRequest)
}
func (n networkHandler) HandleBlockSignatureRequest(ctx context.Context, nodeID interfaces.NodeID, requestID uint32, blockSignatureRequest message.BlockSignatureRequest) ([]byte, error) {
return n.signatureRequestHandler.OnBlockSignatureRequest(ctx, nodeID, requestID, blockSignatureRequest)
}
-365
View File
@@ -1,365 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"fmt"
"sync"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/database/versiondb"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/choices"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/node/vms/components/chain"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/luxfi/evm/core/state/snapshot"
"github.com/ethereum/go-ethereum/eth"
"github.com/luxfi/evm/geth/ethdb"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/message"
syncclient "github.com/luxfi/evm/sync/client"
"github.com/luxfi/evm/sync/statesync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
const (
// State sync fetches [parentsToGet] parents of the block it syncs to.
// The last 256 block hashes are necessary to support the BLOCKHASH opcode.
parentsToGet = 256
)
var stateSyncSummaryKey = []byte("stateSyncSummary")
// stateSyncClientConfig defines the options and dependencies needed to construct a StateSyncerClient
type stateSyncClientConfig struct {
enabled bool
skipResume bool
// Specifies the number of blocks behind the latest state summary that the chain must be
// in order to prefer performing state sync over falling back to the normal bootstrapping
// algorithm.
stateSyncMinBlocks uint64
stateSyncRequestSize uint16 // number of key/value pairs to ask peers for per request
lastAcceptedHeight uint64
chain *eth.Ethereum
state *chain.State
chaindb ethdb.Database
metadataDB database.Database
acceptedBlockDB database.Database
db *versiondb.Database
client syncclient.Client
toEngine chan<- commonEng.Message
}
type stateSyncerClient struct {
*stateSyncClientConfig
resumableSummary message.SyncSummary
cancel context.CancelFunc
wg sync.WaitGroup
// State Sync results
syncSummary message.SyncSummary
stateSyncErr error
}
func NewStateSyncClient(config *stateSyncClientConfig) StateSyncClient {
return &stateSyncerClient{
stateSyncClientConfig: config,
}
}
type StateSyncClient interface {
// methods that implement the client side of [block.StateSyncableVM]
StateSyncEnabled(context.Context) (bool, error)
GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error)
ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error)
// additional methods required by the evm package
ClearOngoingSummary() error
Shutdown() error
Error() error
}
// Syncer represents a step in state sync,
// along with Start/Done methods to control
// and monitor progress.
// Error returns an error if any was encountered.
type Syncer interface {
Start(ctx context.Context) error
Done() <-chan error
}
// StateSyncEnabled returns [client.enabled], which is set in the chain's config file.
func (client *stateSyncerClient) StateSyncEnabled(context.Context) (bool, error) {
return client.enabled, nil
}
// GetOngoingSyncStateSummary returns a state summary that was previously started
// and not finished, and sets [resumableSummary] if one was found.
// Returns [database.ErrNotFound] if no ongoing summary is found or if [client.skipResume] is true.
func (client *stateSyncerClient) GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error) {
if client.skipResume {
return nil, database.ErrNotFound
}
summaryBytes, err := client.metadataDB.Get(stateSyncSummaryKey)
if err != nil {
return nil, err // includes the [database.ErrNotFound] case
}
summary, err := message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
if err != nil {
return nil, fmt.Errorf("failed to parse saved state sync summary to SyncSummary: %w", err)
}
client.resumableSummary = summary
return summary, nil
}
// ClearOngoingSummary clears any marker of an ongoing state sync summary
func (client *stateSyncerClient) ClearOngoingSummary() error {
if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
return fmt.Errorf("failed to clear ongoing summary: %w", err)
}
if err := client.db.Commit(); err != nil {
return fmt.Errorf("failed to commit db while clearing ongoing summary: %w", err)
}
return nil
}
// ParseStateSummary parses [summaryBytes] to [commonEng.Summary]
func (client *stateSyncerClient) ParseStateSummary(_ context.Context, summaryBytes []byte) (block.StateSummary, error) {
return message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
}
// stateSync blockingly performs the state sync for the EVM state and the atomic state
// to [client.syncSummary]. returns an error if one occurred.
func (client *stateSyncerClient) stateSync(ctx context.Context) error {
if err := client.syncBlocks(ctx, client.syncSummary.BlockHash, client.syncSummary.BlockNumber, parentsToGet); err != nil {
return err
}
// Sync the EVM trie.
return client.syncStateTrie(ctx)
}
// acceptSyncSummary returns true if sync will be performed and launches the state sync process
// in a goroutine.
func (client *stateSyncerClient) acceptSyncSummary(proposedSummary message.SyncSummary) (block.StateSyncMode, error) {
isResume := proposedSummary.BlockHash == client.resumableSummary.BlockHash
if !isResume {
// Skip syncing if the blockchain is not significantly ahead of local state,
// since bootstrapping would be faster.
// (Also ensures we don't sync to a height prior to local state.)
if client.lastAcceptedHeight+client.stateSyncMinBlocks > proposedSummary.Height() {
log.Info(
"last accepted too close to most recent syncable block, skipping state sync",
"lastAccepted", client.lastAcceptedHeight,
"syncableHeight", proposedSummary.Height(),
)
return block.StateSyncSkipped, nil
}
// Wipe the snapshot completely if we are not resuming from an existing sync, so that we do not
// use a corrupted snapshot.
// Note: this assumes that when the node is started with state sync disabled, the in-progress state
// sync marker will be wiped, so we do not accidentally resume progress from an incorrect version
// of the snapshot. (if switching between versions that come before this change and back this could
// lead to the snapshot not being cleaned up correctly)
<-snapshot.WipeSnapshot(client.chaindb, true)
// Reset the snapshot generator here so that when state sync completes, snapshots will not attempt to read an
// invalid generator.
// Note: this must be called after WipeSnapshot is called so that we do not invalidate a partially generated snapshot.
snapshot.ResetSnapshotGeneration(client.chaindb)
}
client.syncSummary = proposedSummary
// Update the current state sync summary key in the database
// Note: this must be performed after WipeSnapshot finishes so that we do not start a state sync
// session from a partially wiped snapshot.
if err := client.metadataDB.Put(stateSyncSummaryKey, proposedSummary.Bytes()); err != nil {
return block.StateSyncSkipped, fmt.Errorf("failed to write state sync summary key to disk: %w", err)
}
if err := client.db.Commit(); err != nil {
return block.StateSyncSkipped, fmt.Errorf("failed to commit db: %w", err)
}
log.Info("Starting state sync", "summary", proposedSummary)
// create a cancellable ctx for the state sync goroutine
ctx, cancel := context.WithCancel(context.Background())
client.cancel = cancel
client.wg.Add(1) // track the state sync goroutine so we can wait for it on shutdown
go func() {
defer client.wg.Done()
defer cancel()
if err := client.stateSync(ctx); err != nil {
client.stateSyncErr = err
} else {
client.stateSyncErr = client.finishSync()
}
// notify engine regardless of whether err == nil,
// this error will be propagated to the engine when it calls
// vm.SetState(consensus.Bootstrapping)
log.Info("stateSync completed, notifying engine", "err", client.stateSyncErr)
client.toEngine <- commonEng.StateSyncDone
}()
return block.StateSyncStatic, nil
}
// syncBlocks fetches (up to) [parentsToGet] blocks from peers
// using [client] and writes them to disk.
// the process begins with [fromHash] and it fetches parents recursively.
// fetching starts from the first ancestor not found on disk
func (client *stateSyncerClient) syncBlocks(ctx context.Context, fromHash common.Hash, fromHeight uint64, parentsToGet int) error {
nextHash := fromHash
nextHeight := fromHeight
parentsPerRequest := uint16(32)
// first, check for blocks already available on disk so we don't
// request them from peers.
for parentsToGet >= 0 {
blk := rawdb.ReadBlock(client.chaindb, nextHash, nextHeight)
if blk != nil {
// block exists
nextHash = blk.ParentHash()
nextHeight--
parentsToGet--
continue
}
// block was not found
break
}
// get any blocks we couldn't find on disk from peers and write
// them to disk.
batch := client.chaindb.NewBatch()
for i := parentsToGet - 1; i >= 0 && (nextHash != common.Hash{}); {
if err := ctx.Err(); err != nil {
return err
}
blocks, err := client.client.GetBlocks(ctx, nextHash, nextHeight, parentsPerRequest)
if err != nil {
log.Error("could not get blocks from peer", "err", err, "nextHash", nextHash, "remaining", i+1)
return err
}
for _, block := range blocks {
rawdb.WriteBlock(batch, block)
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
i--
nextHash = block.ParentHash()
nextHeight--
}
log.Info("fetching blocks from peer", "remaining", i+1, "total", parentsToGet)
}
log.Info("fetched blocks from peer", "total", parentsToGet)
return batch.Write()
}
func (client *stateSyncerClient) syncStateTrie(ctx context.Context) error {
log.Info("state sync: sync starting", "root", client.syncSummary.BlockRoot)
evmSyncer, err := statesync.NewStateSyncer(&statesync.StateSyncerConfig{
Client: client.client,
Root: client.syncSummary.BlockRoot,
BatchSize: ethdb.IdealBatchSize,
DB: client.chaindb,
MaxOutstandingCodeHashes: statesync.DefaultMaxOutstandingCodeHashes,
NumCodeFetchingWorkers: statesync.DefaultNumCodeFetchingWorkers,
RequestSize: client.stateSyncRequestSize,
})
if err != nil {
return err
}
if err := evmSyncer.Start(ctx); err != nil {
return err
}
err = <-evmSyncer.Done()
log.Info("state sync: sync finished", "root", client.syncSummary.BlockRoot, "err", err)
return err
}
func (client *stateSyncerClient) Shutdown() error {
if client.cancel != nil {
client.cancel()
}
client.wg.Wait() // wait for the background goroutine to exit
return nil
}
// finishSync is responsible for updating disk and memory pointers so the VM is prepared
// for bootstrapping.
func (client *stateSyncerClient) finishSync() error {
stateBlock, err := client.state.GetBlock(context.TODO(), interfaces.ID(client.syncSummary.BlockHash))
if err != nil {
return fmt.Errorf("could not get block by hash from client state: %s", client.syncSummary.BlockHash)
}
wrapper, ok := stateBlock.(*chain.BlockWrapper)
if !ok {
return fmt.Errorf("could not convert block(%T) to *chain.BlockWrapper", wrapper)
}
evmBlock, ok := wrapper.Block.(*Block)
if !ok {
return fmt.Errorf("could not convert block(%T) to evm.Block", stateBlock)
}
block := evmBlock.ethBlock
if block.Hash() != client.syncSummary.BlockHash {
return fmt.Errorf("attempted to set last summary block to unexpected block hash: (%s != %s)", block.Hash(), client.syncSummary.BlockHash)
}
if block.NumberU64() != client.syncSummary.BlockNumber {
return fmt.Errorf("attempted to set last summary block to unexpected block number: (%d != %d)", block.NumberU64(), client.syncSummary.BlockNumber)
}
// BloomIndexer needs to know that some parts of the chain are not available
// and cannot be indexed. This is done by calling [AddCheckpoint] here.
// Since the indexer uses sections of size [params.BloomBitsBlocks] (= 4096),
// each block is indexed in section number [blockNumber/params.BloomBitsBlocks].
// To allow the indexer to start with the block we just synced to,
// we create a checkpoint for its parent.
// Note: This requires assuming the synced block height is divisible
// by [params.BloomBitsBlocks].
parentHeight := block.NumberU64() - 1
parentHash := block.ParentHash()
client.chain.BloomIndexer().AddCheckpoint(parentHeight/params.BloomBitsBlocks, parentHash)
if err := client.chain.BlockChain().ResetToStateSyncedBlock(block); err != nil {
return err
}
if err := client.updateVMMarkers(); err != nil {
return fmt.Errorf("error updating vm markers, height=%d, hash=%s, err=%w", block.NumberU64(), block.Hash(), err)
}
return client.state.SetLastAcceptedBlock(evmBlock)
}
// updateVMMarkers updates the following markers in the VM's database
// and commits them atomically:
// - updates lastAcceptedKey
// - removes state sync progress markers
func (client *stateSyncerClient) updateVMMarkers() error {
if err := client.acceptedBlockDB.Put(lastAcceptedKey, client.syncSummary.BlockHash[:]); err != nil {
return err
}
if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
return err
}
return client.db.Commit()
}
// Error returns a non-nil error if one occurred during the sync.
func (client *stateSyncerClient) Error() error { return client.stateSyncErr }
-365
View File
@@ -1,365 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"fmt"
"sync"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/choices"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/node/vms/components/chain"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/luxfi/evm/core/state/snapshot"
"github.com/ethereum/go-ethereum/eth"
"github.com/luxfi/evm/geth/ethdb"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/message"
syncclient "github.com/luxfi/evm/sync/client"
"github.com/luxfi/evm/sync/statesync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
const (
// State sync fetches [parentsToGet] parents of the block it syncs to.
// The last 256 block hashes are necessary to support the BLOCKHASH opcode.
parentsToGet = 256
)
var stateSyncSummaryKey = []byte("stateSyncSummary")
// stateSyncClientConfig defines the options and dependencies needed to construct a StateSyncerClient
type stateSyncClientConfig struct {
enabled bool
skipResume bool
// Specifies the number of blocks behind the latest state summary that the chain must be
// in order to prefer performing state sync over falling back to the normal bootstrapping
// algorithm.
stateSyncMinBlocks uint64
stateSyncRequestSize uint16 // number of key/value pairs to ask peers for per request
lastAcceptedHeight uint64
chain *eth.Ethereum
state *chain.State
chaindb ethdb.Database
metadataDB interfaces.Database
acceptedBlockDB interfaces.Database
db *versiondb.Database
client syncclient.Client
toEngine chan<- commonEng.Message
}
type stateSyncerClient struct {
*stateSyncClientConfig
resumableSummary message.SyncSummary
cancel context.CancelFunc
wg sync.WaitGroup
// State Sync results
syncSummary message.SyncSummary
stateSyncErr error
}
func NewStateSyncClient(config *stateSyncClientConfig) StateSyncClient {
return &stateSyncerClient{
stateSyncClientConfig: config,
}
}
type StateSyncClient interface {
// methods that implement the client side of [block.StateSyncableVM]
StateSyncEnabled(context.Context) (bool, error)
GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error)
ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error)
// additional methods required by the evm package
ClearOngoingSummary() error
Shutdown() error
Error() error
}
// Syncer represents a step in state sync,
// along with Start/Done methods to control
// and monitor progress.
// Error returns an error if any was encountered.
type Syncer interface {
Start(ctx context.Context) error
Done() <-chan error
}
// StateSyncEnabled returns [client.enabled], which is set in the chain's config file.
func (client *stateSyncerClient) StateSyncEnabled(context.Context) (bool, error) {
return client.enabled, nil
}
// GetOngoingSyncStateSummary returns a state summary that was previously started
// and not finished, and sets [resumableSummary] if one was found.
// Returns [database.ErrNotFound] if no ongoing summary is found or if [client.skipResume] is true.
func (client *stateSyncerClient) GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error) {
if client.skipResume {
return nil, database.ErrNotFound
}
summaryBytes, err := client.metadataDB.Get(stateSyncSummaryKey)
if err != nil {
return nil, err // includes the [database.ErrNotFound] case
}
summary, err := message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
if err != nil {
return nil, fmt.Errorf("failed to parse saved state sync summary to SyncSummary: %w", err)
}
client.resumableSummary = summary
return summary, nil
}
// ClearOngoingSummary clears any marker of an ongoing state sync summary
func (client *stateSyncerClient) ClearOngoingSummary() error {
if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
return fmt.Errorf("failed to clear ongoing summary: %w", err)
}
if err := client.db.Commit(); err != nil {
return fmt.Errorf("failed to commit db while clearing ongoing summary: %w", err)
}
return nil
}
// ParseStateSummary parses [summaryBytes] to [commonEng.Summary]
func (client *stateSyncerClient) ParseStateSummary(_ context.Context, summaryBytes []byte) (block.StateSummary, error) {
return message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
}
// stateSync blockingly performs the state sync for the EVM state and the atomic state
// to [client.syncSummary]. returns an error if one occurred.
func (client *stateSyncerClient) stateSync(ctx context.Context) error {
if err := client.syncBlocks(ctx, client.syncSummary.BlockHash, client.syncSummary.BlockNumber, parentsToGet); err != nil {
return err
}
// Sync the EVM trie.
return client.syncStateTrie(ctx)
}
// acceptSyncSummary returns true if sync will be performed and launches the state sync process
// in a goroutine.
func (client *stateSyncerClient) acceptSyncSummary(proposedSummary message.SyncSummary) (block.StateSyncMode, error) {
isResume := proposedSummary.BlockHash == client.resumableSummary.BlockHash
if !isResume {
// Skip syncing if the blockchain is not significantly ahead of local state,
// since bootstrapping would be faster.
// (Also ensures we don't sync to a height prior to local state.)
if client.lastAcceptedHeight+client.stateSyncMinBlocks > proposedSummary.Height() {
log.Info(
"last accepted too close to most recent syncable block, skipping state sync",
"lastAccepted", client.lastAcceptedHeight,
"syncableHeight", proposedSummary.Height(),
)
return block.StateSyncSkipped, nil
}
// Wipe the snapshot completely if we are not resuming from an existing sync, so that we do not
// use a corrupted snapshot.
// Note: this assumes that when the node is started with state sync disabled, the in-progress state
// sync marker will be wiped, so we do not accidentally resume progress from an incorrect version
// of the snapshot. (if switching between versions that come before this change and back this could
// lead to the snapshot not being cleaned up correctly)
<-snapshot.WipeSnapshot(client.chaindb, true)
// Reset the snapshot generator here so that when state sync completes, snapshots will not attempt to read an
// invalid generator.
// Note: this must be called after WipeSnapshot is called so that we do not invalidate a partially generated snapshot.
snapshot.ResetSnapshotGeneration(client.chaindb)
}
client.syncSummary = proposedSummary
// Update the current state sync summary key in the database
// Note: this must be performed after WipeSnapshot finishes so that we do not start a state sync
// session from a partially wiped snapshot.
if err := client.metadataDB.Put(stateSyncSummaryKey, proposedSummary.Bytes()); err != nil {
return block.StateSyncSkipped, fmt.Errorf("failed to write state sync summary key to disk: %w", err)
}
if err := client.db.Commit(); err != nil {
return block.StateSyncSkipped, fmt.Errorf("failed to commit db: %w", err)
}
log.Info("Starting state sync", "summary", proposedSummary)
// create a cancellable ctx for the state sync goroutine
ctx, cancel := context.WithCancel(context.Background())
client.cancel = cancel
client.wg.Add(1) // track the state sync goroutine so we can wait for it on shutdown
go func() {
defer client.wg.Done()
defer cancel()
if err := client.stateSync(ctx); err != nil {
client.stateSyncErr = err
} else {
client.stateSyncErr = client.finishSync()
}
// notify engine regardless of whether err == nil,
// this error will be propagated to the engine when it calls
// vm.SetState(consensus.Bootstrapping)
log.Info("stateSync completed, notifying engine", "err", client.stateSyncErr)
client.toEngine <- commonEng.StateSyncDone
}()
return block.StateSyncStatic, nil
}
// syncBlocks fetches (up to) [parentsToGet] blocks from peers
// using [client] and writes them to disk.
// the process begins with [fromHash] and it fetches parents recursively.
// fetching starts from the first ancestor not found on disk
func (client *stateSyncerClient) syncBlocks(ctx context.Context, fromHash common.Hash, fromHeight uint64, parentsToGet int) error {
nextHash := fromHash
nextHeight := fromHeight
parentsPerRequest := uint16(32)
// first, check for blocks already available on disk so we don't
// request them from peers.
for parentsToGet >= 0 {
blk := rawdb.ReadBlock(client.chaindb, nextHash, nextHeight)
if blk != nil {
// block exists
nextHash = blk.ParentHash()
nextHeight--
parentsToGet--
continue
}
// block was not found
break
}
// get any blocks we couldn't find on disk from peers and write
// them to disk.
batch := client.chaindb.NewBatch()
for i := parentsToGet - 1; i >= 0 && (nextHash != common.Hash{}); {
if err := ctx.Err(); err != nil {
return err
}
blocks, err := client.client.GetBlocks(ctx, nextHash, nextHeight, parentsPerRequest)
if err != nil {
log.Error("could not get blocks from peer", "err", err, "nextHash", nextHash, "remaining", i+1)
return err
}
for _, block := range blocks {
rawdb.WriteBlock(batch, block)
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
i--
nextHash = block.ParentHash()
nextHeight--
}
log.Info("fetching blocks from peer", "remaining", i+1, "total", parentsToGet)
}
log.Info("fetched blocks from peer", "total", parentsToGet)
return batch.Write()
}
func (client *stateSyncerClient) syncStateTrie(ctx context.Context) error {
log.Info("state sync: sync starting", "root", client.syncSummary.BlockRoot)
evmSyncer, err := statesync.NewStateSyncer(&statesync.StateSyncerConfig{
Client: client.client,
Root: client.syncSummary.BlockRoot,
BatchSize: ethdb.IdealBatchSize,
DB: client.chaindb,
MaxOutstandingCodeHashes: statesync.DefaultMaxOutstandingCodeHashes,
NumCodeFetchingWorkers: statesync.DefaultNumCodeFetchingWorkers,
RequestSize: client.stateSyncRequestSize,
})
if err != nil {
return err
}
if err := evmSyncer.Start(ctx); err != nil {
return err
}
err = <-evmSyncer.Done()
log.Info("state sync: sync finished", "root", client.syncSummary.BlockRoot, "err", err)
return err
}
func (client *stateSyncerClient) Shutdown() error {
if client.cancel != nil {
client.cancel()
}
client.wg.Wait() // wait for the background goroutine to exit
return nil
}
// finishSync is responsible for updating disk and memory pointers so the VM is prepared
// for bootstrapping.
func (client *stateSyncerClient) finishSync() error {
stateBlock, err := client.state.GetBlock(context.TODO(), interfaces.ID(client.syncSummary.BlockHash))
if err != nil {
return fmt.Errorf("could not get block by hash from client state: %s", client.syncSummary.BlockHash)
}
wrapper, ok := stateBlock.(*chain.BlockWrapper)
if !ok {
return fmt.Errorf("could not convert block(%T) to *chain.BlockWrapper", wrapper)
}
evmBlock, ok := wrapper.Block.(*Block)
if !ok {
return fmt.Errorf("could not convert block(%T) to evm.Block", stateBlock)
}
block := evmBlock.ethBlock
if block.Hash() != client.syncSummary.BlockHash {
return fmt.Errorf("attempted to set last summary block to unexpected block hash: (%s != %s)", block.Hash(), client.syncSummary.BlockHash)
}
if block.NumberU64() != client.syncSummary.BlockNumber {
return fmt.Errorf("attempted to set last summary block to unexpected block number: (%d != %d)", block.NumberU64(), client.syncSummary.BlockNumber)
}
// BloomIndexer needs to know that some parts of the chain are not available
// and cannot be indexed. This is done by calling [AddCheckpoint] here.
// Since the indexer uses sections of size [params.BloomBitsBlocks] (= 4096),
// each block is indexed in section number [blockNumber/params.BloomBitsBlocks].
// To allow the indexer to start with the block we just synced to,
// we create a checkpoint for its parent.
// Note: This requires assuming the synced block height is divisible
// by [params.BloomBitsBlocks].
parentHeight := block.NumberU64() - 1
parentHash := block.ParentHash()
client.chain.BloomIndexer().AddCheckpoint(parentHeight/params.BloomBitsBlocks, parentHash)
if err := client.chain.BlockChain().ResetToStateSyncedBlock(block); err != nil {
return err
}
if err := client.updateVMMarkers(); err != nil {
return fmt.Errorf("error updating vm markers, height=%d, hash=%s, err=%w", block.NumberU64(), block.Hash(), err)
}
return client.state.SetLastAcceptedBlock(evmBlock)
}
// updateVMMarkers updates the following markers in the VM's database
// and commits them atomically:
// - updates lastAcceptedKey
// - removes state sync progress markers
func (client *stateSyncerClient) updateVMMarkers() error {
if err := client.acceptedBlockDB.Put(lastAcceptedKey, client.syncSummary.BlockHash[:]); err != nil {
return err
}
if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
return err
}
return client.db.Commit()
}
// Error returns a non-nil error if one occurred during the sync.
func (client *stateSyncerClient) Error() error { return client.stateSyncErr }
-95
View File
@@ -1,95 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"fmt"
"github.com/luxfi/node/database"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/plugin/evm/message"
"github.com/ethereum/go-ethereum/log"
)
type stateSyncServerConfig struct {
Chain *core.BlockChain
// SyncableInterval is the interval at which blocks are eligible to provide syncable block summaries.
SyncableInterval uint64
}
type stateSyncServer struct {
chain *core.BlockChain
syncableInterval uint64
}
type StateSyncServer interface {
GetLastStateSummary(context.Context) (block.StateSummary, error)
GetStateSummary(context.Context, uint64) (block.StateSummary, error)
}
func NewStateSyncServer(config *stateSyncServerConfig) StateSyncServer {
return &stateSyncServer{
chain: config.Chain,
syncableInterval: config.SyncableInterval,
}
}
// stateSummaryAtHeight returns the SyncSummary at [height] if valid and available.
func (server *stateSyncServer) stateSummaryAtHeight(height uint64) (message.SyncSummary, error) {
blk := server.chain.GetBlockByNumber(height)
if blk == nil {
return message.SyncSummary{}, fmt.Errorf("block not found for height (%d)", height)
}
if !server.chain.HasState(blk.Root()) {
return message.SyncSummary{}, fmt.Errorf("block root does not exist for height (%d), root (%s)", height, blk.Root())
}
summary, err := message.NewSyncSummary(blk.Hash(), height, blk.Root())
if err != nil {
return message.SyncSummary{}, fmt.Errorf("failed to construct syncable block at height %d: %w", height, err)
}
return summary, nil
}
// GetLastStateSummary returns the latest state summary.
// State summary is calculated by the block nearest to last accepted
// that is divisible by [syncableInterval]
// If no summary is available, [database.ErrNotFound] must be returned.
func (server *stateSyncServer) GetLastStateSummary(context.Context) (block.StateSummary, error) {
lastHeight := server.chain.LastAcceptedBlock().NumberU64()
lastSyncSummaryNumber := lastHeight - lastHeight%server.syncableInterval
summary, err := server.stateSummaryAtHeight(lastSyncSummaryNumber)
if err != nil {
log.Debug("could not get latest state summary", "err", err)
return nil, database.ErrNotFound
}
log.Debug("Serving syncable block at latest height", "summary", summary)
return summary, nil
}
// GetStateSummary implements StateSyncableVM and returns a summary corresponding
// to the provided [height] if the node can serve state sync data for that key.
// If not, [database.ErrNotFound] must be returned.
func (server *stateSyncServer) GetStateSummary(_ context.Context, height uint64) (block.StateSummary, error) {
summaryBlock := server.chain.GetBlockByNumber(height)
if summaryBlock == nil ||
summaryBlock.NumberU64() > server.chain.LastAcceptedBlock().NumberU64() ||
summaryBlock.NumberU64()%server.syncableInterval != 0 {
return nil, database.ErrNotFound
}
summary, err := server.stateSummaryAtHeight(summaryBlock.NumberU64())
if err != nil {
log.Debug("could not get state summary", "height", height, "err", err)
return nil, database.ErrNotFound
}
log.Debug("Serving syncable block at requested height", "height", height, "summary", summary)
return summary, nil
}
-95
View File
@@ -1,95 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"fmt"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/plugin/evm/message"
"github.com/ethereum/go-ethereum/log"
)
type stateSyncServerConfig struct {
Chain *core.BlockChain
// SyncableInterval is the interval at which blocks are eligible to provide syncable block summaries.
SyncableInterval uint64
}
type stateSyncServer struct {
chain *core.BlockChain
syncableInterval uint64
}
type StateSyncServer interface {
GetLastStateSummary(context.Context) (block.StateSummary, error)
GetStateSummary(context.Context, uint64) (block.StateSummary, error)
}
func NewStateSyncServer(config *stateSyncServerConfig) StateSyncServer {
return &stateSyncServer{
chain: config.Chain,
syncableInterval: config.SyncableInterval,
}
}
// stateSummaryAtHeight returns the SyncSummary at [height] if valid and available.
func (server *stateSyncServer) stateSummaryAtHeight(height uint64) (message.SyncSummary, error) {
blk := server.chain.GetBlockByNumber(height)
if blk == nil {
return message.SyncSummary{}, fmt.Errorf("block not found for height (%d)", height)
}
if !server.chain.HasState(blk.Root()) {
return message.SyncSummary{}, fmt.Errorf("block root does not exist for height (%d), root (%s)", height, blk.Root())
}
summary, err := message.NewSyncSummary(blk.Hash(), height, blk.Root())
if err != nil {
return message.SyncSummary{}, fmt.Errorf("failed to construct syncable block at height %d: %w", height, err)
}
return summary, nil
}
// GetLastStateSummary returns the latest state summary.
// State summary is calculated by the block nearest to last accepted
// that is divisible by [syncableInterval]
// If no summary is available, [database.ErrNotFound] must be returned.
func (server *stateSyncServer) GetLastStateSummary(context.Context) (block.StateSummary, error) {
lastHeight := server.chain.LastAcceptedBlock().NumberU64()
lastSyncSummaryNumber := lastHeight - lastHeight%server.syncableInterval
summary, err := server.stateSummaryAtHeight(lastSyncSummaryNumber)
if err != nil {
log.Debug("could not get latest state summary", "err", err)
return nil, database.ErrNotFound
}
log.Debug("Serving syncable block at latest height", "summary", summary)
return summary, nil
}
// GetStateSummary implements StateSyncableVM and returns a summary corresponding
// to the provided [height] if the node can serve state sync data for that key.
// If not, [database.ErrNotFound] must be returned.
func (server *stateSyncServer) GetStateSummary(_ context.Context, height uint64) (block.StateSummary, error) {
summaryBlock := server.chain.GetBlockByNumber(height)
if summaryBlock == nil ||
summaryBlock.NumberU64() > server.chain.LastAcceptedBlock().NumberU64() ||
summaryBlock.NumberU64()%server.syncableInterval != 0 {
return nil, database.ErrNotFound
}
summary, err := server.stateSummaryAtHeight(summaryBlock.NumberU64())
if err != nil {
log.Debug("could not get state summary", "height", height, "err", err)
return nil, database.ErrNotFound
}
log.Debug("Serving syncable block at requested height", "height", height, "summary", summary)
return summary, nil
}
-599
View File
@@ -1,599 +0,0 @@
// (c) 2021-2022, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"fmt"
"math/big"
"math/rand"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/luxfi/node/database"
"github.com/luxfi/node/database/prefixdb"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus"
"github.com/luxfi/node/consensus/choices"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/node/utils/set"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/luxfi/evm/consensus/dummy"
"github.com/luxfi/evm/constants"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/geth/ethdb"
"github.com/ethereum/go-ethereum/metrics"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/predicate"
statesyncclient "github.com/luxfi/evm/sync/client"
"github.com/luxfi/evm/sync/statesync"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/geth/rlp"
)
func TestSkipStateSync(t *testing.T) {
rand.Seed(1)
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 300, // must be greater than [syncableInterval] to skip sync
syncMode: block.StateSyncSkipped,
}
vmSetup := createSyncServerAndClientVMs(t, test, parentsToGet)
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncFromScratch(t *testing.T) {
rand.Seed(1)
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
}
vmSetup := createSyncServerAndClientVMs(t, test, parentsToGet)
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncFromScratchExceedParent(t *testing.T) {
rand.Seed(1)
numToGen := parentsToGet + uint64(32)
test := syncTest{
syncableInterval: numToGen,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
}
vmSetup := createSyncServerAndClientVMs(t, test, int(numToGen))
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncToggleEnabledToDisabled(t *testing.T) {
rand.Seed(1)
var lock sync.Mutex
reqCount := 0
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
responseIntercept: func(syncerVM *VM, nodeID ids.NodeID, requestID uint32, response []byte) {
lock.Lock()
defer lock.Unlock()
reqCount++
// Fail all requests after number 50 to interrupt the sync
if reqCount > 50 {
if err := syncerVM.AppRequestFailed(context.Background(), nodeID, requestID, commonEng.ErrTimeout); err != nil {
panic(err)
}
cancel := syncerVM.StateSyncClient.(*stateSyncerClient).cancel
if cancel != nil {
cancel()
} else {
t.Fatal("state sync client not populated correctly")
}
} else {
syncerVM.AppResponse(context.Background(), nodeID, requestID, response)
}
},
expectedErr: context.Canceled,
}
vmSetup := createSyncServerAndClientVMs(t, test, parentsToGet)
// Perform sync resulting in early termination.
testSyncerVM(t, vmSetup, test)
test.syncMode = block.StateSyncStatic
test.responseIntercept = nil
test.expectedErr = nil
syncDisabledVM := &VM{}
appSender := &enginetest.Sender{T: t}
appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil }
appSender.SendAppRequestF = func(ctx context.Context, nodeSet set.Set[ids.NodeID], requestID uint32, request []byte) error {
nodeID, hasItem := nodeSet.Pop()
if !hasItem {
t.Fatal("expected nodeSet to contain at least 1 nodeID")
}
go vmSetup.serverVM.AppRequest(ctx, nodeID, requestID, time.Now().Add(1*time.Second), request)
return nil
}
// Reset metrics to allow re-initialization
vmSetup.syncerVM.ctx.Metrics = metrics.NewPrefixGatherer()
stateSyncDisabledConfigJSON := `{"state-sync-enabled":false}`
if err := syncDisabledVM.Initialize(
context.Background(),
vmSetup.syncerVM.ctx,
vmSetup.syncerDB,
[]byte(genesisJSONLatest),
nil,
[]byte(stateSyncDisabledConfigJSON),
vmSetup.syncerVM.toEngine,
[]*commonEng.Fx{},
appSender,
); err != nil {
t.Fatal(err)
}
defer func() {
if err := syncDisabledVM.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
if height := syncDisabledVM.LastAcceptedBlockInternal().Height(); height != 0 {
t.Fatalf("Unexpected last accepted height: %d", height)
}
enabled, err := syncDisabledVM.StateSyncEnabled(context.Background())
assert.NoError(t, err)
assert.False(t, enabled, "sync should be disabled")
// Process the first 10 blocks from the serverVM
for i := uint64(1); i < 10; i++ {
ethBlock := vmSetup.serverVM.blockChain.GetBlockByNumber(i)
if ethBlock == nil {
t.Fatalf("VM Server did not have a block available at height %d", i)
}
b, err := rlp.EncodeToBytes(ethBlock)
if err != nil {
t.Fatal(err)
}
blk, err := syncDisabledVM.ParseBlock(context.Background(), b)
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
}
// Verify the snapshot disk layer matches the last block root
lastRoot := syncDisabledVM.blockChain.CurrentBlock().Root
if err := syncDisabledVM.blockChain.Snapshots().Verify(lastRoot); err != nil {
t.Fatal(err)
}
syncDisabledVM.blockChain.DrainAcceptorQueue()
// Create a new VM from the same database with state sync enabled.
syncReEnabledVM := &VM{}
// Enable state sync in configJSON
configJSON := fmt.Sprintf(
`{"state-sync-enabled":true, "state-sync-min-blocks":%d}`,
test.stateSyncMinBlocks,
)
// Reset metrics to allow re-initialization
vmSetup.syncerVM.ctx.Metrics = metrics.NewPrefixGatherer()
if err := syncReEnabledVM.Initialize(
context.Background(),
vmSetup.syncerVM.ctx,
vmSetup.syncerDB,
[]byte(genesisJSONLatest),
nil,
[]byte(configJSON),
vmSetup.syncerVM.toEngine,
[]*commonEng.Fx{},
appSender,
); err != nil {
t.Fatal(err)
}
// override [serverVM]'s SendAppResponse function to trigger AppResponse on [syncerVM]
vmSetup.serverAppSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
if test.responseIntercept == nil {
go syncReEnabledVM.AppResponse(ctx, nodeID, requestID, response)
} else {
go test.responseIntercept(syncReEnabledVM, nodeID, requestID, response)
}
return nil
}
// connect peer to [syncerVM]
assert.NoError(t, syncReEnabledVM.Connected(
context.Background(),
vmSetup.serverVM.ctx.NodeID,
statesyncclient.StateSyncVersion,
))
enabled, err = syncReEnabledVM.StateSyncEnabled(context.Background())
assert.NoError(t, err)
assert.True(t, enabled, "sync should be enabled")
vmSetup.syncerVM = syncReEnabledVM
testSyncerVM(t, vmSetup, test)
}
func TestVMShutdownWhileSyncing(t *testing.T) {
var (
lock sync.Mutex
vmSetup *syncVMSetup
)
reqCount := 0
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
responseIntercept: func(syncerVM *VM, nodeID ids.NodeID, requestID uint32, response []byte) {
lock.Lock()
defer lock.Unlock()
reqCount++
// Shutdown the VM after 50 requests to interrupt the sync
if reqCount == 50 {
// Note this verifies the VM shutdown does not time out while syncing.
require.NoError(t, vmSetup.shutdownOnceSyncerVM.Shutdown(context.Background()))
} else if reqCount < 50 {
err := syncerVM.AppResponse(context.Background(), nodeID, requestID, response)
require.NoError(t, err)
}
},
expectedErr: context.Canceled,
}
vmSetup = createSyncServerAndClientVMs(t, test, parentsToGet)
// Perform sync resulting in early termination.
testSyncerVM(t, vmSetup, test)
}
func createSyncServerAndClientVMs(t *testing.T, test syncTest, numBlocks int) *syncVMSetup {
require := require.New(t)
// configure [serverVM]
_, serverVM, _, serverAppSender := GenesisVM(t, true, genesisJSONLatest, "", "")
t.Cleanup(func() {
log.Info("Shutting down server VM")
require.NoError(serverVM.Shutdown(context.Background()))
})
generateAndAcceptBlocks(t, serverVM, numBlocks, func(i int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
if err != nil {
t.Fatal(err)
}
gen.AppendExtra(b)
tx := types.NewTransaction(gen.TxNonce(testEthAddrs[0]), testEthAddrs[1], common.Big1, ethparams.TxGas, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(serverVM.chainConfig.ChainID), testKeys[0])
require.NoError(err)
gen.AddTx(signedTx)
}, nil)
// make some accounts
trieDB := triedb.NewDatabase(serverVM.chaindb, nil)
root, accounts := statesync.FillAccountsWithOverlappingStorage(t, trieDB, types.EmptyRootHash, 1000, 16)
// patch serverVM's lastAcceptedBlock to have the new root
// and update the vm's state so the trie with accounts will
// be returned by StateSyncGetLastSummary
lastAccepted := serverVM.blockChain.LastAcceptedBlock()
patchedBlock := patchBlock(lastAccepted, root, serverVM.chaindb)
blockBytes, err := rlp.EncodeToBytes(patchedBlock)
require.NoError(err)
internalBlock, err := serverVM.parseBlock(context.Background(), blockBytes)
require.NoError(err)
require.NoError(serverVM.State.SetLastAcceptedBlock(internalBlock))
// patch syncableInterval for test
serverVM.StateSyncServer.(*stateSyncServer).syncableInterval = test.syncableInterval
// initialise [syncerVM] with blank genesis state
stateSyncEnabledJSON := fmt.Sprintf(`{"state-sync-enabled":true, "state-sync-min-blocks": %d, "tx-lookup-limit": %d}`, test.stateSyncMinBlocks, 4)
syncerEngineChan, syncerVM, syncerDB, syncerAppSender := GenesisVM(t, false, genesisJSONLatest, stateSyncEnabledJSON, "")
shutdownOnceSyncerVM := &shutdownOnceVM{VM: syncerVM}
t.Cleanup(func() {
require.NoError(shutdownOnceSyncerVM.Shutdown(context.Background()))
})
require.NoError(syncerVM.SetState(context.Background(), consensus.StateSyncing))
enabled, err := syncerVM.StateSyncEnabled(context.Background())
require.NoError(err)
require.True(enabled)
// override [serverVM]'s SendAppResponse function to trigger AppResponse on [syncerVM]
serverAppSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
if test.responseIntercept == nil {
go syncerVM.AppResponse(ctx, nodeID, requestID, response)
} else {
go test.responseIntercept(syncerVM, nodeID, requestID, response)
}
return nil
}
// connect peer to [syncerVM]
require.NoError(
syncerVM.Connected(
context.Background(),
serverVM.ctx.NodeID,
statesyncclient.StateSyncVersion,
),
)
// override [syncerVM]'s SendAppRequest function to trigger AppRequest on [serverVM]
syncerAppSender.SendAppRequestF = func(ctx context.Context, nodeSet set.Set[ids.NodeID], requestID uint32, request []byte) error {
nodeID, hasItem := nodeSet.Pop()
require.True(hasItem, "expected nodeSet to contain at least 1 nodeID")
err := serverVM.AppRequest(ctx, nodeID, requestID, time.Now().Add(1*time.Second), request)
require.NoError(err)
return nil
}
return &syncVMSetup{
serverVM: serverVM,
serverAppSender: serverAppSender,
fundedAccounts: accounts,
syncerVM: syncerVM,
syncerDB: syncerDB,
syncerEngineChan: syncerEngineChan,
shutdownOnceSyncerVM: shutdownOnceSyncerVM,
}
}
// syncVMSetup contains the required set up for a client VM to perform state sync
// off of a server VM.
type syncVMSetup struct {
serverVM *VM
serverAppSender *enginetest.Sender
fundedAccounts map[*testutils.Key]*types.StateAccount
syncerVM *VM
syncerDB luxdatabase.Database
syncerEngineChan <-chan commonEng.Message
shutdownOnceSyncerVM *shutdownOnceVM
}
type shutdownOnceVM struct {
*VM
shutdownOnce sync.Once
}
func (vm *shutdownOnceVM) Shutdown(ctx context.Context) error {
var err error
vm.shutdownOnce.Do(func() { err = vm.VM.Shutdown(ctx) })
return err
}
// syncTest contains both the actual VMs as well as the parameters with the expected output.
type syncTest struct {
responseIntercept func(vm *VM, nodeID ids.NodeID, requestID uint32, response []byte)
stateSyncMinBlocks uint64
syncableInterval uint64
syncMode block.StateSyncMode
expectedErr error
}
func testSyncerVM(t *testing.T, vmSetup *syncVMSetup, test syncTest) {
t.Helper()
var (
require = require.New(t)
serverVM = vmSetup.serverVM
fundedAccounts = vmSetup.fundedAccounts
syncerVM = vmSetup.syncerVM
syncerEngineChan = vmSetup.syncerEngineChan
)
// get last summary and test related methods
summary, err := serverVM.GetLastStateSummary(context.Background())
require.NoError(err, "error getting state sync last summary")
parsedSummary, err := syncerVM.ParseStateSummary(context.Background(), summary.Bytes())
require.NoError(err, "error parsing state summary")
retrievedSummary, err := serverVM.GetStateSummary(context.Background(), parsedSummary.Height())
require.NoError(err, "error getting state sync summary at height")
require.Equal(summary, retrievedSummary)
syncMode, err := parsedSummary.Accept(context.Background())
require.NoError(err, "error accepting state summary")
require.Equal(test.syncMode, syncMode)
if syncMode == block.StateSyncSkipped {
return
}
msg := <-syncerEngineChan
require.Equal(commonEng.StateSyncDone, msg)
// If the test is expected to error, assert the correct error is returned and finish the test.
err = syncerVM.StateSyncClient.Error()
if test.expectedErr != nil {
require.ErrorIs(err, test.expectedErr)
// Note we re-open the database here to avoid a closed error when the test is for a shutdown VM.
chaindb := database.WrapDatabase(prefixdb.NewNested(ethDBPrefix, syncerVM.versiondb))
assertSyncPerformedHeights(t, chaindb, map[uint64]struct{}{})
return
}
require.NoError(err, "state sync failed")
// set [syncerVM] to bootstrapping and verify the last accepted block has been updated correctly
// and that we can bootstrap and process some blocks.
require.NoError(syncerVM.SetState(context.Background(), consensus.Bootstrapping))
require.Equal(serverVM.LastAcceptedBlock().Height(), syncerVM.LastAcceptedBlock().Height(), "block height mismatch between syncer and server")
require.Equal(serverVM.LastAcceptedBlock().ID(), syncerVM.LastAcceptedBlock().ID(), "blockID mismatch between syncer and server")
require.True(syncerVM.blockChain.HasState(syncerVM.blockChain.LastAcceptedBlock().Root()), "unavailable state for last accepted block")
assertSyncPerformedHeights(t, syncerVM.chaindb, map[uint64]struct{}{retrievedSummary.Height(): {}})
lastNumber := syncerVM.blockChain.LastAcceptedBlock().NumberU64()
// check the last block is indexed
lastSyncedBlock := rawdb.ReadBlock(syncerVM.chaindb, rawdb.ReadCanonicalHash(syncerVM.chaindb, lastNumber), lastNumber)
for _, tx := range lastSyncedBlock.Transactions() {
index := rawdb.ReadTxLookupEntry(syncerVM.chaindb, tx.Hash())
require.NotNilf(index, "Miss transaction indices, number %d hash %s", lastNumber, tx.Hash().Hex())
}
// tail should be the last block synced
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := lastSyncedBlock.NumberU64()
core.CheckTxIndices(t, &tail, tail, syncerVM.chaindb, true)
}
blocksToBuild := 10
txsPerBlock := 10
toAddress := testEthAddrs[1] // arbitrary choice
generateAndAcceptBlocks(t, syncerVM, blocksToBuild, func(_ int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
if err != nil {
t.Fatal(err)
}
gen.AppendExtra(b)
i := 0
for k := range fundedAccounts {
tx := types.NewTransaction(gen.TxNonce(k.Address), toAddress, big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(serverVM.chainConfig.ChainID), k.PrivateKey)
require.NoError(err)
gen.AddTx(signedTx)
i++
if i >= txsPerBlock {
break
}
}
},
func(block *types.Block) {
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := block.NumberU64() - syncerVM.ethConfig.TransactionHistory + 1
// tail should be the minimum last synced block, since we skipped it to the last block
if tail < lastSyncedBlock.NumberU64() {
tail = lastSyncedBlock.NumberU64()
}
core.CheckTxIndices(t, &tail, block.NumberU64(), syncerVM.chaindb, true)
}
},
)
// check we can transition to [NormalOp] state and continue to process blocks.
require.NoError(syncerVM.SetState(context.Background(), consensus.NormalOp))
require.True(syncerVM.bootstrapped.Get())
// Generate blocks after we have entered normal consensus as well
generateAndAcceptBlocks(t, syncerVM, blocksToBuild, func(_ int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
require.NoError(err)
gen.AppendExtra(b)
i := 0
for k := range fundedAccounts {
tx := types.NewTransaction(gen.TxNonce(k.Address), toAddress, big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(serverVM.chainConfig.ChainID), k.PrivateKey)
require.NoError(err)
gen.AddTx(signedTx)
i++
if i >= txsPerBlock {
break
}
}
},
func(block *types.Block) {
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := block.NumberU64() - syncerVM.ethConfig.TransactionHistory + 1
// tail should be the minimum last synced block, since we skipped it to the last block
if tail < lastSyncedBlock.NumberU64() {
tail = lastSyncedBlock.NumberU64()
}
core.CheckTxIndices(t, &tail, block.NumberU64(), syncerVM.chaindb, true)
}
},
)
}
// patchBlock returns a copy of [blk] with [root] and updates [db] to
// include the new block as canonical for [blk]'s height.
// This breaks the digestibility of the chain since after this call
// [blk] does not necessarily define a state transition from its parent
// state to the new state root.
func patchBlock(blk *types.Block, root common.Hash, db ethdb.Database) *types.Block {
header := blk.Header()
header.Root = root
receipts := rawdb.ReadRawReceipts(db, blk.Hash(), blk.NumberU64())
newBlk := types.NewBlock(
header, blk.Transactions(), blk.Uncles(), receipts, trie.NewStackTrie(nil),
)
rawdb.WriteBlock(db, newBlk)
rawdb.WriteCanonicalHash(db, newBlk.Hash(), newBlk.NumberU64())
return newBlk
}
// generateAndAcceptBlocks uses [core.GenerateChain] to generate blocks, then
// calls Verify and Accept on each generated block
// TODO: consider using this helper function in vm_test.go and elsewhere in this package to clean up tests
func generateAndAcceptBlocks(t *testing.T, vm *VM, numBlocks int, gen func(int, *core.BlockGen), accepted func(*types.Block)) {
t.Helper()
// acceptExternalBlock defines a function to parse, verify, and accept a block once it has been
// generated by GenerateChain
acceptExternalBlock := func(block *types.Block) {
bytes, err := rlp.EncodeToBytes(block)
if err != nil {
t.Fatal(err)
}
vmBlock, err := vm.ParseBlock(context.Background(), bytes)
if err != nil {
t.Fatal(err)
}
if err := vmBlock.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vmBlock.Accept(context.Background()); err != nil {
t.Fatal(err)
}
if accepted != nil {
accepted(block)
}
}
_, _, err := core.GenerateChain(
vm.chainConfig,
vm.blockChain.LastAcceptedBlock(),
dummy.NewETHFaker(),
vm.chaindb,
numBlocks,
10,
func(i int, g *core.BlockGen) {
g.SetOnBlockGenerated(acceptExternalBlock)
g.SetCoinbase(constants.BlackholeAddr) // necessary for syntactic validation of the block
gen(i, g)
},
)
if err != nil {
t.Fatal(err)
}
vm.blockChain.DrainAcceptorQueue()
}
// assertSyncPerformedHeights iterates over all heights the VM has synced to and
// verifies it matches [expected].
func assertSyncPerformedHeights(t *testing.T, db ethdb.Iteratee, expected map[uint64]struct{}) {
it := customrawdb.NewSyncPerformedIterator(db)
defer it.Release()
found := make(map[uint64]struct{}, len(expected))
for it.Next() {
found[customrawdb.UnpackSyncPerformedKey(it.Key())] = struct{}{}
}
require.NoError(t, it.Error())
require.Equal(t, expected, found)
}
-252
View File
@@ -1,252 +0,0 @@
// Copyright (C) 2019-2023, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"encoding/binary"
"math/big"
"sync"
"testing"
"time"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/network/p2p"
"github.com/luxfi/node/network/p2p/gossip"
"github.com/luxfi/node/proto/pb/sdk"
"github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/utils"
"github.com/luxfi/node/utils/logging"
"github.com/luxfi/node/utils/set"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/ethereum/go-ethereum/core/types"
)
func TestEthTxGossip(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensusCtx := utils.TestConsensusContext()
validatorState := utils.NewTestValidatorState()
consensusCtx.ValidatorState = validatorState
responseSender := &enginetest.SenderStub{
SentAppResponse: make(chan []byte, 1),
}
vm := &VM{
p2pSender: responseSender,
}
require.NoError(vm.Initialize(
ctx,
consensusCtx,
memdb.New(),
[]byte(genesisJSONLatest),
nil,
nil,
make(chan common.Message),
nil,
&enginetest.Sender{},
))
require.NoError(vm.SetState(ctx, consensus.NormalOp))
defer func() {
require.NoError(vm.Shutdown(ctx))
}()
// sender for the peer requesting gossip from [vm]
peerSender := &enginetest.SenderStub{
SentAppRequest: make(chan []byte, 1),
}
network, err := p2p.NewNetwork(logging.NoLog{}, peerSender, prometheus.NewRegistry(), "")
require.NoError(err)
client := network.NewClient(p2p.TxGossipHandlerID)
// we only accept gossip requests from validators
requestingNodeID := ids.GenerateTestNodeID()
require.NoError(vm.Network.Connected(ctx, requestingNodeID, nil))
validatorState.GetCurrentHeightF = func(context.Context) (uint64, error) {
return 0, nil
}
validatorState.GetValidatorSetF = func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return map[ids.NodeID]*validators.GetValidatorOutput{
requestingNodeID: {
NodeID: requestingNodeID,
Weight: 1,
},
}, nil
}
// Ask the VM for any new transactions. We should get nothing at first.
emptyBloomFilter, err := gossip.NewBloomFilter(prometheus.NewRegistry(), "", txGossipBloomMinTargetElements, txGossipBloomTargetFalsePositiveRate, txGossipBloomResetFalsePositiveRate)
require.NoError(err)
emptyBloomFilterBytes, _ := emptyBloomFilter.Marshal()
request := &sdk.PullGossipRequest{
Filter: emptyBloomFilterBytes,
Salt: agoUtils.RandomBytes(32),
}
requestBytes, err := proto.Marshal(request)
require.NoError(err)
wg := &sync.WaitGroup{}
wg.Add(1)
onResponse := func(_ context.Context, nodeID ids.NodeID, responseBytes []byte, err error) {
require.NoError(err)
response := &sdk.PullGossipResponse{}
require.NoError(proto.Unmarshal(responseBytes, response))
require.Empty(response.Gossip)
wg.Done()
}
require.NoError(client.AppRequest(ctx, set.Of(vm.ctx.NodeID), requestBytes, onResponse))
require.NoError(vm.AppRequest(ctx, requestingNodeID, 1, time.Time{}, <-peerSender.SentAppRequest))
require.NoError(network.AppResponse(ctx, consensusCtx.NodeID, 1, <-responseSender.SentAppResponse))
wg.Wait()
// Issue a tx to the VM
address := testEthAddrs[0]
key := testKeys[0]
tx := types.NewTransaction(0, address, big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), key)
require.NoError(err)
errs := vm.txPool.Add([]*types.Transaction{signedTx}, true, true)
require.Len(errs, 1)
require.Nil(errs[0])
// wait so we aren't throttled by the vm
time.Sleep(5 * time.Second)
marshaller := GossipEthTxMarshaller{}
// Ask the VM for new transactions. We should get the newly issued tx.
wg.Add(1)
onResponse = func(_ context.Context, nodeID ids.NodeID, responseBytes []byte, err error) {
require.NoError(err)
response := &sdk.PullGossipResponse{}
require.NoError(proto.Unmarshal(responseBytes, response))
require.Len(response.Gossip, 1)
gotTx, err := marshaller.UnmarshalGossip(response.Gossip[0])
require.NoError(err)
require.Equal(signedTx.Hash(), gotTx.Tx.Hash())
wg.Done()
}
require.NoError(client.AppRequest(ctx, set.Of(vm.ctx.NodeID), requestBytes, onResponse))
require.NoError(vm.AppRequest(ctx, requestingNodeID, 3, time.Time{}, <-peerSender.SentAppRequest))
require.NoError(network.AppResponse(ctx, consensusCtx.NodeID, 3, <-responseSender.SentAppResponse))
wg.Wait()
}
func TestEthTxPushGossipOutbound(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensusCtx := utils.TestConsensusContext()
sender := &enginetest.SenderStub{
SentAppGossip: make(chan []byte, 1),
}
vm := &VM{
ethTxPullGossiper: gossip.NoOpGossiper{},
}
require.NoError(vm.Initialize(
ctx,
consensusCtx,
memdb.New(),
[]byte(genesisJSONLatest),
nil,
nil,
make(chan common.Message),
nil,
sender,
))
require.NoError(vm.SetState(ctx, consensus.NormalOp))
defer func() {
require.NoError(vm.Shutdown(ctx))
}()
address := testEthAddrs[0]
key := testKeys[0]
tx := types.NewTransaction(0, address, big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), key)
require.NoError(err)
// issue a tx
require.NoError(vm.txPool.Add([]*types.Transaction{signedTx}, true, true)[0])
vm.ethTxPushGossiper.Get().Add(&GossipEthTx{signedTx})
sent := <-sender.SentAppGossip
got := &sdk.PushGossip{}
// we should get a message that has the protocol prefix and the gossip
// message
require.Equal(byte(p2p.TxGossipHandlerID), sent[0])
require.NoError(proto.Unmarshal(sent[1:], got))
marshaller := GossipEthTxMarshaller{}
require.Len(got.Gossip, 1)
gossipedTx, err := marshaller.UnmarshalGossip(got.Gossip[0])
require.NoError(err)
require.Equal(ids.ID(signedTx.Hash()), gossipedTx.GossipID())
}
// Tests that a gossiped tx is added to the mempool and forwarded
func TestEthTxPushGossipInbound(t *testing.T) {
require := require.New(t)
ctx := context.Background()
consensusCtx := utils.TestConsensusContext()
sender := &enginetest.Sender{}
vm := &VM{
ethTxPullGossiper: gossip.NoOpGossiper{},
}
require.NoError(vm.Initialize(
ctx,
consensusCtx,
memdb.New(),
[]byte(genesisJSONLatest),
nil,
nil,
make(chan common.Message),
nil,
sender,
))
require.NoError(vm.SetState(ctx, consensus.NormalOp))
defer func() {
require.NoError(vm.Shutdown(ctx))
}()
address := testEthAddrs[0]
key := testKeys[0]
tx := types.NewTransaction(0, address, big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), key)
require.NoError(err)
marshaller := GossipEthTxMarshaller{}
gossipedTx := &GossipEthTx{
Tx: signedTx,
}
gossipedTxBytes, err := marshaller.MarshalGossip(gossipedTx)
require.NoError(err)
inboundGossip := &sdk.PushGossip{
Gossip: [][]byte{gossipedTxBytes},
}
inboundGossipBytes, err := proto.Marshal(inboundGossip)
require.NoError(err)
inboundGossipMsg := append(binary.AppendUvarint(nil, p2p.TxGossipHandlerID), inboundGossipBytes...)
require.NoError(vm.AppGossip(ctx, ids.EmptyNodeID, inboundGossipMsg))
require.True(vm.txPool.Has(signedTx.Hash()))
}
+3 -3
View File
@@ -1,9 +1,9 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// ACP176 implements the fee logic specified here:
// https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates/README.md
package acp176
// LIP176 implements the fee logic specified here:
// https://github.com/lux-foundation/LIPs/blob/main/LIPs/176-dynamic-evm-gas-limit-and-price-discovery-updates/README.md
package lip176
import (
"encoding/binary"
+1 -1
View File
@@ -1,7 +1,7 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package acp176
package lip176
import (
"math"
+2 -2
View File
@@ -6,10 +6,10 @@ package etna
import "github.com/luxfi/evm/utils"
// MinBaseFee is the minimum base fee specified in ACP-125 that is allowed after
// MinBaseFee is the minimum base fee specified in LIP-125 that is allowed after
// the Etna upgrade.
//
// See: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/125-basefee-reduction
// See: https://github.com/lux-foundation/LIPs/tree/main/LIPs/125-basefee-reduction
//
// This value modifies the previously used `ap4.MinBaseFee`.
const MinBaseFee = utils.GWei
@@ -1,95 +0,0 @@
// (c) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// subnetevm defines the dynamic fee window used after subnetevm upgrade.
package subnetevm
import (
"encoding/binary"
"errors"
"fmt"
"math"
"github.com/luxfi/node/utils/wrappers"
safemath "github.com/ethereum/go-ethereum/common/math"
)
const (
// WindowLen is the number of seconds of gas consumption to track.
WindowLen = 10
// WindowSize is the number of bytes that are used to encode the window.
WindowSize = wrappers.LongLen * WindowLen
)
var ErrWindowInsufficientLength = errors.New("insufficient length for window")
// Window is a window of the last [WindowLen] seconds of gas usage.
//
// Index 0 is the oldest entry, and [WindowLen]-1 is the current entry.
type Window [WindowLen]uint64
func ParseWindow(bytes []byte) (Window, error) {
if len(bytes) < WindowSize {
return Window{}, fmt.Errorf("%w: expected at least %d bytes but got %d bytes",
ErrWindowInsufficientLength,
WindowSize,
len(bytes),
)
}
var window Window
for i := range window {
offset := i * wrappers.LongLen
window[i] = binary.BigEndian.Uint64(bytes[offset:])
}
return window, nil
}
// Add adds the amounts to the most recent entry in the window.
//
// If the most recent entry overflows, it is set to [math.MaxUint64].
func (w *Window) Add(amounts ...uint64) {
const lastIndex uint = WindowLen - 1
w[lastIndex] = add(w[lastIndex], amounts...)
}
// Shift removes the oldest n entries from the window and adds n new empty
// entries.
func (w *Window) Shift(n uint64) {
if n >= WindowLen {
*w = Window{}
return
}
var newWindow Window
copy(newWindow[:], w[n:])
*w = newWindow
}
// Sum returns the sum of all the entries in the window.
//
// If the sum overflows, [math.MaxUint64] is returned.
func (w *Window) Sum() uint64 {
return add(0, w[:]...)
}
func (w *Window) Bytes() []byte {
bytes := make([]byte, WindowSize)
for i, v := range w {
offset := i * wrappers.LongLen
binary.BigEndian.PutUint64(bytes[offset:], v)
}
return bytes
}
func add(sum uint64, values ...uint64) uint64 {
var overflow bool
for _, v := range values {
sum, overflow = safemath.SafeAdd(sum, v)
if overflow {
return math.MaxUint64
}
}
return sum
}
@@ -1,34 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"context"
"sync"
"time"
"github.com/luxfi/node/ids"
luxuptime "github.com/luxfi/node/consensus/uptime"
stateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
type ValidatorReader interface {
// GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID
// and the last updated time.
// GetValidatorAndUptime holds the VM lock while performing the operation and can be called concurrently.
GetValidatorAndUptime(validationID ids.ID) (stateinterfaces.Validator, time.Duration, time.Time, error)
}
type Manager interface {
stateinterfaces.StateReader
luxuptime.Manager
// Initialize initializes the validator manager
// by syncing the validator state with the current validator set
// and starting the uptime tracking.
Initialize(ctx context.Context) error
// Shutdown stops the uptime tracking and writes the validator state to the database.
Shutdown() error
// DispatchSync starts the sync process
DispatchSync(ctx context.Context, lock sync.Locker)
}
@@ -1,34 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"context"
"sync"
"time"
"github.com/luxfi/evm/interfaces"
luxuptime "github.com/luxfi/node/consensus/uptime"
stateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
type ValidatorReader interface {
// GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID
// and the last updated time.
// GetValidatorAndUptime holds the VM lock while performing the operation and can be called concurrently.
GetValidatorAndUptime(validationID interfaces.ID) (stateinterfaces.Validator, time.Duration, time.Time, error)
}
type Manager interface {
stateinterfaces.StateReader
luxuptime.Manager
// Initialize initializes the validator manager
// by syncing the validator state with the current validator set
// and starting the uptime tracking.
Initialize(ctx context.Context) error
// Shutdown stops the uptime tracking and writes the validator state to the database.
Shutdown() error
// DispatchSync starts the sync process
DispatchSync(ctx context.Context, lock sync.Locker)
}
@@ -1,53 +0,0 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package validators
import (
"fmt"
"time"
"github.com/luxfi/node/ids"
"github.com/luxfi/evm/plugin/evm/validators/interfaces"
stateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
type RLocker interface {
RLock()
RUnlock()
}
type lockedReader struct {
manager interfaces.Manager
lock RLocker
}
func NewLockedValidatorReader(
manager interfaces.Manager,
lock RLocker,
) interfaces.ValidatorReader {
return &lockedReader{
lock: lock,
manager: manager,
}
}
// GetValidatorAndUptime returns the calculated uptime of the validator specified by validationID
// and the last updated time.
// GetValidatorAndUptime holds the lock while performing the operation and can be called concurrently.
func (l *lockedReader) GetValidatorAndUptime(validationID ids.ID) (stateinterfaces.Validator, time.Duration, time.Time, error) {
l.lock.RLock()
defer l.lock.RUnlock()
vdr, err := l.manager.GetValidator(validationID)
if err != nil {
return stateinterfaces.Validator{}, 0, time.Time{}, fmt.Errorf("failed to get validator: %w", err)
}
uptime, lastUpdated, err := l.manager.CalculateUptime(vdr.NodeID)
if err != nil {
return stateinterfaces.Validator{}, 0, time.Time{}, fmt.Errorf("failed to get uptime: %w", err)
}
return vdr, uptime, lastUpdated, nil
}
-170
View File
@@ -1,170 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package validators
import (
"context"
"fmt"
"sync"
"time"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
luxuptime "github.com/luxfi/node/consensus/uptime"
luxvalidators "github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/utils/timer/mockable"
validators "github.com/luxfi/evm/plugin/evm/validators/state"
stateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
"github.com/luxfi/evm/plugin/evm/validators/uptime"
uptimeinterfaces "github.com/luxfi/evm/plugin/evm/validators/uptime/interfaces"
"github.com/luxfi/evm/log"
)
const (
SyncFrequency = 1 * time.Minute
)
type manager struct {
chainCtx *interfaces.ChainContext
stateinterfaces.State
uptimeinterfaces.PausableManager
}
// NewManager returns a new validator manager
// that manages the validator state and the uptime manager.
// Manager is not thread safe and should be used with the VM locked.
func NewManager(
ctx *interfaces.ChainContext,
db interfaces.Database,
clock *mockable.Clock,
) (*manager, error) {
validatorState, err := validators.NewState(db)
if err != nil {
return nil, fmt.Errorf("failed to initialize validator state: %w", err)
}
// Initialize uptime manager
uptimeManager := uptime.NewPausableManager(luxuptime.NewManager(validatorState, clock))
validatorState.RegisterListener(uptimeManager)
return &manager{
chainCtx: ctx,
State: validatorState,
PausableManager: uptimeManager,
}, nil
}
// Initialize initializes the validator manager
// by syncing the validator state with the current validator set
// and starting the uptime tracking.
func (m *manager) Initialize(ctx context.Context) error {
// sync validators first
if err := m.sync(ctx); err != nil {
return fmt.Errorf("failed to update validators: %w", err)
}
vdrIDs := m.GetNodeIDs().List()
// Then start tracking with updated validators
// StartTracking initializes the uptime tracking with the known validators
// and update their uptime to account for the time we were being offline.
if err := m.StartTracking(vdrIDs); err != nil {
return fmt.Errorf("failed to start tracking uptime: %w", err)
}
return nil
}
// Shutdown stops the uptime tracking and writes the validator state to the database.
func (m *manager) Shutdown() error {
vdrIDs := m.GetNodeIDs().List()
if err := m.StopTracking(vdrIDs); err != nil {
return fmt.Errorf("failed to stop tracking uptime: %w", err)
}
if err := m.WriteState(); err != nil {
return fmt.Errorf("failed to write validator: %w", err)
}
return nil
}
// DispatchSync starts the sync process
// DispatchSync holds the given lock while performing the sync.
func (m *manager) DispatchSync(ctx context.Context, lock sync.Locker) {
ticker := time.NewTicker(SyncFrequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
lock.Lock()
if err := m.sync(ctx); err != nil {
log.Error("failed to sync validators", "error", err)
}
lock.Unlock()
case <-ctx.Done():
return
}
}
}
// sync synchronizes the validator state with the current validator set
// and writes the state to the database.
// sync is not safe to call concurrently and should be called with the VM locked.
func (m *manager) sync(ctx context.Context) error {
now := time.Now()
log.Debug("performing validator sync")
// get current validator set
currentValidatorSet, _, err := m.chainCtx.ValidatorState.GetCurrentValidatorSet(ctx, m.chainCtx.SubnetID)
if err != nil {
return fmt.Errorf("failed to get current validator set: %w", err)
}
// load the current validator set into the validator state
if err := loadValidators(m.State, currentValidatorSet); err != nil {
return fmt.Errorf("failed to load current validators: %w", err)
}
// write validators to the database
if err := m.State.WriteState(); err != nil {
return fmt.Errorf("failed to write validator state: %w", err)
}
// TODO: add metrics
log.Debug("validator sync complete", "duration", time.Since(now))
return nil
}
// loadValidators loads the [validators] into the validator state [validatorState]
func loadValidators(validatorState stateinterfaces.State, newValidators map[interfaces.ID]*luxvalidators.GetCurrentValidatorOutput) error {
currentValidationIDs := validatorState.GetValidationIDs()
// first check if we need to delete any existing validators
for vID := range currentValidationIDs {
// if the validator is not in the new set of validators
// delete the validator
if _, exists := newValidators[vID]; !exists {
validatorState.DeleteValidator(vID)
}
}
// then load the new validators
for newVID, newVdr := range newValidators {
currentVdr := stateinterfaces.Validator{
ValidationID: newVID,
NodeID: newVdr.NodeID,
Weight: newVdr.Weight,
StartTimestamp: newVdr.StartTime,
IsActive: newVdr.IsActive,
IsL1Validator: newVdr.IsL1Validator,
}
if currentValidationIDs.Contains(newVID) {
if err := validatorState.UpdateValidator(currentVdr); err != nil {
return err
}
} else {
if err := validatorState.AddValidator(currentVdr); err != nil {
return err
}
}
}
return nil
}
-170
View File
@@ -1,170 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package validators
import (
"context"
"fmt"
"sync"
"time"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
luxuptime "github.com/luxfi/node/consensus/uptime"
luxvalidators "github.com/luxfi/node/consensus/validators"
"github.com/luxfi/evm/interfaces"
validators "github.com/luxfi/evm/plugin/evm/validators/state"
stateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
"github.com/luxfi/evm/plugin/evm/validators/uptime"
uptimeinterfaces "github.com/luxfi/evm/plugin/evm/validators/uptime/interfaces"
"github.com/luxfi/evm/log"
)
const (
SyncFrequency = 1 * time.Minute
)
type manager struct {
chainCtx *interfaces.ChainContext
stateinterfaces.State
uptimeinterfaces.PausableManager
}
// NewManager returns a new validator manager
// that manages the validator state and the uptime manager.
// Manager is not thread safe and should be used with the VM locked.
func NewManager(
ctx *interfaces.ChainContext,
db interfaces.Database,
clock *interfaces.MockableTimer,
) (*manager, error) {
validatorState, err := validators.NewState(db)
if err != nil {
return nil, fmt.Errorf("failed to initialize validator state: %w", err)
}
// Initialize uptime manager
uptimeManager := uptime.NewPausableManager(luxuptime.NewManager(validatorState, clock))
validatorState.RegisterListener(uptimeManager)
return &manager{
chainCtx: ctx,
State: validatorState,
PausableManager: uptimeManager,
}, nil
}
// Initialize initializes the validator manager
// by syncing the validator state with the current validator set
// and starting the uptime tracking.
func (m *manager) Initialize(ctx context.Context) error {
// sync validators first
if err := m.sync(ctx); err != nil {
return fmt.Errorf("failed to update validators: %w", err)
}
vdrIDs := m.GetNodeIDs().List()
// Then start tracking with updated validators
// StartTracking initializes the uptime tracking with the known validators
// and update their uptime to account for the time we were being offline.
if err := m.StartTracking(vdrIDs); err != nil {
return fmt.Errorf("failed to start tracking uptime: %w", err)
}
return nil
}
// Shutdown stops the uptime tracking and writes the validator state to the database.
func (m *manager) Shutdown() error {
vdrIDs := m.GetNodeIDs().List()
if err := m.StopTracking(vdrIDs); err != nil {
return fmt.Errorf("failed to stop tracking uptime: %w", err)
}
if err := m.WriteState(); err != nil {
return fmt.Errorf("failed to write validator: %w", err)
}
return nil
}
// DispatchSync starts the sync process
// DispatchSync holds the given lock while performing the sync.
func (m *manager) DispatchSync(ctx context.Context, lock sync.Locker) {
ticker := time.NewTicker(SyncFrequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
lock.Lock()
if err := m.sync(ctx); err != nil {
log.Error("failed to sync validators", "error", err)
}
lock.Unlock()
case <-ctx.Done():
return
}
}
}
// sync synchronizes the validator state with the current validator set
// and writes the state to the database.
// sync is not safe to call concurrently and should be called with the VM locked.
func (m *manager) sync(ctx context.Context) error {
now := time.Now()
log.Debug("performing validator sync")
// get current validator set
currentValidatorSet, _, err := m.chainCtx.ValidatorState.GetCurrentValidatorSet(ctx, m.chainCtx.SubnetID)
if err != nil {
return fmt.Errorf("failed to get current validator set: %w", err)
}
// load the current validator set into the validator state
if err := loadValidators(m.State, currentValidatorSet); err != nil {
return fmt.Errorf("failed to load current validators: %w", err)
}
// write validators to the database
if err := m.State.WriteState(); err != nil {
return fmt.Errorf("failed to write validator state: %w", err)
}
// TODO: add metrics
log.Debug("validator sync complete", "duration", time.Since(now))
return nil
}
// loadValidators loads the [validators] into the validator state [validatorState]
func loadValidators(validatorState stateinterfaces.State, newValidators map[interfaces.ID]*luxvalidators.GetCurrentValidatorOutput) error {
currentValidationIDs := validatorState.GetValidationIDs()
// first check if we need to delete any existing validators
for vID := range currentValidationIDs {
// if the validator is not in the new set of validators
// delete the validator
if _, exists := newValidators[vID]; !exists {
validatorState.DeleteValidator(vID)
}
}
// then load the new validators
for newVID, newVdr := range newValidators {
currentVdr := stateinterfaces.Validator{
ValidationID: newVID,
NodeID: newVdr.NodeID,
Weight: newVdr.Weight,
StartTimestamp: newVdr.StartTime,
IsActive: newVdr.IsActive,
IsL1Validator: newVdr.IsL1Validator,
}
if currentValidationIDs.Contains(newVID) {
if err := validatorState.UpdateValidator(currentVdr); err != nil {
return err
}
} else {
if err := validatorState.AddValidator(currentVdr); err != nil {
return err
}
}
}
return nil
}
-221
View File
@@ -1,221 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package validators
import (
"testing"
"github.com/luxfi/node/database/memdb"
"github.com/luxfi/node/ids"
"github.com/luxfi/evm/plugin/evm/validators/state"
"github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
avagovalidators "github.com/luxfi/node/consensus/validators"
)
func TestLoadNewValidators(t *testing.T) {
testNodeIDs := []ids.NodeID{
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
}
testValidationIDs := []ids.ID{
ids.GenerateTestID(),
ids.GenerateTestID(),
ids.GenerateTestID(),
}
tests := []struct {
name string
initialValidators map[ids.ID]*avagovalidators.GetCurrentValidatorOutput
newValidators map[ids.ID]*avagovalidators.GetCurrentValidatorOutput
registerMockListenerCalls func(*interfaces.MockStateCallbackListener)
expectedLoadErr error
}{
{
name: "before empty/after empty",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{},
registerMockListenerCalls: func(*interfaces.MockStateCallbackListener) {},
},
{
name: "before empty/after one",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
},
},
{
name: "before one/after empty",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{},
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
// initial validator will trigger first
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
// then it will be removed
mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1)
},
},
{
name: "no change",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
},
},
{
name: "status and weight change and new one",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
Weight: 1,
},
},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: false,
StartTime: 0,
Weight: 2,
},
testValidationIDs[1]: {
NodeID: testNodeIDs[1],
IsActive: true,
StartTime: 0,
},
},
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
// initial validator will trigger first
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
// then it will be updated
mock.EXPECT().OnValidatorStatusUpdated(testValidationIDs[0], testNodeIDs[0], false).Times(1)
// new validator will be added
mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[1], uint64(0), true).Times(1)
},
},
{
name: "renew validation ID",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[1]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
// initial validator will trigger first
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
// then it will be removed
mock.EXPECT().OnValidatorRemoved(testValidationIDs[0], testNodeIDs[0]).Times(1)
// new validator will be added
mock.EXPECT().OnValidatorAdded(testValidationIDs[1], testNodeIDs[0], uint64(0), true).Times(1)
},
},
{
name: "renew node ID",
initialValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
IsActive: true,
StartTime: 0,
},
},
newValidators: map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[1],
IsActive: true,
StartTime: 0,
},
},
expectedLoadErr: state.ErrImmutableField,
registerMockListenerCalls: func(mock *interfaces.MockStateCallbackListener) {
// initial validator will trigger first
mock.EXPECT().OnValidatorAdded(testValidationIDs[0], testNodeIDs[0], uint64(0), true).Times(1)
// then it won't be called since we don't track the node ID changes
},
},
}
for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
require := require.New(tt)
db := memdb.New()
validatorState, err := state.NewState(db)
require.NoError(err)
// set initial validators
for vID, validator := range test.initialValidators {
err := validatorState.AddValidator(interfaces.Validator{
ValidationID: vID,
NodeID: validator.NodeID,
Weight: validator.Weight,
StartTimestamp: validator.StartTime,
IsActive: validator.IsActive,
IsL1Validator: validator.IsL1Validator,
})
require.NoError(err)
}
// enable mock listener
ctrl := gomock.NewController(tt)
mockListener := interfaces.NewMockStateCallbackListener(ctrl)
test.registerMockListenerCalls(mockListener)
validatorState.RegisterListener(mockListener)
// load new validators
err = loadValidators(validatorState, test.newValidators)
if test.expectedLoadErr != nil {
require.Error(err)
return
}
require.NoError(err)
// check if the state is as expected
require.Equal(len(test.newValidators), validatorState.GetValidationIDs().Len())
for vID, validator := range test.newValidators {
v, err := validatorState.GetValidator(vID)
require.NoError(err)
require.Equal(validator.NodeID, v.NodeID)
require.Equal(validator.Weight, v.Weight)
require.Equal(validator.StartTime, v.StartTimestamp)
require.Equal(validator.IsActive, v.IsActive)
require.Equal(validator.IsL1Validator, v.IsL1Validator)
}
})
}
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"math"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/node/utils/wrappers"
)
const (
codecVersion = uint16(0)
)
var vdrCodec interfaces.Codec
func init() {
vdrCodec = codec.NewManager(math.MaxInt32)
c := linearcodec.NewDefault()
errs := wrappers.Errs{}
errs.Add(
c.RegisterType(validatorData{}),
vdrCodec.RegisterCodec(codecVersion, c),
)
if errs.Errored() {
panic(errs.Err)
}
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"math"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/codec/linearcodec"
"github.com/luxfi/evm/interfaces"
)
const (
codecVersion = uint16(0)
)
var vdrCodec interfaces.Codec
func init() {
vdrCodec = codec.NewManager(math.MaxInt32)
c := linearcodec.NewDefault()
errs := wrappers.Errs{}
errs.Add(
c.RegisterType(validatorData{}),
vdrCodec.RegisterCodec(codecVersion, c),
)
if errs.Errored() {
panic(errs.Err)
}
}
@@ -1,77 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/luxfi/evm/plugin/evm/validators/state/interfaces (interfaces: StateCallbackListener)
//
// Generated by this command:
//
// mockgen -package=interfaces -destination=mock_listener.go . StateCallbackListener
//
// Package interfaces is a generated GoMock package.
package interfaces
import (
reflect "reflect"
ids "github.com/luxfi/node/ids"
gomock "go.uber.org/mock/gomock"
)
// MockStateCallbackListener is a mock of StateCallbackListener interface.
type MockStateCallbackListener struct {
ctrl *gomock.Controller
recorder *MockStateCallbackListenerMockRecorder
isgomock struct{}
}
// MockStateCallbackListenerMockRecorder is the mock recorder for MockStateCallbackListener.
type MockStateCallbackListenerMockRecorder struct {
mock *MockStateCallbackListener
}
// NewMockStateCallbackListener creates a new mock instance.
func NewMockStateCallbackListener(ctrl *gomock.Controller) *MockStateCallbackListener {
mock := &MockStateCallbackListener{ctrl: ctrl}
mock.recorder = &MockStateCallbackListenerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStateCallbackListener) EXPECT() *MockStateCallbackListenerMockRecorder {
return m.recorder
}
// OnValidatorAdded mocks base method.
func (m *MockStateCallbackListener) OnValidatorAdded(vID ids.ID, nodeID ids.NodeID, startTime uint64, isActive bool) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "OnValidatorAdded", vID, nodeID, startTime, isActive)
}
// OnValidatorAdded indicates an expected call of OnValidatorAdded.
func (mr *MockStateCallbackListenerMockRecorder) OnValidatorAdded(vID, nodeID, startTime, isActive any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorAdded", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorAdded), vID, nodeID, startTime, isActive)
}
// OnValidatorRemoved mocks base method.
func (m *MockStateCallbackListener) OnValidatorRemoved(vID ids.ID, nodeID ids.NodeID) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "OnValidatorRemoved", vID, nodeID)
}
// OnValidatorRemoved indicates an expected call of OnValidatorRemoved.
func (mr *MockStateCallbackListenerMockRecorder) OnValidatorRemoved(vID, nodeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorRemoved", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorRemoved), vID, nodeID)
}
// OnValidatorStatusUpdated mocks base method.
func (m *MockStateCallbackListener) OnValidatorStatusUpdated(vID ids.ID, nodeID ids.NodeID, isActive bool) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "OnValidatorStatusUpdated", vID, nodeID, isActive)
}
// OnValidatorStatusUpdated indicates an expected call of OnValidatorStatusUpdated.
func (mr *MockStateCallbackListenerMockRecorder) OnValidatorStatusUpdated(vID, nodeID, isActive any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorStatusUpdated", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorStatusUpdated), vID, nodeID, isActive)
}
@@ -1,55 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/node/utils/set"
)
type StateReader interface {
// GetValidator returns the validator data for the given validation ID
GetValidator(vID interfaces.ID) (Validator, error)
// GetValidationIDs returns the validation IDs in the state
GetValidationIDs() set.Set[interfaces.ID]
// GetNodeIDs returns the validator node IDs in the state
GetNodeIDs() set.Set[interfaces.NodeID]
// GetValidationID returns the validation ID for the given node ID
GetValidationID(nodeID interfaces.NodeID) (interfaces.ID, error)
}
type State interface {
uptime.State
StateReader
// AddValidator adds a new validator to the state
AddValidator(vdr Validator) error
// UpdateValidator updates the validator in the state
UpdateValidator(vdr Validator) error
// DeleteValidator deletes the validator from the state
DeleteValidator(vID interfaces.ID) error
// WriteState writes the validator state to the disk
WriteState() error
// RegisterListener registers a listener to the state
RegisterListener(StateCallbackListener)
}
// StateCallbackListener is a listener for the validator state
type StateCallbackListener interface {
// OnValidatorAdded is called when a new validator is added
OnValidatorAdded(vID interfaces.ID, nodeID interfaces.NodeID, startTime uint64, isActive bool)
// OnValidatorRemoved is called when a validator is removed
OnValidatorRemoved(vID interfaces.ID, nodeID interfaces.NodeID)
// OnValidatorStatusUpdated is called when a validator status is updated
OnValidatorStatusUpdated(vID interfaces.ID, nodeID interfaces.NodeID, isActive bool)
}
type Validator struct {
ValidationID interfaces.ID `json:"validationID"`
NodeID interfaces.NodeID `json:"nodeID"`
Weight uint64 `json:"weight"`
StartTimestamp uint64 `json:"startTimestamp"`
IsActive bool `json:"isActive"`
IsL1Validator bool `json:"isL1Validator"`
}
@@ -1,55 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/evm/utils"
)
type StateReader interface {
// GetValidator returns the validator data for the given validation ID
GetValidator(vID interfaces.ID) (Validator, error)
// GetValidationIDs returns the validation IDs in the state
GetValidationIDs() interfaces.Set[interfaces.ID]
// GetNodeIDs returns the validator node IDs in the state
GetNodeIDs() interfaces.Set[interfaces.NodeID]
// GetValidationID returns the validation ID for the given node ID
GetValidationID(nodeID interfaces.NodeID) (interfaces.ID, error)
}
type State interface {
uptime.State
StateReader
// AddValidator adds a new validator to the state
AddValidator(vdr Validator) error
// UpdateValidator updates the validator in the state
UpdateValidator(vdr Validator) error
// DeleteValidator deletes the validator from the state
DeleteValidator(vID interfaces.ID) error
// WriteState writes the validator state to the disk
WriteState() error
// RegisterListener registers a listener to the state
RegisterListener(StateCallbackListener)
}
// StateCallbackListener is a listener for the validator state
type StateCallbackListener interface {
// OnValidatorAdded is called when a new validator is added
OnValidatorAdded(vID interfaces.ID, nodeID interfaces.NodeID, startTime uint64, isActive bool)
// OnValidatorRemoved is called when a validator is removed
OnValidatorRemoved(vID interfaces.ID, nodeID interfaces.NodeID)
// OnValidatorStatusUpdated is called when a validator status is updated
OnValidatorStatusUpdated(vID interfaces.ID, nodeID interfaces.NodeID, isActive bool)
}
type Validator struct {
ValidationID interfaces.ID `json:"validationID"`
NodeID interfaces.NodeID `json:"nodeID"`
Weight uint64 `json:"weight"`
StartTimestamp uint64 `json:"startTimestamp"`
IsActive bool `json:"isActive"`
IsL1Validator bool `json:"isL1Validator"`
}
-360
View File
@@ -1,360 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"fmt"
"time"
"github.com/luxfi/node/database"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/evm/utils"
"github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
var _ uptime.State = &state{}
type dbUpdateStatus bool
var (
ErrAlreadyExists = fmt.Errorf("validator already exists")
ErrImmutableField = fmt.Errorf("immutable field cannot be updated")
)
const (
updatedStatus dbUpdateStatus = true
deletedStatus dbUpdateStatus = false
)
type validatorData struct {
UpDuration time.Duration `serialize:"true"`
LastUpdated uint64 `serialize:"true"`
NodeID interfaces.NodeID `serialize:"true"`
Weight uint64 `serialize:"true"`
StartTime uint64 `serialize:"true"`
IsActive bool `serialize:"true"`
IsL1Validator bool `serialize:"true"`
validationID interfaces.ID // database key
}
type state struct {
data map[interfaces.ID]*validatorData // vID -> validatorData
index map[interfaces.NodeID]interfaces.ID // nodeID -> vID
// updatedData tracks the updates since WriteValidator was last called
updatedData map[interfaces.ID]dbUpdateStatus // vID -> updated status
db database.Database
listeners []interfaces.StateCallbackListener
}
// NewState creates a new State, it also loads the data from the disk
func NewState(db database.Database) (interfaces.State, error) {
s := &state{
index: make(map[interfaces.NodeID]interfaces.ID),
data: make(map[interfaces.ID]*validatorData),
updatedData: make(map[interfaces.ID]dbUpdateStatus),
db: db,
}
if err := s.loadFromDisk(); err != nil {
return nil, fmt.Errorf("failed to load data from disk: %w", err)
}
return s, nil
}
// GetUptime returns the uptime of the validator with the given nodeID
func (s *state) GetUptime(
nodeID interfaces.NodeID,
) (time.Duration, time.Time, error) {
data, err := s.getData(nodeID)
if err != nil {
return 0, time.Time{}, err
}
return data.UpDuration, data.getLastUpdated(), nil
}
// SetUptime sets the uptime of the validator with the given nodeID
func (s *state) SetUptime(
nodeID interfaces.NodeID,
upDuration time.Duration,
lastUpdated time.Time,
) error {
data, err := s.getData(nodeID)
if err != nil {
return err
}
data.UpDuration = upDuration
data.setLastUpdated(lastUpdated)
s.updatedData[data.validationID] = updatedStatus
return nil
}
// GetStartTime returns the start time of the validator with the given nodeID
func (s *state) GetStartTime(nodeID interfaces.NodeID) (time.Time, error) {
data, err := s.getData(nodeID)
if err != nil {
return time.Time{}, err
}
return data.getStartTime(), nil
}
// AddValidator adds a new validator to the state
// the new validator is marked as updated and will be written to the disk when WriteState is called
func (s *state) AddValidator(vdr interfaces.Validator) error {
data := &validatorData{
NodeID: vdr.NodeID,
validationID: vdr.ValidationID,
IsActive: vdr.IsActive,
StartTime: vdr.StartTimestamp,
UpDuration: 0,
LastUpdated: vdr.StartTimestamp,
IsL1Validator: vdr.IsL1Validator,
Weight: vdr.Weight,
}
if err := s.addData(vdr.ValidationID, data); err != nil {
return err
}
s.updatedData[vdr.ValidationID] = updatedStatus
for _, listener := range s.listeners {
listener.OnValidatorAdded(vdr.ValidationID, vdr.NodeID, vdr.StartTimestamp, vdr.IsActive)
}
return nil
}
// UpdateValidator updates the validator in the state
// returns an error if the validator does not exist or if the immutable fields are modified
func (s *state) UpdateValidator(vdr interfaces.Validator) error {
data, exists := s.data[vdr.ValidationID]
if !exists {
return database.ErrNotFound
}
// check immutable fields
if !data.constantsAreUnmodified(vdr) {
return ErrImmutableField
}
// check if mutable fields have changed
updated := false
if data.IsActive != vdr.IsActive {
data.IsActive = vdr.IsActive
updated = true
for _, listener := range s.listeners {
listener.OnValidatorStatusUpdated(data.validationID, data.NodeID, data.IsActive)
}
}
if data.Weight != vdr.Weight {
data.Weight = vdr.Weight
updated = true
}
if updated {
s.updatedData[vdr.ValidationID] = updatedStatus
}
return nil
}
// DeleteValidator marks the validator as deleted
// marked validator will be deleted from disk when WriteState is called
func (s *state) DeleteValidator(vID interfaces.ID) error {
data, exists := s.data[vID]
if !exists {
return database.ErrNotFound
}
delete(s.data, data.validationID)
delete(s.index, data.NodeID)
// mark as deleted for WriteValidator
s.updatedData[data.validationID] = deletedStatus
for _, listener := range s.listeners {
listener.OnValidatorRemoved(vID, data.NodeID)
}
return nil
}
// WriteState writes the updated state to the disk
func (s *state) WriteState() error {
// TODO: consider adding batch size
batch := s.db.NewBatch()
for vID, updateStatus := range s.updatedData {
switch updateStatus {
case updatedStatus:
data := s.data[vID]
dataBytes, err := vdrCodec.Marshal(codecVersion, data)
if err != nil {
return err
}
if err := batch.Put(vID[:], dataBytes); err != nil {
return err
}
case deletedStatus:
if err := batch.Delete(vID[:]); err != nil {
return err
}
}
}
if err := batch.Write(); err != nil {
return err
}
// we've successfully flushed the updates, clear the updated marker.
clear(s.updatedData)
return nil
}
// SetStatus sets the active status of the validator with the given vID
func (s *state) SetStatus(vID interfaces.ID, isActive bool) error {
data, exists := s.data[vID]
if !exists {
return database.ErrNotFound
}
data.IsActive = isActive
s.updatedData[vID] = updatedStatus
for _, listener := range s.listeners {
listener.OnValidatorStatusUpdated(vID, data.NodeID, isActive)
}
return nil
}
// GetValidationIDs returns the validation IDs in the state
func (s *state) GetValidationIDs() interfaces.Set[interfaces.ID] {
ids := utils.NewSet[interfaces.ID](len(s.data))
for vID := range s.data {
ids.Add(vID)
}
return ids
}
// GetNodeIDs returns the node IDs of validators in the state
func (s *state) GetNodeIDs() interfaces.Set[interfaces.NodeID] {
ids := utils.NewSet[interfaces.NodeID](len(s.index))
for nodeID := range s.index {
ids.Add(nodeID)
}
return ids
}
// GetValidationID returns the validation ID for the given nodeID
func (s *state) GetValidationID(nodeID interfaces.NodeID) (interfaces.ID, error) {
vID, exists := s.index[nodeID]
if !exists {
return interfaces.ID{}, database.ErrNotFound
}
return vID, nil
}
// GetValidator returns the validator data for the given validationID
func (s *state) GetValidator(vID interfaces.ID) (interfaces.Validator, error) {
data, ok := s.data[vID]
if !ok {
return interfaces.Validator{}, database.ErrNotFound
}
return interfaces.Validator{
ValidationID: data.validationID,
NodeID: data.NodeID,
StartTimestamp: data.StartTime,
IsActive: data.IsActive,
Weight: data.Weight,
IsL1Validator: data.IsL1Validator,
}, nil
}
// RegisterListener registers a listener to the state
// OnValidatorAdded is called for all current validators on the provided listener before this function returns
func (s *state) RegisterListener(listener interfaces.StateCallbackListener) {
s.listeners = append(s.listeners, listener)
// notify the listener of the current state
for vID, data := range s.data {
listener.OnValidatorAdded(vID, data.NodeID, data.StartTime, data.IsActive)
}
}
// parseValidatorData parses the data from the bytes into given validatorData
func parseValidatorData(bytes []byte, data *validatorData) error {
if len(bytes) != 0 {
if _, err := vdrCodec.Unmarshal(bytes, data); err != nil {
return err
}
}
return nil
}
// Load the state from the disk
func (s *state) loadFromDisk() error {
it := s.db.NewIterator()
defer it.Release()
for it.Next() {
vIDBytes := it.Key()
vID, err := ids.ToID(vIDBytes)
if err != nil {
return fmt.Errorf("failed to parse validator ID: %w", err)
}
vdr := &validatorData{
validationID: vID,
}
if err := parseValidatorData(it.Value(), vdr); err != nil {
return fmt.Errorf("failed to parse validator data: %w", err)
}
if err := s.addData(vID, vdr); err != nil {
return err
}
}
return it.Error()
}
// addData adds the data to the state
// returns an error if the data already exists
func (s *state) addData(vID interfaces.ID, data *validatorData) error {
if _, exists := s.data[vID]; exists {
return fmt.Errorf("%w, vID: %s", ErrAlreadyExists, vID)
}
if _, exists := s.index[data.NodeID]; exists {
return fmt.Errorf("%w, nodeID: %s", ErrAlreadyExists, data.NodeID)
}
s.data[vID] = data
s.index[data.NodeID] = vID
return nil
}
// getData returns the data for the validator with the given nodeID
// returns database.ErrNotFound if the data does not exist
func (s *state) getData(nodeID interfaces.NodeID) (*validatorData, error) {
vID, exists := s.index[nodeID]
if !exists {
return nil, database.ErrNotFound
}
data, exists := s.data[vID]
if !exists {
return nil, database.ErrNotFound
}
return data, nil
}
func (v *validatorData) setLastUpdated(t time.Time) {
v.LastUpdated = uint64(t.Unix())
}
func (v *validatorData) getLastUpdated() time.Time {
return time.Unix(int64(v.LastUpdated), 0)
}
func (v *validatorData) getStartTime() time.Time {
return time.Unix(int64(v.StartTime), 0)
}
// constantsAreUnmodified returns true if the constants of this validator have
// not been modified compared to the updated validator.
func (v *validatorData) constantsAreUnmodified(u interfaces.Validator) bool {
return v.validationID == u.ValidationID &&
v.NodeID == u.NodeID &&
v.IsL1Validator == u.IsL1Validator &&
v.StartTime == u.StartTimestamp
}
-360
View File
@@ -1,360 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"fmt"
"time"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/evm/utils"
"github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
var _ uptime.State = &state{}
type dbUpdateStatus bool
var (
ErrAlreadyExists = fmt.Errorf("validator already exists")
ErrImmutableField = fmt.Errorf("immutable field cannot be updated")
)
const (
updatedStatus dbUpdateStatus = true
deletedStatus dbUpdateStatus = false
)
type validatorData struct {
UpDuration time.Duration `serialize:"true"`
LastUpdated uint64 `serialize:"true"`
NodeID interfaces.NodeID `serialize:"true"`
Weight uint64 `serialize:"true"`
StartTime uint64 `serialize:"true"`
IsActive bool `serialize:"true"`
IsL1Validator bool `serialize:"true"`
validationID interfaces.ID // database key
}
type state struct {
data map[interfaces.ID]*validatorData // vID -> validatorData
index map[interfaces.NodeID]interfaces.ID // nodeID -> vID
// updatedData tracks the updates since WriteValidator was last called
updatedData map[interfaces.ID]dbUpdateStatus // vID -> updated status
db interfaces.Database
listeners []interfaces.StateCallbackListener
}
// NewState creates a new State, it also loads the data from the disk
func NewState(db interfaces.Database) (interfaces.State, error) {
s := &state{
index: make(map[interfaces.NodeID]interfaces.ID),
data: make(map[interfaces.ID]*validatorData),
updatedData: make(map[interfaces.ID]dbUpdateStatus),
db: db,
}
if err := s.loadFromDisk(); err != nil {
return nil, fmt.Errorf("failed to load data from disk: %w", err)
}
return s, nil
}
// GetUptime returns the uptime of the validator with the given nodeID
func (s *state) GetUptime(
nodeID interfaces.NodeID,
) (time.Duration, time.Time, error) {
data, err := s.getData(nodeID)
if err != nil {
return 0, time.Time{}, err
}
return data.UpDuration, data.getLastUpdated(), nil
}
// SetUptime sets the uptime of the validator with the given nodeID
func (s *state) SetUptime(
nodeID interfaces.NodeID,
upDuration time.Duration,
lastUpdated time.Time,
) error {
data, err := s.getData(nodeID)
if err != nil {
return err
}
data.UpDuration = upDuration
data.setLastUpdated(lastUpdated)
s.updatedData[data.validationID] = updatedStatus
return nil
}
// GetStartTime returns the start time of the validator with the given nodeID
func (s *state) GetStartTime(nodeID interfaces.NodeID) (time.Time, error) {
data, err := s.getData(nodeID)
if err != nil {
return time.Time{}, err
}
return data.getStartTime(), nil
}
// AddValidator adds a new validator to the state
// the new validator is marked as updated and will be written to the disk when WriteState is called
func (s *state) AddValidator(vdr interfaces.Validator) error {
data := &validatorData{
NodeID: vdr.NodeID,
validationID: vdr.ValidationID,
IsActive: vdr.IsActive,
StartTime: vdr.StartTimestamp,
UpDuration: 0,
LastUpdated: vdr.StartTimestamp,
IsL1Validator: vdr.IsL1Validator,
Weight: vdr.Weight,
}
if err := s.addData(vdr.ValidationID, data); err != nil {
return err
}
s.updatedData[vdr.ValidationID] = updatedStatus
for _, listener := range s.listeners {
listener.OnValidatorAdded(vdr.ValidationID, vdr.NodeID, vdr.StartTimestamp, vdr.IsActive)
}
return nil
}
// UpdateValidator updates the validator in the state
// returns an error if the validator does not exist or if the immutable fields are modified
func (s *state) UpdateValidator(vdr interfaces.Validator) error {
data, exists := s.data[vdr.ValidationID]
if !exists {
return database.ErrNotFound
}
// check immutable fields
if !data.constantsAreUnmodified(vdr) {
return ErrImmutableField
}
// check if mutable fields have changed
updated := false
if data.IsActive != vdr.IsActive {
data.IsActive = vdr.IsActive
updated = true
for _, listener := range s.listeners {
listener.OnValidatorStatusUpdated(data.validationID, data.NodeID, data.IsActive)
}
}
if data.Weight != vdr.Weight {
data.Weight = vdr.Weight
updated = true
}
if updated {
s.updatedData[vdr.ValidationID] = updatedStatus
}
return nil
}
// DeleteValidator marks the validator as deleted
// marked validator will be deleted from disk when WriteState is called
func (s *state) DeleteValidator(vID interfaces.ID) error {
data, exists := s.data[vID]
if !exists {
return database.ErrNotFound
}
delete(s.data, data.validationID)
delete(s.index, data.NodeID)
// mark as deleted for WriteValidator
s.updatedData[data.validationID] = deletedStatus
for _, listener := range s.listeners {
listener.OnValidatorRemoved(vID, data.NodeID)
}
return nil
}
// WriteState writes the updated state to the disk
func (s *state) WriteState() error {
// TODO: consider adding batch size
batch := s.db.NewBatch()
for vID, updateStatus := range s.updatedData {
switch updateStatus {
case updatedStatus:
data := s.data[vID]
dataBytes, err := vdrCodec.Marshal(codecVersion, data)
if err != nil {
return err
}
if err := batch.Put(vID[:], dataBytes); err != nil {
return err
}
case deletedStatus:
if err := batch.Delete(vID[:]); err != nil {
return err
}
}
}
if err := batch.Write(); err != nil {
return err
}
// we've successfully flushed the updates, clear the updated marker.
clear(s.updatedData)
return nil
}
// SetStatus sets the active status of the validator with the given vID
func (s *state) SetStatus(vID interfaces.ID, isActive bool) error {
data, exists := s.data[vID]
if !exists {
return database.ErrNotFound
}
data.IsActive = isActive
s.updatedData[vID] = updatedStatus
for _, listener := range s.listeners {
listener.OnValidatorStatusUpdated(vID, data.NodeID, isActive)
}
return nil
}
// GetValidationIDs returns the validation IDs in the state
func (s *state) GetValidationIDs() interfaces.Set[interfaces.ID] {
ids := utils.NewSet[interfaces.ID](len(s.data))
for vID := range s.data {
ids.Add(vID)
}
return ids
}
// GetNodeIDs returns the node IDs of validators in the state
func (s *state) GetNodeIDs() interfaces.Set[interfaces.NodeID] {
ids := utils.NewSet[interfaces.NodeID](len(s.index))
for nodeID := range s.index {
ids.Add(nodeID)
}
return ids
}
// GetValidationID returns the validation ID for the given nodeID
func (s *state) GetValidationID(nodeID interfaces.NodeID) (interfaces.ID, error) {
vID, exists := s.index[nodeID]
if !exists {
return interfaces.ID{}, database.ErrNotFound
}
return vID, nil
}
// GetValidator returns the validator data for the given validationID
func (s *state) GetValidator(vID interfaces.ID) (interfaces.Validator, error) {
data, ok := s.data[vID]
if !ok {
return interfaces.Validator{}, database.ErrNotFound
}
return interfaces.Validator{
ValidationID: data.validationID,
NodeID: data.NodeID,
StartTimestamp: data.StartTime,
IsActive: data.IsActive,
Weight: data.Weight,
IsL1Validator: data.IsL1Validator,
}, nil
}
// RegisterListener registers a listener to the state
// OnValidatorAdded is called for all current validators on the provided listener before this function returns
func (s *state) RegisterListener(listener interfaces.StateCallbackListener) {
s.listeners = append(s.listeners, listener)
// notify the listener of the current state
for vID, data := range s.data {
listener.OnValidatorAdded(vID, data.NodeID, data.StartTime, data.IsActive)
}
}
// parseValidatorData parses the data from the bytes into given validatorData
func parseValidatorData(bytes []byte, data *validatorData) error {
if len(bytes) != 0 {
if _, err := vdrCodec.Unmarshal(bytes, data); err != nil {
return err
}
}
return nil
}
// Load the state from the disk
func (s *state) loadFromDisk() error {
it := s.db.NewIterator()
defer it.Release()
for it.Next() {
vIDBytes := it.Key()
vID, err := ids.ToID(vIDBytes)
if err != nil {
return fmt.Errorf("failed to parse validator ID: %w", err)
}
vdr := &validatorData{
validationID: vID,
}
if err := parseValidatorData(it.Value(), vdr); err != nil {
return fmt.Errorf("failed to parse validator data: %w", err)
}
if err := s.addData(vID, vdr); err != nil {
return err
}
}
return it.Error()
}
// addData adds the data to the state
// returns an error if the data already exists
func (s *state) addData(vID interfaces.ID, data *validatorData) error {
if _, exists := s.data[vID]; exists {
return fmt.Errorf("%w, vID: %s", ErrAlreadyExists, vID)
}
if _, exists := s.index[data.NodeID]; exists {
return fmt.Errorf("%w, nodeID: %s", ErrAlreadyExists, data.NodeID)
}
s.data[vID] = data
s.index[data.NodeID] = vID
return nil
}
// getData returns the data for the validator with the given nodeID
// returns database.ErrNotFound if the data does not exist
func (s *state) getData(nodeID interfaces.NodeID) (*validatorData, error) {
vID, exists := s.index[nodeID]
if !exists {
return nil, database.ErrNotFound
}
data, exists := s.data[vID]
if !exists {
return nil, database.ErrNotFound
}
return data, nil
}
func (v *validatorData) setLastUpdated(t time.Time) {
v.LastUpdated = uint64(t.Unix())
}
func (v *validatorData) getLastUpdated() time.Time {
return time.Unix(int64(v.LastUpdated), 0)
}
func (v *validatorData) getStartTime() time.Time {
return time.Unix(int64(v.StartTime), 0)
}
// constantsAreUnmodified returns true if the constants of this validator have
// not been modified compared to the updated validator.
func (v *validatorData) constantsAreUnmodified(u interfaces.Validator) bool {
return v.validationID == u.ValidationID &&
v.NodeID == u.NodeID &&
v.IsL1Validator == u.IsL1Validator &&
v.StartTime == u.StartTimestamp
}
@@ -1,332 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/luxfi/node/codec"
"github.com/luxfi/node/database"
"github.com/luxfi/node/database/memdb"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/utils/wrappers"
"github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
func TestState(t *testing.T) {
require := require.New(t)
db := memdb.New()
state, err := NewState(db)
require.NoError(err)
// get non-existent uptime
nodeID := ids.GenerateTestNodeID()
vID := ids.GenerateTestID()
_, _, err = state.GetUptime(nodeID)
require.ErrorIs(err, database.ErrNotFound)
// set non-existent uptime
startTime := time.Now()
err = state.SetUptime(nodeID, 1, startTime)
require.ErrorIs(err, database.ErrNotFound)
// add new validator
vdr := interfaces.Validator{
ValidationID: vID,
NodeID: nodeID,
Weight: 1,
StartTimestamp: uint64(startTime.Unix()),
IsActive: true,
IsL1Validator: true,
}
state.AddValidator(vdr)
// adding the same validator should fail
err = state.AddValidator(vdr)
require.ErrorIs(err, ErrAlreadyExists)
// adding the same nodeID should fail
newVdr := vdr
newVdr.ValidationID = ids.GenerateTestID()
err = state.AddValidator(newVdr)
require.ErrorIs(err, ErrAlreadyExists)
// get uptime
uptime, lastUpdated, err := state.GetUptime(nodeID)
require.NoError(err)
require.Equal(time.Duration(0), uptime)
require.Equal(startTime.Unix(), lastUpdated.Unix())
// set uptime
newUptime := 2 * time.Minute
newLastUpdated := lastUpdated.Add(time.Hour)
require.NoError(state.SetUptime(nodeID, newUptime, newLastUpdated))
// get new uptime
uptime, lastUpdated, err = state.GetUptime(nodeID)
require.NoError(err)
require.Equal(newUptime, uptime)
require.Equal(newLastUpdated, lastUpdated)
// set status
vdr.IsActive = false
require.NoError(state.UpdateValidator(vdr))
// get status
data, err := state.GetValidator(vID)
require.NoError(err)
require.False(data.IsActive)
// set weight
newWeight := uint64(2)
vdr.Weight = newWeight
require.NoError(state.UpdateValidator(vdr))
// get weight
data, err = state.GetValidator(vID)
require.NoError(err)
require.Equal(newWeight, data.Weight)
// set a different node ID should fail
newNodeID := ids.GenerateTestNodeID()
vdr.NodeID = newNodeID
require.ErrorIs(state.UpdateValidator(vdr), ErrImmutableField)
// set a different start time should fail
vdr.StartTimestamp = vdr.StartTimestamp + 100
require.ErrorIs(state.UpdateValidator(vdr), ErrImmutableField)
// set IsL1Validator should fail
vdr.IsL1Validator = false
require.ErrorIs(state.UpdateValidator(vdr), ErrImmutableField)
// set validation ID should result in not found
vdr.ValidationID = ids.GenerateTestID()
require.ErrorIs(state.UpdateValidator(vdr), database.ErrNotFound)
// delete uptime
require.NoError(state.DeleteValidator(vID))
// get deleted uptime
_, _, err = state.GetUptime(nodeID)
require.ErrorIs(err, database.ErrNotFound)
}
func TestWriteValidator(t *testing.T) {
require := require.New(t)
db := memdb.New()
state, err := NewState(db)
require.NoError(err)
// write empty uptimes
require.NoError(state.WriteState())
// load uptime
nodeID := ids.GenerateTestNodeID()
vID := ids.GenerateTestID()
startTime := time.Now()
require.NoError(state.AddValidator(interfaces.Validator{
ValidationID: vID,
NodeID: nodeID,
Weight: 1,
StartTimestamp: uint64(startTime.Unix()),
IsActive: true,
IsL1Validator: true,
}))
// write state, should reflect to DB
require.NoError(state.WriteState())
require.True(db.Has(vID[:]))
// set uptime
newUptime := 2 * time.Minute
newLastUpdated := startTime.Add(time.Hour)
require.NoError(state.SetUptime(nodeID, newUptime, newLastUpdated))
require.NoError(state.WriteState())
// refresh state, should load from DB
state, err = NewState(db)
require.NoError(err)
// get uptime
uptime, lastUpdated, err := state.GetUptime(nodeID)
require.NoError(err)
require.Equal(newUptime, uptime)
require.Equal(newLastUpdated.Unix(), lastUpdated.Unix())
// delete
require.NoError(state.DeleteValidator(vID))
// write state, should reflect to DB
require.NoError(state.WriteState())
require.False(db.Has(vID[:]))
}
func TestParseValidator(t *testing.T) {
testNodeID, err := ids.NodeIDFromString("NodeID-CaBYJ9kzHvrQFiYWowMkJGAQKGMJqZoat")
require.NoError(t, err)
type test struct {
name string
bytes []byte
expected *validatorData
expectedErr error
}
tests := []test{
{
name: "nil",
bytes: nil,
expected: &validatorData{
LastUpdated: 0,
StartTime: 0,
validationID: ids.Empty,
NodeID: ids.EmptyNodeID,
UpDuration: 0,
Weight: 0,
IsActive: false,
IsL1Validator: false,
},
expectedErr: nil,
},
{
name: "empty",
bytes: []byte{},
expected: &validatorData{
LastUpdated: 0,
StartTime: 0,
validationID: ids.Empty,
NodeID: ids.EmptyNodeID,
UpDuration: 0,
Weight: 0,
IsActive: false,
IsL1Validator: false,
},
expectedErr: nil,
},
{
name: "valid",
bytes: []byte{
// codec version
0x00, 0x00,
// up duration
0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80,
// last updated
0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0,
// node ID
0x7e, 0xef, 0xe8, 0x8a, 0x45, 0xfb, 0x7a, 0xc4,
0xb0, 0x59, 0xc9, 0x33, 0x71, 0x0a, 0x57, 0x33,
0xff, 0x9f, 0x4b, 0xab,
// weight
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
// start time
0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80,
// status
0x01,
// IsL1Validator
0x01,
},
expected: &validatorData{
UpDuration: time.Duration(6000000),
LastUpdated: 900000,
NodeID: testNodeID,
StartTime: 6000000,
IsActive: true,
Weight: 1,
IsL1Validator: true,
},
},
{
name: "invalid codec version",
bytes: []byte{
// codec version
0x00, 0x02,
// up duration
0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80,
// last updated
0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0,
},
expected: nil,
expectedErr: codec.ErrUnknownVersion,
},
{
name: "short byte len",
bytes: []byte{
// codec version
0x00, 0x00,
// up duration
0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80,
// last updated
0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0,
},
expected: nil,
expectedErr: wrappers.ErrInsufficientLength,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
var data validatorData
err := parseValidatorData(tt.bytes, &data)
require.ErrorIs(err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
require.Equal(tt.expected, &data)
})
}
}
func TestStateListener(t *testing.T) {
require := require.New(t)
db := memdb.New()
state, err := NewState(db)
require.NoError(err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectedvID := ids.GenerateTestID()
expectedNodeID := ids.GenerateTestNodeID()
expectedStartTime := time.Now()
mockListener := interfaces.NewMockStateCallbackListener(ctrl)
// add initial validator to test RegisterListener
initialvID := ids.GenerateTestID()
initialNodeID := ids.GenerateTestNodeID()
initialStartTime := time.Now()
// add initial validator
require.NoError(state.AddValidator(interfaces.Validator{
ValidationID: initialvID,
NodeID: initialNodeID,
Weight: 1,
StartTimestamp: uint64(initialStartTime.Unix()),
IsActive: true,
IsL1Validator: true,
}))
// register listener
mockListener.EXPECT().OnValidatorAdded(initialvID, initialNodeID, uint64(initialStartTime.Unix()), true)
state.RegisterListener(mockListener)
// add new validator
mockListener.EXPECT().OnValidatorAdded(expectedvID, expectedNodeID, uint64(expectedStartTime.Unix()), true)
vdr := interfaces.Validator{
ValidationID: expectedvID,
NodeID: expectedNodeID,
Weight: 1,
StartTimestamp: uint64(expectedStartTime.Unix()),
IsActive: true,
IsL1Validator: true,
}
require.NoError(state.AddValidator(vdr))
// set status
mockListener.EXPECT().OnValidatorStatusUpdated(expectedvID, expectedNodeID, false)
vdr.IsActive = false
require.NoError(state.UpdateValidator(vdr))
// set status twice should not trigger listener
require.NoError(state.UpdateValidator(vdr))
// remove validator
mockListener.EXPECT().OnValidatorRemoved(expectedvID, expectedNodeID)
require.NoError(state.DeleteValidator(expectedvID))
}
@@ -1,16 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus/uptime"
validatorsstateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
type PausableManager interface {
uptime.Manager
validatorsstateinterfaces.StateCallbackListener
IsPaused(nodeID ids.NodeID) bool
}
@@ -1,16 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package interfaces
import (
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
validatorsstateinterfaces "github.com/luxfi/evm/plugin/evm/validators/state/interfaces"
)
type PausableManager interface {
uptime.Manager
validatorsstateinterfaces.StateCallbackListener
IsPaused(nodeID interfaces.NodeID) bool
}
@@ -1,130 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package uptime
import (
"errors"
"github.com/luxfi/evm/log"
"github.com/luxfi/evm/plugin/evm/validators/uptime/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/node/utils/set"
)
var errPausedDisconnect = errors.New("paused node cannot be disconnected")
type pausableManager struct {
uptime.Manager
pausedVdrs set.Set[interfaces.NodeID]
// connectedVdrs is a set of nodes that are connected to the manager.
// This is used to immediately connect nodes when they are unpaused.
connectedVdrs set.Set[interfaces.NodeID]
}
// NewPausableManager takes an uptime.Manager and returns a PausableManager
func NewPausableManager(manager uptime.Manager) interfaces.PausableManager {
return &pausableManager{
pausedVdrs: make(set.Set[interfaces.NodeID]),
connectedVdrs: make(set.Set[interfaces.NodeID]),
Manager: manager,
}
}
// Connect connects the node with the given ID to the uptime.Manager
// If the node is paused, it will not be connected
func (p *pausableManager) Connect(nodeID interfaces.NodeID) error {
p.connectedVdrs.Add(nodeID)
if !p.IsPaused(nodeID) && !p.Manager.IsConnected(nodeID) {
return p.Manager.Connect(nodeID)
}
return nil
}
// Disconnect disconnects the node with the given ID from the uptime.Manager
// If the node is paused, it will not be disconnected
// Invariant: we should never have a connected paused node that is disconnecting
func (p *pausableManager) Disconnect(nodeID interfaces.NodeID) error {
p.connectedVdrs.Remove(nodeID)
if p.Manager.IsConnected(nodeID) {
if p.IsPaused(nodeID) {
// We should never see this case
return errPausedDisconnect
}
return p.Manager.Disconnect(nodeID)
}
return nil
}
// IsConnected returns true if the node with the given ID is connected to this manager
// Note: Inner manager may have a different view of the connection status due to pausing
func (p *pausableManager) IsConnected(nodeID interfaces.NodeID) bool {
return p.connectedVdrs.Contains(nodeID)
}
// OnValidatorAdded is called when a validator is added.
// If the node is inactive, it will be paused.
func (p *pausableManager) OnValidatorAdded(_ interfaces.ID, nodeID interfaces.NodeID, _ uint64, isActive bool) {
if !isActive {
err := p.pause(nodeID)
if err != nil {
log.Error("failed to handle added validator %s: %s", nodeID, err)
}
}
}
// OnValidatorRemoved is called when a validator is removed.
// If the node is already paused, it will be resumed.
func (p *pausableManager) OnValidatorRemoved(_ interfaces.ID, nodeID interfaces.NodeID) {
if p.IsPaused(nodeID) {
err := p.resume(nodeID)
if err != nil {
log.Error("failed to handle validator removed %s: %s", nodeID, err)
}
}
}
// OnValidatorStatusUpdated is called when the status of a validator is updated.
// If the node is active, it will be resumed. If the node is inactive, it will be paused.
func (p *pausableManager) OnValidatorStatusUpdated(_ interfaces.ID, nodeID interfaces.NodeID, isActive bool) {
var err error
if isActive {
err = p.resume(nodeID)
} else {
err = p.pause(nodeID)
}
if err != nil {
log.Error("failed to update status for node %s: %s", nodeID, err)
}
}
// IsPaused returns true if the node with the given ID is paused.
func (p *pausableManager) IsPaused(nodeID interfaces.NodeID) bool {
return p.pausedVdrs.Contains(nodeID)
}
// pause pauses uptime tracking for the node with the given ID
// pause can disconnect the node from the uptime.Manager if it is connected.
func (p *pausableManager) pause(nodeID interfaces.NodeID) error {
p.pausedVdrs.Add(nodeID)
if p.Manager.IsConnected(nodeID) {
// If the node is connected, then we need to disconnect it from
// manager
// This should be fine in case tracking has not started yet since
// the inner manager should handle disconnects accordingly
return p.Manager.Disconnect(nodeID)
}
return nil
}
// resume resumes uptime tracking for the node with the given ID
// resume can connect the node to the uptime.Manager if it was connected.
func (p *pausableManager) resume(nodeID interfaces.NodeID) error {
p.pausedVdrs.Remove(nodeID)
if p.connectedVdrs.Contains(nodeID) && !p.Manager.IsConnected(nodeID) {
return p.Manager.Connect(nodeID)
}
return nil
}
@@ -1,130 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package uptime
import (
"errors"
"github.com/luxfi/evm/log"
"github.com/luxfi/evm/plugin/evm/validators/uptime/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/evm/utils"
)
var errPausedDisconnect = errors.New("paused node cannot be disconnected")
type pausableManager struct {
uptime.Manager
pausedVdrs interfaces.Set[interfaces.NodeID]
// connectedVdrs is a set of nodes that are connected to the manager.
// This is used to immediately connect nodes when they are unpaused.
connectedVdrs interfaces.Set[interfaces.NodeID]
}
// NewPausableManager takes an uptime.Manager and returns a PausableManager
func NewPausableManager(manager uptime.Manager) interfaces.PausableManager {
return &pausableManager{
pausedVdrs: make(interfaces.Set[interfaces.NodeID]),
connectedVdrs: make(interfaces.Set[interfaces.NodeID]),
Manager: manager,
}
}
// Connect connects the node with the given ID to the uptime.Manager
// If the node is paused, it will not be connected
func (p *pausableManager) Connect(nodeID interfaces.NodeID) error {
p.connectedVdrs.Add(nodeID)
if !p.IsPaused(nodeID) && !p.Manager.IsConnected(nodeID) {
return p.Manager.Connect(nodeID)
}
return nil
}
// Disconnect disconnects the node with the given ID from the uptime.Manager
// If the node is paused, it will not be disconnected
// Invariant: we should never have a connected paused node that is disconnecting
func (p *pausableManager) Disconnect(nodeID interfaces.NodeID) error {
p.connectedVdrs.Remove(nodeID)
if p.Manager.IsConnected(nodeID) {
if p.IsPaused(nodeID) {
// We should never see this case
return errPausedDisconnect
}
return p.Manager.Disconnect(nodeID)
}
return nil
}
// IsConnected returns true if the node with the given ID is connected to this manager
// Note: Inner manager may have a different view of the connection status due to pausing
func (p *pausableManager) IsConnected(nodeID interfaces.NodeID) bool {
return p.connectedVdrs.Contains(nodeID)
}
// OnValidatorAdded is called when a validator is added.
// If the node is inactive, it will be paused.
func (p *pausableManager) OnValidatorAdded(_ interfaces.ID, nodeID interfaces.NodeID, _ uint64, isActive bool) {
if !isActive {
err := p.pause(nodeID)
if err != nil {
log.Error("failed to handle added validator %s: %s", nodeID, err)
}
}
}
// OnValidatorRemoved is called when a validator is removed.
// If the node is already paused, it will be resumed.
func (p *pausableManager) OnValidatorRemoved(_ interfaces.ID, nodeID interfaces.NodeID) {
if p.IsPaused(nodeID) {
err := p.resume(nodeID)
if err != nil {
log.Error("failed to handle validator removed %s: %s", nodeID, err)
}
}
}
// OnValidatorStatusUpdated is called when the status of a validator is updated.
// If the node is active, it will be resumed. If the node is inactive, it will be paused.
func (p *pausableManager) OnValidatorStatusUpdated(_ interfaces.ID, nodeID interfaces.NodeID, isActive bool) {
var err error
if isActive {
err = p.resume(nodeID)
} else {
err = p.pause(nodeID)
}
if err != nil {
log.Error("failed to update status for node %s: %s", nodeID, err)
}
}
// IsPaused returns true if the node with the given ID is paused.
func (p *pausableManager) IsPaused(nodeID interfaces.NodeID) bool {
return p.pausedVdrs.Contains(nodeID)
}
// pause pauses uptime tracking for the node with the given ID
// pause can disconnect the node from the uptime.Manager if it is connected.
func (p *pausableManager) pause(nodeID interfaces.NodeID) error {
p.pausedVdrs.Add(nodeID)
if p.Manager.IsConnected(nodeID) {
// If the node is connected, then we need to disconnect it from
// manager
// This should be fine in case tracking has not started yet since
// the inner manager should handle disconnects accordingly
return p.Manager.Disconnect(nodeID)
}
return nil
}
// resume resumes uptime tracking for the node with the given ID
// resume can connect the node to the uptime.Manager if it was connected.
func (p *pausableManager) resume(nodeID interfaces.NodeID) error {
p.pausedVdrs.Remove(nodeID)
if p.connectedVdrs.Contains(nodeID) && !p.Manager.IsConnected(nodeID) {
return p.Manager.Connect(nodeID)
}
return nil
}
@@ -1,323 +0,0 @@
// Copyright (C) 2019-2024, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package uptime
import (
"testing"
"time"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus/uptime"
"github.com/luxfi/node/utils/timer/mockable"
"github.com/luxfi/evm/plugin/evm/validators/uptime/interfaces"
"github.com/stretchr/testify/require"
)
func TestPausableManager(t *testing.T) {
vID := ids.GenerateTestID()
nodeID0 := ids.GenerateTestNodeID()
startTime := time.Now()
tests := []struct {
name string
testFunc func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State)
}{
{
name: "Case 1: Connect, pause, start tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Connect before tracking
require.NoError(up.Connect(nodeID0))
addTime(clk, time.Second)
expectedUptime := 1 * time.Second
// Pause before tracking
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Elapse Time
addTime(clk, time.Second)
// Since we have not started tracking this node yet, its observed uptime should
// be incremented even though it is actually paused.
expectedUptime += 1 * time.Second
// Start tracking
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime here should not increase after start tracking
// since the node is still paused after we started tracking
currentTime := addTime(clk, time.Second)
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 2: Start tracking, connect, pause, re-connect, resume",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Connect
addTime(clk, 1*time.Second)
require.NoError(up.Connect(nodeID0))
// Pause
addTime(clk, 1*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Elapse time
currentTime := addTime(clk, 2*time.Second)
// Uptime should be 1 second since the node was paused after 1 sec
expectedUptime := 1 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect and check uptime
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect again and check uptime
addTime(clk, 4*time.Second)
require.NoError(up.Connect(nodeID0))
currentTime = addTime(clk, 5*time.Second)
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Resume and check uptime
currentTime = addTime(clk, 6*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Uptime should not have increased since the node was paused
// and we just resumed it
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Elapsed time check
currentTime = addTime(clk, 7*time.Second)
// Uptime should increase by 7 seconds above since the node was resumed
expectedUptime += 7 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 3: Pause, start tracking, connect, re-connect, resume",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Pause before tracking
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Start tracking
addTime(clk, time.Second)
// Uptime should be 1 since the node was paused before we started tracking
expectedUptime := 1 * time.Second
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Connect and check uptime
addTime(clk, 1*time.Second)
checkUptime(t, up, nodeID0, expectedUptime, clk.Time())
require.NoError(up.Connect(nodeID0))
currentTime := addTime(clk, 2*time.Second)
// Uptime should not have increased since the node was paused after we started tracking
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect and check uptime
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect again and resume
addTime(clk, 4*time.Second)
require.NoError(up.Connect(nodeID0))
addTime(clk, 5*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Check uptime after resume
currentTime = addTime(clk, 6*time.Second)
// Uptime should have increased by 6 seconds since the node was resumed
expectedUptime += 6 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 4: Start tracking, connect, pause, stop tracking, resume tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Pause and check uptime
currentTime := addTime(clk, 2*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Uptime should be 2 seconds since the node was paused after 2 seconds
expectedUptime := 2 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Stop tracking and reinitialize manager
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
up = NewPausableManager(uptime.NewManager(s, clk))
// Uptime should not have increased since the node was paused
// and we have not started tracking again
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Pause and check uptime
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Resume and check uptime
currentTime = addTime(clk, 5*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Uptime should have increased by 5 seconds since the node was resumed
expectedUptime += 5 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Start tracking and check elapsed time
currentTime = addTime(clk, 6*time.Second)
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime should have increased by 6 seconds since we started tracking
// and node was resumed (we assume the node was online until we started tracking)
expectedUptime += 6 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Elapsed time
currentTime = addTime(clk, 7*time.Second)
// Uptime should not have increased since the node was not connected
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect and final uptime check
require.NoError(up.Connect(nodeID0))
currentTime = addTime(clk, 8*time.Second)
// Uptime should have increased by 8 seconds since the node was connected
expectedUptime += 8 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 5: Node paused after we stop tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Stop tracking
currentTime := addTime(clk, 2*time.Second)
expectedUptime := 2 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
// Pause after a while
addTime(clk, 3*time.Second)
// expectedUptime should increase since we stopped tracking
expectedUptime += 3 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// expectedUptime should increase since we stopped tracking (even if the node was paused)
currentTime = addTime(clk, 4*time.Second)
expectedUptime += 4 * time.Second
// Start tracking and check elapsed time
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime have increased since the node was paused before we started tracking
// We should be optimistic and assume the node was online and active until we start tracking
require.True(up.IsPaused(nodeID0))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 6: Paused node got resumed after we stop tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Pause after a while
currentTime := addTime(clk, 2*time.Second)
// expectedUptime should increase
expectedUptime := 2 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Stop tracking
currentTime = addTime(clk, 3*time.Second)
// expectedUptime should not increase since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
// Resume after a while
addTime(clk, 4*time.Second)
// expectedUptime should increase since we stopped tracking
expectedUptime += 4 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// expectedUptime should increase since we stopped tracking
currentTime = addTime(clk, 5*time.Second)
expectedUptime += 5 * time.Second
// Start tracking and check elapsed time
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime should have increased by 4 seconds since the node was resumed
// We should be optimistic and assume the node was online and active until we start tracking
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
up, clk, s := setupTestEnv(nodeID0, startTime)
test.testFunc(t, up, clk, s)
})
}
}
func setupTestEnv(nodeID ids.NodeID, startTime time.Time) (interfaces.PausableManager, *mockable.Clock, uptime.State) {
clk := mockable.Clock{}
clk.Set(startTime)
s := uptime.NewTestState()
s.AddNode(nodeID, startTime)
up := NewPausableManager(uptime.NewManager(s, &clk))
return up, &clk, s
}
func addTime(clk *mockable.Clock, duration time.Duration) time.Time {
clk.Set(clk.Time().Add(duration))
return clk.Time()
}
func checkUptime(t *testing.T, up interfaces.PausableManager, nodeID ids.NodeID, expectedUptime time.Duration, expectedLastUpdate time.Time) {
t.Helper()
uptime, lastUpdated, err := up.CalculateUptime(nodeID)
require.NoError(t, err)
require.Equal(t, expectedLastUpdate.Unix(), lastUpdated.Unix())
require.Equal(t, expectedUptime, uptime)
}
-35
View File
@@ -1,35 +0,0 @@
package evm
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/luxfi/node/version"
"github.com/stretchr/testify/assert"
)
type rpcChainCompatibility struct {
RPCChainVMProtocolVersion map[string]uint `json:"rpcChainVMProtocolVersion"`
}
const compatibilityFile = "../../compatibility.json"
func TestCompatibility(t *testing.T) {
compat, err := os.ReadFile(compatibilityFile)
require.NoError(t, err, "reading compatibility file")
var parsedCompat rpcChainCompatibility
err = json.Unmarshal(compat, &parsedCompat)
require.NoError(t, err, "json decoding compatibility file")
rpcChainVMVersion, valueInJSON := parsedCompat.RPCChainVMProtocolVersion[Version]
if !valueInJSON {
t.Fatalf("%s has evm version %s missing from rpcChainVMProtocolVersion object",
filepath.Base(compatibilityFile), Version)
}
if rpcChainVMVersion != version.RPCChainVMProtocol {
t.Fatalf("%s has evm version %s stated as compatible with RPC chain VM protocol version %d but Lux protocol version is %d",
filepath.Base(compatibilityFile), Version, rpcChainVMVersion, version.RPCChainVMProtocol)
}
}
+2 -2
View File
@@ -76,7 +76,7 @@ import (
"github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/database/prefixdb"
"github.com/luxfi/node/network/p2p"
"github.com/luxfi/node/network/p2p/acp118"
"github.com/luxfi/node/network/p2p/lip118"
"github.com/luxfi/node/network/p2p/gossip"
evmValidators "github.com/luxfi/evm/plugin/evm/validators"
"github.com/luxfi/node/utils"
@@ -546,7 +546,7 @@ func (vm *VM) Initialize(
go vm.ctx.Log.RecoverAndPanic(vm.startContinuousProfiler)
// Add p2p warp message warpHandler
warpHandler := acp118.NewCachedHandler(meteredCache, vm.warpBackend, vm.ctx.WarpSigner)
warpHandler := lip118.NewCachedHandler(meteredCache, vm.warpBackend, vm.ctx.WarpSigner)
vm.Network.AddHandler(p2p.SignatureRequestHandlerID, warpHandler)
vm.setAppRequestHandlers()
-1299
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-237
View File
@@ -1,237 +0,0 @@
// (c) 2019-2021, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/interfaces/core/rawdb"
"github.com/luxfi/evm/plugin/evm/config"
"github.com/luxfi/evm/plugin/evm/database"
"github.com/luxfi/node/api/metrics"
luxdatabase "github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/database/pebbledb"
"github.com/luxfi/node/database/leveldb"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/constants"
"github.com/luxfi/node/utils/logging"
)
const (
dbMetricsPrefix = "db"
meterDBGatherer = "meterdb"
)
type DatabaseConfig struct {
// If true, all writes are to memory and are discarded at shutdown.
ReadOnly bool `json:"readOnly"`
// Path to database
Path string `json:"path"`
// Name of the database type to use
Name string `json:"name"`
// Config bytes (JSON) for the database
// See relevant (pebbledb, leveldb) config options
Config []byte `json:"-"`
}
// initializeDBs initializes the databases used by the VM.
// If [useStandaloneDB] is true, the chain will use a standalone database for its state.
// Otherwise, the chain will use the provided [avaDB] for its state.
func (vm *VM) initializeDBs(avaDB luxinterfaces.Database) error {
db := avaDB
// skip standalone database initialization if we are running in unit tests
if vm.ctx.NetworkID != constants.UnitTestID {
// first initialize the accepted block database to check if we need to use a standalone database
acceptedDB := interfaces.NewPrefixDB(acceptedPrefix, avaDB)
useStandAloneDB, err := vm.useStandaloneDatabase(acceptedDB)
if err != nil {
return err
}
if useStandAloneDB {
// If we are using a standalone database, we need to create a new database
// for the chain state.
dbConfig, err := getDatabaseConfig(vm.config, vm.ctx.ChainDataDir)
if err != nil {
return err
}
log.Info("Using standalone database for the chain state", "DatabaseConfig", dbConfig)
db, err = newStandaloneDatabase(dbConfig, vm.ctx.Metrics, vm.ctx.Log)
if err != nil {
return err
}
vm.usingStandaloneDB = true
}
}
// Use NewNested rather than New so that the structure of the database
// remains the same regardless of the provided baseDB type.
vm.chaindb = rawdb.NewDatabase(database.WrapDatabase(interfaces.NewPrefixDBNested(ethDBPrefix, db)))
vm.versiondb = interfaces.NewVersionDB(db)
vm.acceptedBlockDB = interfaces.NewPrefixDB(acceptedPrefix, vm.versiondb)
vm.metadataDB = interfaces.NewPrefixDB(metadataPrefix, vm.versiondb)
vm.db = db
// Note warpDB and validatorsDB are not part of versiondb because it is not necessary
// that they are committed to the database atomically with
// the last accepted block.
// [warpDB] is used to store warp message signatures
// set to a prefixDB with the prefix [warpPrefix]
vm.warpDB = interfaces.NewPrefixDB(warpPrefix, db)
// [validatorsDB] is used to store the current validator set and uptimes
// set to a prefixDB with the prefix [validatorsDBPrefix]
vm.validatorsDB = interfaces.NewPrefixDB(validatorsDBPrefix, db)
return nil
}
func (vm *VM) inspectDatabases() error {
start := time.Now()
log.Info("Starting database inspection")
if err := rawdb.InspectDatabase(vm.chaindb, nil, nil); err != nil {
return err
}
if err := inspectDB(vm.acceptedBlockDB, "acceptedBlockDB"); err != nil {
return err
}
if err := inspectDB(vm.metadataDB, "metadataDB"); err != nil {
return err
}
if err := inspectDB(vm.warpDB, "warpDB"); err != nil {
return err
}
if err := inspectDB(vm.validatorsDB, "validatorsDB"); err != nil {
return err
}
log.Info("Completed database inspection", "elapsed", time.Since(start))
return nil
}
// useStandaloneDatabase returns true if the chain can and should use a standalone database
// other than given by [db] in Initialize()
func (vm *VM) useStandaloneDatabase(acceptedDB luxinterfaces.Database) (bool, error) {
// no config provided, use default
standaloneDBFlag := vm.config.UseStandaloneDatabase
if standaloneDBFlag != nil {
return standaloneDBFlag.Bool(), nil
}
// check if the chain can use a standalone database
_, err := acceptedDB.Get(lastAcceptedKey)
if err == luxdatabase.ErrNotFound {
// If there is nothing in the database, we can use the standalone database
return true, nil
}
return false, err
}
// getDatabaseConfig returns the database configuration for the chain
// to be used by separate, standalone database.
func getDatabaseConfig(config config.Config, chainDataDir string) (DatabaseConfig, error) {
var (
configBytes []byte
err error
)
if len(config.DatabaseConfigContent) != 0 {
dbConfigContent := config.DatabaseConfigContent
configBytes, err = base64.StdEncoding.DecodeString(dbConfigContent)
if err != nil {
return DatabaseConfig{}, fmt.Errorf("unable to decode base64 content: %w", err)
}
} else if len(config.DatabaseConfigFile) != 0 {
configPath := config.DatabaseConfigFile
configBytes, err = os.ReadFile(configPath)
if err != nil {
return DatabaseConfig{}, err
}
}
dbPath := filepath.Join(chainDataDir, "db")
if len(config.DatabasePath) != 0 {
dbPath = config.DatabasePath
}
return DatabaseConfig{
Name: config.DatabaseType,
ReadOnly: config.DatabaseReadOnly,
Path: dbPath,
Config: configBytes,
}, nil
}
func inspectDB(db luxinterfaces.Database, label string) error {
it := db.NewIterator()
defer it.Release()
var (
count int64
start = time.Now()
logged = time.Now()
// Totals
total common.StorageSize
)
// Inspect key-value database first.
for it.Next() {
var (
key = it.Key()
size = common.StorageSize(len(key) + len(it.Value()))
)
total += size
count++
if count%1000 == 0 && time.Since(logged) > 8*time.Second {
log.Info("Inspecting database", "label", label, "count", count, "elapsed", common.PrettyDuration(time.Since(start)))
logged = time.Now()
}
}
// Display the database statistic.
log.Info("Database statistics", "label", label, "total", total.String(), "count", count)
return nil
}
func newStandaloneDatabase(dbConfig DatabaseConfig, gatherer metrics.MultiGatherer, logger logging.Logger) (luxinterfaces.Database, error) {
dbPath := filepath.Join(dbConfig.Path, dbConfig.Name)
dbConfigBytes := dbConfig.Config
// If the database is pebble, we need to set the config
// to use no sync. Sync mode in pebble has an issue with OSs like MacOS.
if dbConfig.Name == pebbledb.Name {
cfg := pebbledb.DefaultConfig
// Default to "no sync" for pebble db
cfg.Sync = false
if len(dbConfigBytes) > 0 {
if err := json.Unmarshal(dbConfigBytes, &cfg); err != nil {
return nil, err
}
}
var err error
// Marshal the config back to bytes to ensure that new defaults are applied
dbConfigBytes, err = json.Marshal(cfg)
if err != nil {
return nil, err
}
}
var db luxinterfaces.Database
switch dbConfig.Name {
case pebbledb.Name:
db, err = pebbledb.New(dbPath, dbConfigBytes, logger, gatherer)
case leveldb.Name:
db, err = leveldb.New(dbPath, dbConfigBytes, logger, gatherer)
default:
return nil, fmt.Errorf("unknown database type: %s", dbConfig.Name)
}
if err != nil {
return nil, fmt.Errorf("couldn't create database: %w", err)
}
return db, nil
}
-237
View File
@@ -1,237 +0,0 @@
// (c) 2019-2021, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/luxfi/evm/interfaces/core/rawdb"
"github.com/luxfi/evm/plugin/evm/config"
"github.com/luxfi/evm/plugin/evm/database"
"github.com/luxfi/node/api/metrics"
luxdatabase "github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/database/pebbledb"
"github.com/luxfi/node/database/leveldb"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/evm/interfaces"
"github.com/luxfi/node/utils/logging"
)
const (
dbMetricsPrefix = "db"
meterDBGatherer = "meterdb"
)
type DatabaseConfig struct {
// If true, all writes are to memory and are discarded at shutdown.
ReadOnly bool `json:"readOnly"`
// Path to database
Path string `json:"path"`
// Name of the database type to use
Name string `json:"name"`
// Config bytes (JSON) for the database
// See relevant (pebbledb, leveldb) config options
Config []byte `json:"-"`
}
// initializeDBs initializes the databases used by the VM.
// If [useStandaloneDB] is true, the chain will use a standalone database for its state.
// Otherwise, the chain will use the provided [avaDB] for its state.
func (vm *VM) initializeDBs(avaDB luxinterfaces.Database) error {
db := avaDB
// skip standalone database initialization if we are running in unit tests
if vm.ctx.NetworkID != constants.UnitTestID {
// first initialize the accepted block database to check if we need to use a standalone database
acceptedDB := interfaces.NewPrefixDB(acceptedPrefix, avaDB)
useStandAloneDB, err := vm.useStandaloneDatabase(acceptedDB)
if err != nil {
return err
}
if useStandAloneDB {
// If we are using a standalone database, we need to create a new database
// for the chain state.
dbConfig, err := getDatabaseConfig(vm.config, vm.ctx.ChainDataDir)
if err != nil {
return err
}
log.Info("Using standalone database for the chain state", "DatabaseConfig", dbConfig)
db, err = newStandaloneDatabase(dbConfig, vm.ctx.Metrics, vm.ctx.Log)
if err != nil {
return err
}
vm.usingStandaloneDB = true
}
}
// Use NewNested rather than New so that the structure of the database
// remains the same regardless of the provided baseDB type.
vm.chaindb = rawdb.NewDatabase(database.WrapDatabase(interfaces.NewPrefixDBNested(ethDBPrefix, db)))
vm.versiondb = interfaces.NewVersionDB(db)
vm.acceptedBlockDB = interfaces.NewPrefixDB(acceptedPrefix, vm.versiondb)
vm.metadataDB = interfaces.NewPrefixDB(metadataPrefix, vm.versiondb)
vm.db = db
// Note warpDB and validatorsDB are not part of versiondb because it is not necessary
// that they are committed to the database atomically with
// the last accepted block.
// [warpDB] is used to store warp message signatures
// set to a prefixDB with the prefix [warpPrefix]
vm.warpDB = interfaces.NewPrefixDB(warpPrefix, db)
// [validatorsDB] is used to store the current validator set and uptimes
// set to a prefixDB with the prefix [validatorsDBPrefix]
vm.validatorsDB = interfaces.NewPrefixDB(validatorsDBPrefix, db)
return nil
}
func (vm *VM) inspectDatabases() error {
start := time.Now()
log.Info("Starting database inspection")
if err := rawdb.InspectDatabase(vm.chaindb, nil, nil); err != nil {
return err
}
if err := inspectDB(vm.acceptedBlockDB, "acceptedBlockDB"); err != nil {
return err
}
if err := inspectDB(vm.metadataDB, "metadataDB"); err != nil {
return err
}
if err := inspectDB(vm.warpDB, "warpDB"); err != nil {
return err
}
if err := inspectDB(vm.validatorsDB, "validatorsDB"); err != nil {
return err
}
log.Info("Completed database inspection", "elapsed", time.Since(start))
return nil
}
// useStandaloneDatabase returns true if the chain can and should use a standalone database
// other than given by [db] in Initialize()
func (vm *VM) useStandaloneDatabase(acceptedDB luxinterfaces.Database) (bool, error) {
// no config provided, use default
standaloneDBFlag := vm.config.UseStandaloneDatabase
if standaloneDBFlag != nil {
return standaloneDBFlag.Bool(), nil
}
// check if the chain can use a standalone database
_, err := acceptedDB.Get(lastAcceptedKey)
if err == luxdatabase.ErrNotFound {
// If there is nothing in the database, we can use the standalone database
return true, nil
}
return false, err
}
// getDatabaseConfig returns the database configuration for the chain
// to be used by separate, standalone database.
func getDatabaseConfig(config config.Config, chainDataDir string) (DatabaseConfig, error) {
var (
configBytes []byte
err error
)
if len(config.DatabaseConfigContent) != 0 {
dbConfigContent := config.DatabaseConfigContent
configBytes, err = base64.StdEncoding.DecodeString(dbConfigContent)
if err != nil {
return DatabaseConfig{}, fmt.Errorf("unable to decode base64 content: %w", err)
}
} else if len(config.DatabaseConfigFile) != 0 {
configPath := config.DatabaseConfigFile
configBytes, err = os.ReadFile(configPath)
if err != nil {
return DatabaseConfig{}, err
}
}
dbPath := filepath.Join(chainDataDir, "db")
if len(config.DatabasePath) != 0 {
dbPath = config.DatabasePath
}
return DatabaseConfig{
Name: config.DatabaseType,
ReadOnly: config.DatabaseReadOnly,
Path: dbPath,
Config: configBytes,
}, nil
}
func inspectDB(db luxinterfaces.Database, label string) error {
it := db.NewIterator()
defer it.Release()
var (
count int64
start = time.Now()
logged = time.Now()
// Totals
total common.StorageSize
)
// Inspect key-value database first.
for it.Next() {
var (
key = it.Key()
size = common.StorageSize(len(key) + len(it.Value()))
)
total += size
count++
if count%1000 == 0 && time.Since(logged) > 8*time.Second {
log.Info("Inspecting database", "label", label, "count", count, "elapsed", common.PrettyDuration(time.Since(start)))
logged = time.Now()
}
}
// Display the database statistic.
log.Info("Database statistics", "label", label, "total", total.String(), "count", count)
return nil
}
func newStandaloneDatabase(dbConfig DatabaseConfig, gatherer metrics.MultiGatherer, logger logging.Logger) (luxinterfaces.Database, error) {
dbPath := filepath.Join(dbConfig.Path, dbConfig.Name)
dbConfigBytes := dbConfig.Config
// If the database is pebble, we need to set the config
// to use no sync. Sync mode in pebble has an issue with OSs like MacOS.
if dbConfig.Name == pebbledb.Name {
cfg := pebbledb.DefaultConfig
// Default to "no sync" for pebble db
cfg.Sync = false
if len(dbConfigBytes) > 0 {
if err := json.Unmarshal(dbConfigBytes, &cfg); err != nil {
return nil, err
}
}
var err error
// Marshal the config back to bytes to ensure that new defaults are applied
dbConfigBytes, err = json.Marshal(cfg)
if err != nil {
return nil, err
}
}
var db luxinterfaces.Database
switch dbConfig.Name {
case pebbledb.Name:
db, err = pebbledb.New(dbPath, dbConfigBytes, logger, gatherer)
case leveldb.Name:
db, err = leveldb.New(dbPath, dbConfigBytes, logger, gatherer)
default:
return nil, fmt.Errorf("unknown database type: %s", dbConfig.Name)
}
if err != nil {
return nil, fmt.Errorf("couldn't create database: %w", err)
}
return db, nil
}
File diff suppressed because it is too large Load Diff
-387
View File
@@ -1,387 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"testing"
"time"
"github.com/luxfi/node/consensus"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/utils/constants"
"github.com/luxfi/node/vms/components/chain"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/metrics"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/precompile/contracts/txallowlist"
"github.com/luxfi/evm/utils"
"github.com/luxfi/evm/vmerrs"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var DefaultEtnaTime = uint64(upgrade.GetConfig(testNetworkID).EtnaTime.Unix())
func TestVMUpgradeBytesPrecompile(t *testing.T) {
// Make a TxAllowListConfig upgrade at genesis and convert it to JSON to apply as upgradeBytes.
enableAllowListTimestamp := upgrade.InitiallyActiveTime // enable at initial time
upgradeConfig := &extras.UpgradeConfig{
PrecompileUpgrades: []extras.PrecompileUpgrade{
{
Config: txallowlist.NewConfig(utils.TimeToNewUint64(enableAllowListTimestamp), testEthAddrs[0:1], nil, nil),
},
},
}
upgradeBytesJSON, err := json.Marshal(upgradeConfig)
if err != nil {
t.Fatalf("could not marshal upgradeConfig to json: %s", err)
}
// initialize the VM with these upgrade bytes
issuer, vm, dbManager, appSender := GenesisVM(t, true, genesisJSONEVM, "", string(upgradeBytesJSON))
vm.clock.Set(enableAllowListTimestamp)
// Submit a successful transaction
tx0 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0])
assert.NoError(t, err)
errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
// Submit a rejected transaction, should throw an error
tx1 := types.NewTransaction(uint64(0), testEthAddrs[1], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx1, err := types.SignTx(tx1, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[1])
if err != nil {
t.Fatal(err)
}
errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; !errors.Is(err, vmerrors.ErrSenderAddressNotAllowListed) {
t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err)
}
// shutdown the vm
if err := vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
// prepare the new upgrade bytes to disable the TxAllowList
disableAllowListTimestamp := vm.clock.Time().Add(10 * time.Hour) // arbitrary choice
upgradeConfig.PrecompileUpgrades = append(
upgradeConfig.PrecompileUpgrades,
extras.PrecompileUpgrade{
Config: txallowlist.NewDisableConfig(utils.TimeToNewUint64(disableAllowListTimestamp)),
},
)
upgradeBytesJSON, err = json.Marshal(upgradeConfig)
if err != nil {
t.Fatalf("could not marshal upgradeConfig to json: %s", err)
}
// restart the vm
// Reset metrics to allow re-initialization
vm.ctx.Metrics = metrics.NewPrefixGatherer()
if err := vm.Initialize(
context.Background(), vm.ctx, dbManager, []byte(genesisJSONEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender,
); err != nil {
t.Fatal(err)
}
defer func() {
if err := vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
// Set the VM's state to NormalOp to initialize the tx pool.
if err := vm.SetState(context.Background(), consensus.Bootstrapping); err != nil {
t.Fatal(err)
}
if err := vm.SetState(context.Background(), consensus.NormalOp); err != nil {
t.Fatal(err)
}
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
vm.clock.Set(disableAllowListTimestamp)
// Make a block, previous rules still apply (TxAllowList is active)
// Submit a successful transaction
errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
// Submit a rejected transaction, should throw an error
errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; !errors.Is(err, vmerrors.ErrSenderAddressNotAllowListed) {
t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err)
}
blk := issueAndAccept(t, issuer, vm)
// Verify that the constructed block only has the whitelisted tx
block := blk.(*chain.BlockWrapper).Block.(*Block).ethBlock
txs := block.Transactions()
if txs.Len() != 1 {
t.Fatalf("Expected number of txs to be %d, but found %d", 1, txs.Len())
}
assert.Equal(t, signedTx0.Hash(), txs[0].Hash())
// verify the issued block is after the network upgrade
assert.GreaterOrEqual(t, int64(block.Time()), disableAllowListTimestamp.Unix())
<-newTxPoolHeadChan // wait for new head in tx pool
// retry the rejected Tx, which should now succeed
errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
vm.clock.Set(vm.clock.Time().Add(2 * time.Second)) // add 2 seconds for gas fee to adjust
blk = issueAndAccept(t, issuer, vm)
// Verify that the constructed block only has the previously rejected tx
block = blk.(*chain.BlockWrapper).Block.(*Block).ethBlock
txs = block.Transactions()
if txs.Len() != 1 {
t.Fatalf("Expected number of txs to be %d, but found %d", 1, txs.Len())
}
assert.Equal(t, signedTx1.Hash(), txs[0].Hash())
}
func TestNetworkUpgradesOverriden(t *testing.T) {
var genesis core.Genesis
if err := json.Unmarshal([]byte(genesisJSONPreEVM), &genesis); err != nil {
t.Fatalf("could not unmarshal genesis bytes: %s", err)
}
genesisBytes, err := json.Marshal(&genesis)
if err != nil {
t.Fatalf("could not unmarshal genesis bytes: %s", err)
}
upgradeBytesJSON := `{
"networkUpgradeOverrides": {
"evmTimestamp": 2,
"durangoTimestamp": 1607144402
}
}`
vm := &VM{}
ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, string(genesisBytes))
appSender := &enginetest.Sender{T: t}
appSender.CantSendAppGossip = true
appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil }
err = vm.Initialize(
context.Background(),
ctx,
dbManager,
genesisBytes,
[]byte(upgradeBytesJSON),
nil,
issuer,
[]*commonEng.Fx{},
appSender,
)
require.NoError(t, err, "error initializing GenesisVM")
require.NoError(t, vm.SetState(context.Background(), consensus.Bootstrapping))
require.NoError(t, vm.SetState(context.Background(), consensus.NormalOp))
defer func() {
if err := vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
// verify upgrade overrides
require.False(t, vm.chainConfigExtra().IsEVM(0))
require.True(t, vm.chainConfigExtra().IsEVM(2))
require.False(t, vm.chainConfigExtra().IsDurango(0))
require.False(t, vm.chainConfigExtra().IsDurango(uint64(upgrade.InitiallyActiveTime.Unix())))
require.True(t, vm.chainConfigExtra().IsDurango(1607144402))
}
func mustMarshal(t *testing.T, v interface{}) string {
b, err := json.Marshal(v)
require.NoError(t, err)
return string(b)
}
func TestVMStateUpgrade(t *testing.T) {
// modify genesis to add a key to the state
genesis := &core.Genesis{}
err := json.Unmarshal([]byte(genesisJSONEVM), genesis)
require.NoError(t, err)
genesisAccount, ok := genesis.Alloc[testEthAddrs[0]]
require.True(t, ok)
storageKey := common.HexToHash("0x1234")
genesisAccount.Storage = map[common.Hash]common.Hash{storageKey: common.HexToHash("0x5555")}
genesisCode, err := hexutil.Decode("0xabcd")
require.NoError(t, err)
genesisAccount.Code = genesisCode
genesisAccount.Nonce = 2 // set to a non-zero value to test that it is preserved
genesis.Alloc[testEthAddrs[0]] = genesisAccount // have to assign this back to the map for changes to take effect.
genesisStr := mustMarshal(t, genesis)
upgradedCodeStr := "0xdeadbeef" // this code will be applied during the upgrade
upgradedCode, err := hexutil.Decode(upgradedCodeStr)
// This modification will be applied to an existing account
genesisAccountUpgrade := &extras.StateUpgradeAccount{
BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)),
Storage: map[common.Hash]common.Hash{storageKey: {}},
Code: upgradedCode,
}
// This modification will be applied to a new account
newAccount := common.Address{42}
require.NoError(t, err)
newAccountUpgrade := &extras.StateUpgradeAccount{
BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)),
Storage: map[common.Hash]common.Hash{storageKey: common.HexToHash("0x6666")},
Code: upgradedCode,
}
upgradeTimestamp := upgrade.InitiallyActiveTime.Add(10 * time.Hour)
upgradeBytesJSON := fmt.Sprintf(
`{
"stateUpgrades": [
{
"blockTimestamp": %d,
"accounts": {
"%s": %s,
"%s": %s
}
}
]
}`,
upgradeTimestamp.Unix(),
testEthAddrs[0].Hex(),
mustMarshal(t, genesisAccountUpgrade),
newAccount.Hex(),
mustMarshal(t, newAccountUpgrade),
)
require.Contains(t, upgradeBytesJSON, upgradedCodeStr)
// initialize the VM with these upgrade bytes
issuer, vm, _, _ := GenesisVM(t, true, genesisStr, "", upgradeBytesJSON)
defer func() { require.NoError(t, vm.Shutdown(context.Background())) }()
// Verify the new account doesn't exist yet
genesisState, err := vm.blockChain.State()
require.NoError(t, err)
require.Equal(t, common.U2560, genesisState.GetBalance(newAccount))
// Advance the chain to the upgrade time
vm.clock.Set(upgradeTimestamp)
// Submit a successful (unrelated) transaction, so we can build a block
// in this tx, testEthAddrs[1] sends 1 wei to itself.
tx0 := types.NewTransaction(uint64(0), testEthAddrs[1], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[1])
require.NoError(t, err)
errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
require.NoError(t, errs[0], "Failed to add tx")
blk := issueAndAccept(t, issuer, vm)
require.NotNil(t, blk)
require.EqualValues(t, 1, blk.Height())
// Verify the state upgrade was applied
state, err := vm.blockChain.State()
require.NoError(t, err)
// Existing account
expectedGenesisAccountBalance := uint256.MustFromBig(
new(big.Int).Add(
genesisAccount.Balance,
(*big.Int)(genesisAccountUpgrade.BalanceChange),
),
)
require.Equal(t, state.GetBalance(testEthAddrs[0]), expectedGenesisAccountBalance)
require.Equal(t, state.GetState(testEthAddrs[0], storageKey), genesisAccountUpgrade.Storage[storageKey])
require.Equal(t, state.GetCode(testEthAddrs[0]), upgradedCode)
require.Equal(t, state.GetCodeHash(testEthAddrs[0]), crypto.Keccak256Hash(upgradedCode))
require.Equal(t, state.GetNonce(testEthAddrs[0]), genesisAccount.Nonce) // Nonce should be preserved since it was non-zero
// New account
expectedNewAccountBalance := uint256.MustFromBig((*big.Int)(newAccountUpgrade.BalanceChange))
require.Equal(t, state.GetBalance(newAccount), expectedNewAccountBalance)
require.Equal(t, state.GetCode(newAccount), upgradedCode)
require.Equal(t, state.GetCodeHash(newAccount), crypto.Keccak256Hash(upgradedCode))
require.Equal(t, state.GetNonce(newAccount), uint64(1)) // Nonce should be set to 1 when code is set if nonce was 0
require.Equal(t, state.GetState(newAccount, storageKey), newAccountUpgrade.Storage[storageKey])
}
func TestVMEupgradeActivatesCancun(t *testing.T) {
tests := []struct {
name string
genesisJSON string
upgradeJSON string
check func(*testing.T, *VM) // function to check the VM state
}{
{
name: "Etna activates Cancun",
genesisJSON: genesisJSONEtna,
check: func(t *testing.T, vm *VM) {
require.True(t, vm.chainConfig.IsCancun(common.Big0, DefaultEtnaTime))
},
},
{
name: "Later Etna activates Cancun",
genesisJSON: genesisJSONDurango,
upgradeJSON: func() string {
upgrade := &extras.UpgradeConfig{
NetworkUpgradeOverrides: &extras.NetworkUpgrades{
EtnaTimestamp: utils.NewUint64(DefaultEtnaTime + 2),
},
}
b, err := json.Marshal(upgrade)
require.NoError(t, err)
return string(b)
}(),
check: func(t *testing.T, vm *VM) {
require.False(t, vm.chainConfig.IsCancun(common.Big0, DefaultEtnaTime))
require.True(t, vm.chainConfig.IsCancun(common.Big0, DefaultEtnaTime+2))
},
},
{
name: "Changed Etna changes Cancun",
genesisJSON: genesisJSONEtna,
upgradeJSON: func() string {
upgrade := &extras.UpgradeConfig{
NetworkUpgradeOverrides: &extras.NetworkUpgrades{
EtnaTimestamp: utils.NewUint64(DefaultEtnaTime + 2),
},
}
b, err := json.Marshal(upgrade)
require.NoError(t, err)
return string(b)
}(),
check: func(t *testing.T, vm *VM) {
require.False(t, vm.chainConfig.IsCancun(common.Big0, DefaultEtnaTime))
require.True(t, vm.chainConfig.IsCancun(common.Big0, DefaultEtnaTime+2))
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, vm, _, _ := GenesisVM(t, true, test.genesisJSON, "", test.upgradeJSON)
defer func() { require.NoError(t, vm.Shutdown(context.Background())) }()
test.check(t, vm)
})
}
}
-161
View File
@@ -1,161 +0,0 @@
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"testing"
"time"
"github.com/luxfi/node/database"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus"
commonEng "github.com/luxfi/node/consensus/engine/core"
"github.com/luxfi/node/consensus/engine/enginetest"
avagovalidators "github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/consensus/validators/validatorstest"
"github.com/ethereum/go-ethereum/core"
"github.com/luxfi/evm/plugin/evm/validators"
"github.com/luxfi/evm/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidatorState(t *testing.T) {
require := require.New(t)
genesis := &core.Genesis{}
require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONLatest)))
genesisJSON, err := genesis.MarshalJSON()
require.NoError(err)
vm := &VM{}
ctx, dbManager, genesisBytes, issuer, _ := setupGenesis(t, string(genesisJSON))
appSender := &enginetest.Sender{T: t}
appSender.CantSendAppGossip = true
testNodeIDs := []ids.NodeID{
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
ids.GenerateTestNodeID(),
}
testValidationIDs := []ids.ID{
ids.GenerateTestID(),
ids.GenerateTestID(),
ids.GenerateTestID(),
}
ctx.ValidatorState = &validatorstest.State{
GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagovalidators.GetCurrentValidatorOutput, uint64, error) {
return map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
PublicKey: nil,
Weight: 1,
},
testValidationIDs[1]: {
NodeID: testNodeIDs[1],
PublicKey: nil,
Weight: 1,
},
testValidationIDs[2]: {
NodeID: testNodeIDs[2],
PublicKey: nil,
Weight: 1,
},
}, 0, nil
},
}
appSender.SendAppGossipF = func(context.Context, commonEng.SendConfig, []byte) error { return nil }
err = vm.Initialize(
context.Background(),
ctx,
dbManager,
genesisBytes,
[]byte(""),
[]byte(""),
issuer,
[]*commonEng.Fx{},
appSender,
)
require.NoError(err, "error initializing GenesisVM")
// Test case 1: state should not be populated until bootstrapped
require.NoError(vm.SetState(context.Background(), consensus.Bootstrapping))
require.Equal(0, vm.validatorsManager.GetValidationIDs().Len())
_, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0])
require.ErrorIs(database.ErrNotFound, err)
require.False(vm.validatorsManager.StartedTracking())
// Test case 2: state should be populated after bootstrapped
require.NoError(vm.SetState(context.Background(), consensus.NormalOp))
require.Len(vm.validatorsManager.GetValidationIDs(), 3)
_, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0])
require.NoError(err)
require.True(vm.validatorsManager.StartedTracking())
// Test case 3: restarting VM should not lose state
vm.Shutdown(context.Background())
// Shutdown should stop tracking
require.False(vm.validatorsManager.StartedTracking())
vm = &VM{}
err = vm.Initialize(
context.Background(),
utils.TestConsensusContext(), // this context does not have validators state, making VM to source it from the database
dbManager,
genesisBytes,
[]byte(""),
[]byte(""),
issuer,
[]*commonEng.Fx{},
appSender,
)
require.NoError(err, "error initializing GenesisVM")
require.Len(vm.validatorsManager.GetValidationIDs(), 3)
_, _, err = vm.validatorsManager.CalculateUptime(testNodeIDs[0])
require.NoError(err)
require.False(vm.validatorsManager.StartedTracking())
// Test case 4: new validators should be added to the state
newValidationID := ids.GenerateTestID()
newNodeID := ids.GenerateTestNodeID()
testState := &validatorstest.State{
GetCurrentValidatorSetF: func(ctx context.Context, subnetID ids.ID) (map[ids.ID]*avagovalidators.GetCurrentValidatorOutput, uint64, error) {
return map[ids.ID]*avagovalidators.GetCurrentValidatorOutput{
testValidationIDs[0]: {
NodeID: testNodeIDs[0],
PublicKey: nil,
Weight: 1,
},
testValidationIDs[1]: {
NodeID: testNodeIDs[1],
PublicKey: nil,
Weight: 1,
},
testValidationIDs[2]: {
NodeID: testNodeIDs[2],
PublicKey: nil,
Weight: 1,
},
newValidationID: {
NodeID: newNodeID,
PublicKey: nil,
Weight: 1,
},
}, 0, nil
},
}
// set VM as bootstrapped
require.NoError(vm.SetState(context.Background(), consensus.Bootstrapping))
require.NoError(vm.SetState(context.Background(), consensus.NormalOp))
vm.ctx.ValidatorState = testState
// new validator should be added to the state eventually after SyncFrequency
require.EventuallyWithT(func(c *assert.CollectT) {
vm.vmLock.Lock()
defer vm.vmLock.Unlock()
assert.Len(c, vm.validatorsManager.GetNodeIDs(), 4)
newValidator, err := vm.validatorsManager.GetValidator(newValidationID)
assert.NoError(c, err)
assert.Equal(c, newNodeID, newValidator.NodeID)
}, validators.SyncFrequency*2, 5*time.Second)
}
-907
View File
@@ -1,907 +0,0 @@
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"encoding/json"
"errors"
"math/big"
"testing"
"time"
_ "embed"
"github.com/luxfi/node/ids"
"github.com/luxfi/node/consensus/choices"
"github.com/luxfi/node/consensus/engine/linear/block"
"github.com/luxfi/node/consensus/validators"
"github.com/luxfi/node/utils"
"github.com/luxfi/node/utils/crypto/bls"
"github.com/luxfi/node/utils/set"
"github.com/luxfi/node/vms/components/chain"
luxWarp "github.com/luxfi/node/vms/platformvm/warp"
"github.com/luxfi/node/vms/platformvm/warp/payload"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/luxfi/evm/geth/eth/tracers"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/plugin/evm/message"
"github.com/luxfi/evm/precompile/contract"
"github.com/luxfi/evm/predicate"
evmUtils "github.com/luxfi/evm/utils"
"github.com/luxfi/evm/x/warp"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
var (
//go:embed ExampleWarp.bin
exampleWarpBin string
//go:embed ExampleWarp.abi
exampleWarpABI string
)
type warpMsgFrom int
const (
fromSubnet warpMsgFrom = iota
fromPrimary
)
type useWarpMsgSigners int
const (
signersSubnet useWarpMsgSigners = iota
signersPrimary
)
func TestSendWarpMessage(t *testing.T) {
require := require.New(t)
genesis := &core.Genesis{}
require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONDurango)))
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)),
}
genesisJSON, err := genesis.MarshalJSON()
require.NoError(err)
issuer, vm, _, _ := GenesisVM(t, true, string(genesisJSON), "", "")
defer func() {
require.NoError(vm.Shutdown(context.Background()))
}()
acceptedLogsChan := make(chan []*types.Log, 10)
logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan)
defer logsSub.Unsubscribe()
payloadData := avagoUtils.RandomBytes(100)
warpSendMessageInput, err := warpcontract.PackSendWarpMessage(payloadData)
require.NoError(err)
addressedPayload, err := payload.NewAddressedCall(
testEthAddrs[0].Bytes(),
payloadData,
)
require.NoError(err)
expectedUnsignedMessage, err := luxWarp.NewUnsignedMessage(
vm.ctx.NetworkID,
vm.ctx.ChainID,
addressedPayload.Bytes(),
)
require.NoError(err)
// Submit a transaction to trigger sending a warp message
tx0 := types.NewTransaction(uint64(0), warpcontract.ContractAddress, big.NewInt(1), 100_000, big.NewInt(testMinGasPrice), warpSendMessageInput)
signedTx0, err := types.SignTx(tx0, types.LatestSignerForChainID(vm.chainConfig.ChainID), testKeys[0])
require.NoError(err)
errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
require.NoError(errs[0])
<-issuer
blk, err := vm.BuildBlock(context.Background())
require.NoError(err)
require.NoError(blk.Verify(context.Background()))
// Verify that the constructed block contains the expected log with an unsigned warp message in the log data
ethBlock1 := blk.(*chain.BlockWrapper).Block.(*Block).ethBlock
require.Len(ethBlock1.Transactions(), 1)
receipts := rawdb.ReadReceipts(vm.chaindb, ethBlock1.Hash(), ethBlock1.NumberU64(), ethBlock1.Time(), vm.chainConfig)
require.Len(receipts, 1)
require.Len(receipts[0].Logs, 1)
expectedTopics := []common.Hash{
warpcontract.WarpABI.Events["SendWarpMessage"].ID,
common.BytesToHash(testEthAddrs[0].Bytes()),
common.Hash(expectedUnsignedMessage.ID()),
}
require.Equal(expectedTopics, receipts[0].Logs[0].Topics)
logData := receipts[0].Logs[0].Data
unsignedMessage, err := warpcontract.UnpackSendWarpEventDataToMessage(logData)
require.NoError(err)
// Verify the signature cannot be fetched before the block is accepted
_, err = vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMessage)
require.Error(err)
_, err = vm.warpBackend.GetBlockSignature(context.TODO(), blk.ID())
require.Error(err)
require.NoError(vm.SetPreference(context.Background(), blk.ID()))
require.NoError(blk.Accept(context.Background()))
vm.blockChain.DrainAcceptorQueue()
// Verify the message signature after accepting the block.
rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMessage)
require.NoError(err)
blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes[:])
require.NoError(err)
select {
case acceptedLogs := <-acceptedLogsChan:
require.Len(acceptedLogs, 1, "unexpected length of accepted logs")
require.Equal(acceptedLogs[0], receipts[0].Logs[0])
case <-time.After(time.Second):
require.Fail("Failed to read accepted logs from subscription")
}
// Verify the produced message signature is valid
require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes()))
// Verify the blockID will now be signed by the backend and produces a valid signature.
rawSignatureBytes, err = vm.warpBackend.GetBlockSignature(context.TODO(), blk.ID())
require.NoError(err)
blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes[:])
require.NoError(err)
blockHashPayload, err := payload.NewHash(blk.ID())
require.NoError(err)
unsignedMessage, err = luxWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, blockHashPayload.Bytes())
require.NoError(err)
// Verify the produced message signature is valid
require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes()))
}
func TestValidateWarpMessage(t *testing.T) {
require := require.New(t)
sourceChainID := ids.GenerateTestID()
sourceAddress := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2")
payloadData := []byte{1, 2, 3}
addressedPayload, err := payload.NewAddressedCall(
sourceAddress.Bytes(),
payloadData,
)
require.NoError(err)
unsignedMessage, err := luxWarp.NewUnsignedMessage(testNetworkID, sourceChainID, addressedPayload.Bytes())
require.NoError(err)
exampleWarpABI := contract.ParseABI(exampleWarpABI)
exampleWarpPayload, err := exampleWarpABI.Pack(
"validateWarpMessage",
uint32(0),
sourceChainID,
sourceAddress,
payloadData,
)
require.NoError(err)
testWarpVMTransaction(t, unsignedMessage, true, exampleWarpPayload)
}
func TestValidateInvalidWarpMessage(t *testing.T) {
require := require.New(t)
sourceChainID := ids.GenerateTestID()
sourceAddress := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2")
payloadData := []byte{1, 2, 3}
addressedPayload, err := payload.NewAddressedCall(
sourceAddress.Bytes(),
payloadData,
)
require.NoError(err)
unsignedMessage, err := luxWarp.NewUnsignedMessage(testNetworkID, sourceChainID, addressedPayload.Bytes())
require.NoError(err)
exampleWarpABI := contract.ParseABI(exampleWarpABI)
exampleWarpPayload, err := exampleWarpABI.Pack(
"validateInvalidWarpMessage",
uint32(0),
)
require.NoError(err)
testWarpVMTransaction(t, unsignedMessage, false, exampleWarpPayload)
}
func TestValidateWarpBlockHash(t *testing.T) {
require := require.New(t)
sourceChainID := ids.GenerateTestID()
blockHash := ids.GenerateTestID()
blockHashPayload, err := payload.NewHash(blockHash)
require.NoError(err)
unsignedMessage, err := luxWarp.NewUnsignedMessage(testNetworkID, sourceChainID, blockHashPayload.Bytes())
require.NoError(err)
exampleWarpABI := contract.ParseABI(exampleWarpABI)
exampleWarpPayload, err := exampleWarpABI.Pack(
"validateWarpBlockHash",
uint32(0),
sourceChainID,
blockHash,
)
require.NoError(err)
testWarpVMTransaction(t, unsignedMessage, true, exampleWarpPayload)
}
func TestValidateInvalidWarpBlockHash(t *testing.T) {
require := require.New(t)
sourceChainID := ids.GenerateTestID()
blockHash := ids.GenerateTestID()
blockHashPayload, err := payload.NewHash(blockHash)
require.NoError(err)
unsignedMessage, err := luxWarp.NewUnsignedMessage(testNetworkID, sourceChainID, blockHashPayload.Bytes())
require.NoError(err)
exampleWarpABI := contract.ParseABI(exampleWarpABI)
exampleWarpPayload, err := exampleWarpABI.Pack(
"validateInvalidWarpBlockHash",
uint32(0),
)
require.NoError(err)
testWarpVMTransaction(t, unsignedMessage, false, exampleWarpPayload)
}
func testWarpVMTransaction(t *testing.T, unsignedMessage *luxWarp.UnsignedMessage, validSignature bool, txPayload []byte) {
require := require.New(t)
genesis := &core.Genesis{}
require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONDurango)))
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)),
}
genesisJSON, err := genesis.MarshalJSON()
require.NoError(err)
issuer, vm, _, _ := GenesisVM(t, true, string(genesisJSON), "", "")
defer func() {
require.NoError(vm.Shutdown(context.Background()))
}()
acceptedLogsChan := make(chan []*types.Log, 10)
logsSub := vm.eth.APIBackend.SubscribeAcceptedLogsEvent(acceptedLogsChan)
defer logsSub.Unsubscribe()
nodeID1 := ids.GenerateTestNodeID()
blsSecretKey1, err := localsigner.New()
require.NoError(err)
blsPublicKey1 := blsSecretKey1.PublicKey()
blsSignature1, err := blsSecretKey1.Sign(unsignedMessage.Bytes())
require.NoError(err)
nodeID2 := ids.GenerateTestNodeID()
blsSecretKey2, err := localsigner.New()
require.NoError(err)
blsPublicKey2 := blsSecretKey2.PublicKey()
blsSignature2, err := blsSecretKey2.Sign(unsignedMessage.Bytes())
require.NoError(err)
blsAggregatedSignature, err := bls.AggregateSignatures([]*bls.Signature{blsSignature1, blsSignature2})
require.NoError(err)
minimumValidPChainHeight := uint64(10)
getValidatorSetTestErr := errors.New("can't get validator set test error")
vm.ctx.ValidatorState = &validatorstest.State{
// TODO: test both Primary Network / C-Chain and non-Primary Network
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
return ids.Empty, nil
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if height < minimumValidPChainHeight {
return nil, getValidatorSetTestErr
}
return map[ids.NodeID]*validators.GetValidatorOutput{
nodeID1: {
NodeID: nodeID1,
PublicKey: blsPublicKey1,
Weight: 50,
},
nodeID2: {
NodeID: nodeID2,
PublicKey: blsPublicKey2,
Weight: 50,
},
}, nil
},
}
signersBitSet := set.NewBits()
signersBitSet.Add(0)
signersBitSet.Add(1)
warpSignature := &luxWarp.BitSetSignature{
Signers: signersBitSet.Bytes(),
}
blsAggregatedSignatureBytes := bls.SignatureToBytes(blsAggregatedSignature)
copy(warpSignature.Signature[:], blsAggregatedSignatureBytes)
signedMessage, err := luxWarp.NewMessage(
unsignedMessage,
warpSignature,
)
require.NoError(err)
createTx, err := types.SignTx(
types.NewContractCreation(0, common.Big0, 7_000_000, big.NewInt(225*utils.GWei), common.Hex2Bytes(exampleWarpBin)),
types.LatestSignerForChainID(vm.chainConfig.ChainID),
testKeys[0],
)
require.NoError(err)
exampleWarpAddress := crypto.CreateAddress(testEthAddrs[0], 0)
tx, err := types.SignTx(
predicate.NewPredicateTx(
vm.chainConfig.ChainID,
1,
&exampleWarpAddress,
1_000_000,
big.NewInt(225*utils.GWei),
big.NewInt(utils.GWei),
common.Big0,
txPayload,
types.AccessList{},
warpcontract.ContractAddress,
signedMessage.Bytes(),
),
types.LatestSignerForChainID(vm.chainConfig.ChainID),
testKeys[0],
)
require.NoError(err)
errs := vm.txPool.AddRemotesSync([]*types.Transaction{createTx, tx})
for i, err := range errs {
require.NoError(err, "failed to add tx at index %d", i)
}
// If [validSignature] set the signature to be considered valid at the verified height.
blockCtx := &block.Context{
PChainHeight: minimumValidPChainHeight - 1,
}
if validSignature {
blockCtx.PChainHeight = minimumValidPChainHeight
}
vm.clock.Set(vm.clock.Time().Add(2 * time.Second))
<-issuer
warpBlock, err := vm.BuildBlockWithContext(context.Background(), blockCtx)
require.NoError(err)
warpBlockVerifyWithCtx, ok := warpBlock.(block.WithVerifyContext)
require.True(ok)
shouldVerifyWithCtx, err := warpBlockVerifyWithCtx.ShouldVerifyWithContext(context.Background())
require.NoError(err)
require.True(shouldVerifyWithCtx)
require.NoError(warpBlockVerifyWithCtx.VerifyWithContext(context.Background(), blockCtx))
require.NoError(vm.SetPreference(context.Background(), warpBlock.ID()))
require.NoError(warpBlock.Accept(context.Background()))
vm.blockChain.DrainAcceptorQueue()
ethBlock := warpBlock.(*chain.BlockWrapper).Block.(*Block).ethBlock
verifiedMessageReceipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash())
require.Len(verifiedMessageReceipts, 2)
for i, receipt := range verifiedMessageReceipts {
require.Equal(types.ReceiptStatusSuccessful, receipt.Status, "index: %d", i)
}
tracerAPI := tracers.NewAPI(vm.eth.APIBackend)
txTraceResults, err := tracerAPI.TraceBlockByHash(context.Background(), ethBlock.Hash(), nil)
require.NoError(err)
require.Len(txTraceResults, 2)
blockTxTraceResultBytes, err := json.Marshal(txTraceResults[1].Result)
require.NoError(err)
unmarshalResults := make(map[string]interface{})
require.NoError(json.Unmarshal(blockTxTraceResultBytes, &unmarshalResults))
require.Equal("", unmarshalResults["returnValue"])
txTraceResult, err := tracerAPI.TraceTransaction(context.Background(), tx.Hash(), nil)
require.NoError(err)
txTraceResultBytes, err := json.Marshal(txTraceResult)
require.NoError(err)
require.JSONEq(string(txTraceResultBytes), string(blockTxTraceResultBytes))
}
func TestReceiveWarpMessage(t *testing.T) {
require := require.New(t)
genesis := &core.Genesis{}
require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONDurango)))
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
// Note that warp is enabled without RequirePrimaryNetworkSigners
// by default in the genesis configuration.
warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)),
}
genesisJSON, err := genesis.MarshalJSON()
require.NoError(err)
// disable warp so we can re-enable it with RequirePrimaryNetworkSigners
disableTime := upgrade.InitiallyActiveTime.Add(10 * time.Second)
disableConfig := warpcontract.NewDisableConfig(utils.TimeToNewUint64(disableTime))
// re-enable warp with RequirePrimaryNetworkSigners
reEnableTime := disableTime.Add(10 * time.Second)
reEnableConfig := warpcontract.NewConfig(
utils.TimeToNewUint64(reEnableTime),
0, // QuorumNumerator
true, // RequirePrimaryNetworkSigners
)
upgradeConfig := extras.UpgradeConfig{
PrecompileUpgrades: []extras.PrecompileUpgrade{
{Config: disableConfig},
{Config: reEnableConfig},
},
}
upgradeBytes, err := json.Marshal(upgradeConfig)
require.NoError(err)
issuer, vm, _, _ := GenesisVM(t, true, string(genesisJSON), "", string(upgradeBytes))
defer func() {
require.NoError(vm.Shutdown(context.Background()))
}()
type test struct {
name string
sourceChainID ids.ID
msgFrom warpMsgFrom
useSigners useWarpMsgSigners
blockTime time.Time
}
blockGap := 2 * time.Second // Build blocks with a gap. Blocks built too quickly will have high fees.
tests := []test{
{
name: "subnet message should be signed by subnet without RequirePrimaryNetworkSigners",
sourceChainID: vm.ctx.ChainID,
msgFrom: fromSubnet,
useSigners: signersSubnet,
blockTime: upgrade.InitiallyActiveTime,
},
{
name: "P-Chain message should be signed by subnet without RequirePrimaryNetworkSigners",
sourceChainID: constants.PlatformChainID,
msgFrom: fromPrimary,
useSigners: signersSubnet,
blockTime: upgrade.InitiallyActiveTime.Add(blockGap),
},
{
name: "C-Chain message should be signed by subnet without RequirePrimaryNetworkSigners",
sourceChainID: vm.ctx.CChainID,
msgFrom: fromPrimary,
useSigners: signersSubnet,
blockTime: upgrade.InitiallyActiveTime.Add(2 * blockGap),
},
// Note here we disable warp and re-enable it with RequirePrimaryNetworkSigners
// by using reEnableTime.
{
name: "subnet message should be signed by subnet with RequirePrimaryNetworkSigners (unimpacted)",
sourceChainID: vm.ctx.ChainID,
msgFrom: fromSubnet,
useSigners: signersSubnet,
blockTime: reEnableTime,
},
{
name: "P-Chain message should be signed by subnet with RequirePrimaryNetworkSigners (unimpacted)",
sourceChainID: constants.PlatformChainID,
msgFrom: fromPrimary,
useSigners: signersSubnet,
blockTime: reEnableTime.Add(blockGap),
},
{
name: "C-Chain message should be signed by primary with RequirePrimaryNetworkSigners (impacted)",
sourceChainID: vm.ctx.CChainID,
msgFrom: fromPrimary,
useSigners: signersPrimary,
blockTime: reEnableTime.Add(2 * blockGap),
},
}
// Note each test corresponds to a block, the tests must be ordered by block
// time and cannot, eg be run in parallel or a separate golang test.
for _, test := range tests {
testReceiveWarpMessage(
t, issuer, vm, test.sourceChainID, test.msgFrom, test.useSigners, test.blockTime,
)
}
}
func testReceiveWarpMessage(
t *testing.T, issuer chan commonEng.Message, vm *VM,
sourceChainID ids.ID,
msgFrom warpMsgFrom, useSigners useWarpMsgSigners,
blockTime time.Time,
) {
require := require.New(t)
payloadData := avagoUtils.RandomBytes(100)
addressedPayload, err := payload.NewAddressedCall(
testEthAddrs[0].Bytes(),
payloadData,
)
require.NoError(err)
unsignedMessage, err := luxWarp.NewUnsignedMessage(
vm.ctx.NetworkID,
sourceChainID,
addressedPayload.Bytes(),
)
require.NoError(err)
type signer struct {
networkID ids.ID
nodeID ids.NodeID
secret bls.Signer
signature *bls.Signature
weight uint64
}
newSigner := func(networkID ids.ID, weight uint64) signer {
secret, err := localsigner.New()
require.NoError(err)
sig, err := secret.Sign(unsignedMessage.Bytes())
require.NoError(err)
return signer{
networkID: networkID,
nodeID: ids.GenerateTestNodeID(),
secret: secret,
signature: sig,
weight: weight,
}
}
primarySigners := []signer{
newSigner(constants.PrimaryNetworkID, 50),
newSigner(constants.PrimaryNetworkID, 50),
}
subnetSigners := []signer{
newSigner(vm.ctx.SubnetID, 50),
newSigner(vm.ctx.SubnetID, 50),
}
signers := subnetSigners
if useSigners == signersPrimary {
signers = primarySigners
}
blsSignatures := make([]*bls.Signature, len(signers))
for i := range signers {
blsSignatures[i] = signers[i].signature
}
blsAggregatedSignature, err := bls.AggregateSignatures(blsSignatures)
require.NoError(err)
minimumValidPChainHeight := uint64(10)
getValidatorSetTestErr := errors.New("can't get validator set test error")
vm.ctx.ValidatorState = &validatorstest.State{
GetSubnetIDF: func(ctx context.Context, chainID ids.ID) (ids.ID, error) {
if msgFrom == fromPrimary {
return constants.PrimaryNetworkID, nil
}
return vm.ctx.SubnetID, nil
},
GetValidatorSetF: func(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
if height < minimumValidPChainHeight {
return nil, getValidatorSetTestErr
}
signers := subnetSigners
if subnetID == constants.PrimaryNetworkID {
signers = primarySigners
}
vdrOutput := make(map[ids.NodeID]*validators.GetValidatorOutput)
for _, s := range signers {
vdrOutput[s.nodeID] = &validators.GetValidatorOutput{
NodeID: s.nodeID,
PublicKey: s.secret.PublicKey(),
Weight: s.weight,
}
}
return vdrOutput, nil
},
}
signersBitSet := set.NewBits()
for i := range signers {
signersBitSet.Add(i)
}
warpSignature := &luxWarp.BitSetSignature{
Signers: signersBitSet.Bytes(),
}
blsAggregatedSignatureBytes := bls.SignatureToBytes(blsAggregatedSignature)
copy(warpSignature.Signature[:], blsAggregatedSignatureBytes)
signedMessage, err := luxWarp.NewMessage(
unsignedMessage,
warpSignature,
)
require.NoError(err)
getWarpMsgInput, err := warpcontract.PackGetVerifiedWarpMessage(0)
require.NoError(err)
getVerifiedWarpMessageTx, err := types.SignTx(
predicate.NewPredicateTx(
vm.chainConfig.ChainID,
vm.txPool.Nonce(testEthAddrs[0]),
&warpcontract.Module.Address,
1_000_000,
big.NewInt(225*utils.GWei),
big.NewInt(utils.GWei),
common.Big0,
getWarpMsgInput,
types.AccessList{},
warpcontract.ContractAddress,
signedMessage.Bytes(),
),
types.LatestSignerForChainID(vm.chainConfig.ChainID),
testKeys[0],
)
require.NoError(err)
errs := vm.txPool.AddRemotesSync([]*types.Transaction{getVerifiedWarpMessageTx})
for i, err := range errs {
require.NoError(err, "failed to add tx at index %d", i)
}
// Build, verify, and accept block with valid proposer context.
validProposerCtx := &block.Context{
PChainHeight: minimumValidPChainHeight,
}
vm.clock.Set(blockTime)
<-issuer
block2, err := vm.BuildBlockWithContext(context.Background(), validProposerCtx)
require.NoError(err)
// Require the block was built with a successful predicate result
ethBlock := block2.(*chain.BlockWrapper).Block.(*Block).ethBlock
headerPredicateResultsBytes := customheader.PredicateBytesFromExtra(ethBlock.Extra())
results, err := predicate.ParseResults(headerPredicateResultsBytes)
require.NoError(err)
// Predicate results encode the index of invalid warp messages in a bitset.
// An empty bitset indicates success.
txResultsBytes := results.GetResults(
getVerifiedWarpMessageTx.Hash(),
warpcontract.ContractAddress,
)
bitset := set.BitsFromBytes(txResultsBytes)
require.Zero(bitset.Len()) // Empty bitset indicates success
block2VerifyWithCtx, ok := block2.(block.WithVerifyContext)
require.True(ok)
shouldVerifyWithCtx, err := block2VerifyWithCtx.ShouldVerifyWithContext(context.Background())
require.NoError(err)
require.True(shouldVerifyWithCtx)
require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), validProposerCtx))
require.NoError(vm.SetPreference(context.Background(), block2.ID()))
// Verify the block with another valid context with identical predicate results
require.NoError(block2VerifyWithCtx.VerifyWithContext(context.Background(), &block.Context{
PChainHeight: minimumValidPChainHeight + 1,
}))
// Verify the block in a different context causing the warp message to fail verification changing
// the expected header predicate results.
require.ErrorIs(block2VerifyWithCtx.VerifyWithContext(context.Background(), &block.Context{
PChainHeight: minimumValidPChainHeight - 1,
}), errInvalidHeaderPredicateResults)
// Accept the block after performing multiple VerifyWithContext operations
require.NoError(block2.Accept(context.Background()))
vm.blockChain.DrainAcceptorQueue()
verifiedMessageReceipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash())
require.Len(verifiedMessageReceipts, 1)
verifiedMessageTxReceipt := verifiedMessageReceipts[0]
require.Equal(types.ReceiptStatusSuccessful, verifiedMessageTxReceipt.Status)
expectedOutput, err := warpcontract.PackGetVerifiedWarpMessageOutput(warpcontract.GetVerifiedWarpMessageOutput{
Message: warpcontract.WarpMessage{
SourceChainID: common.Hash(sourceChainID),
OriginSenderAddress: testEthAddrs[0],
Payload: payloadData,
},
Valid: true,
})
require.NoError(err)
tracerAPI := tracers.NewAPI(vm.eth.APIBackend)
txTraceResults, err := tracerAPI.TraceBlockByHash(context.Background(), ethBlock.Hash(), nil)
require.NoError(err)
require.Len(txTraceResults, 1)
blockTxTraceResultBytes, err := json.Marshal(txTraceResults[0].Result)
require.NoError(err)
unmarshalResults := make(map[string]interface{})
require.NoError(json.Unmarshal(blockTxTraceResultBytes, &unmarshalResults))
require.Equal(common.Bytes2Hex(expectedOutput), unmarshalResults["returnValue"])
txTraceResult, err := tracerAPI.TraceTransaction(context.Background(), getVerifiedWarpMessageTx.Hash(), nil)
require.NoError(err)
txTraceResultBytes, err := json.Marshal(txTraceResult)
require.NoError(err)
require.JSONEq(string(txTraceResultBytes), string(blockTxTraceResultBytes))
}
func TestMessageSignatureRequestsToVM(t *testing.T) {
_, vm, _, appSender := GenesisVM(t, true, genesisJSONEVM, "", "")
defer func() {
err := vm.Shutdown(context.Background())
require.NoError(t, err)
}()
// Generate a new warp unsigned message and add to warp backend
warpMessage, err := luxWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, []byte{1, 2, 3})
require.NoError(t, err)
// Add the known message and get its signature to confirm.
err = vm.warpBackend.AddMessage(warpMessage)
require.NoError(t, err)
signature, err := vm.warpBackend.GetMessageSignature(context.TODO(), warpMessage)
require.NoError(t, err)
var knownSignature [bls.SignatureLen]byte
copy(knownSignature[:], signature)
tests := map[string]struct {
messageID ids.ID
expectedResponse [bls.SignatureLen]byte
}{
"known": {
messageID: warpMessage.ID(),
expectedResponse: knownSignature,
},
"unknown": {
messageID: ids.GenerateTestID(),
expectedResponse: [bls.SignatureLen]byte{},
},
}
for name, test := range tests {
calledSendAppResponseFn := false
appSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
calledSendAppResponseFn = true
var response message.SignatureResponse
_, err := message.Codec.Unmarshal(responseBytes, &response)
require.NoError(t, err)
require.Equal(t, test.expectedResponse, response.Signature)
return nil
}
t.Run(name, func(t *testing.T) {
var signatureRequest message.Request = message.MessageSignatureRequest{
MessageID: test.messageID,
}
requestBytes, err := message.Codec.Marshal(message.Version, &signatureRequest)
require.NoError(t, err)
// Send the app request and make sure we called SendAppResponseFn
deadline := time.Now().Add(60 * time.Second)
require.NoError(t, vm.Network.AppRequest(context.Background(), ids.GenerateTestNodeID(), peertest.TestPeerRequestID, deadline, requestBytes))
require.True(t, calledSendAppResponseFn)
})
}
}
func TestBlockSignatureRequestsToVM(t *testing.T) {
_, vm, _, appSender := GenesisVM(t, true, genesisJSONEVM, "", "")
defer func() {
err := vm.Shutdown(context.Background())
require.NoError(t, err)
}()
lastAcceptedID, err := vm.LastAccepted(context.Background())
require.NoError(t, err)
signature, err := vm.warpBackend.GetBlockSignature(context.TODO(), lastAcceptedID)
require.NoError(t, err)
var knownSignature [bls.SignatureLen]byte
copy(knownSignature[:], signature)
tests := map[string]struct {
blockID ids.ID
expectedResponse [bls.SignatureLen]byte
}{
"known": {
blockID: lastAcceptedID,
expectedResponse: knownSignature,
},
"unknown": {
blockID: ids.GenerateTestID(),
expectedResponse: [bls.SignatureLen]byte{},
},
}
for name, test := range tests {
calledSendAppResponseFn := false
appSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, responseBytes []byte) error {
calledSendAppResponseFn = true
var response message.SignatureResponse
_, err := message.Codec.Unmarshal(responseBytes, &response)
require.NoError(t, err)
require.Equal(t, test.expectedResponse, response.Signature)
return nil
}
t.Run(name, func(t *testing.T) {
var signatureRequest message.Request = message.BlockSignatureRequest{
BlockID: test.blockID,
}
requestBytes, err := message.Codec.Marshal(message.Version, &signatureRequest)
require.NoError(t, err)
// Send the app request and make sure we called SendAppResponseFn
deadline := time.Now().Add(60 * time.Second)
require.NoError(t, vm.Network.AppRequest(context.Background(), ids.GenerateTestNodeID(), peertest.TestPeerRequestID, deadline, requestBytes))
require.True(t, calledSendAppResponseFn)
})
}
}
func TestClearWarpDB(t *testing.T) {
ctx, db, genesisBytes, issuer, _ := setupGenesis(t, genesisJSONLatest)
vm := &VM{}
err := vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, &enginetest.Sender{})
require.NoError(t, err)
// use multiple messages to test that all messages get cleared
payloads := [][]byte{[]byte("test1"), []byte("test2"), []byte("test3"), []byte("test4"), []byte("test5")}
messages := []*luxWarp.UnsignedMessage{}
// add all messages
for _, payload := range payloads {
unsignedMsg, err := luxWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, payload)
require.NoError(t, err)
err = vm.warpBackend.AddMessage(unsignedMsg)
require.NoError(t, err)
// ensure that the message was added
_, err = vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMsg)
require.NoError(t, err)
messages = append(messages, unsignedMsg)
}
require.NoError(t, vm.Shutdown(context.Background()))
// Restart VM with the same database default should not prune the warp db
vm = &VM{}
// we need new context since the previous one has registered metrics.
ctx, _, _, _, _ = setupGenesis(t, genesisJSONLatest)
err = vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, &enginetest.Sender{})
require.NoError(t, err)
// check messages are still present
for _, message := range messages {
bytes, err := vm.warpBackend.GetMessageSignature(context.TODO(), message)
require.NoError(t, err)
require.NotEmpty(t, bytes)
}
require.NoError(t, vm.Shutdown(context.Background()))
// restart the VM with pruning enabled
vm = &VM{}
config := `{"prune-warp-db-enabled": true}`
ctx, _, _, _, _ = setupGenesis(t, genesisJSONLatest)
err = vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte(config), issuer, []*commonEng.Fx{}, &enginetest.Sender{})
require.NoError(t, err)
it := vm.warpDB.NewIterator()
require.False(t, it.Next())
it.Release()
// ensure all messages have been deleted
for _, message := range messages {
_, err := vm.warpBackend.GetMessageSignature(context.TODO(), message)
require.ErrorIs(t, err, &commonEng.AppError{Code: warp.ParseErrCode})
}
}
-17
View File
@@ -1,17 +0,0 @@
// (c) 2019-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"fmt"
"github.com/luxfi/node/version"
"github.com/luxfi/evm/plugin/evm"
"github.com/luxfi/evm/plugin/runner"
)
func main() {
versionString := fmt.Sprintf("EVM/%s [Lux=%s, rpcchainvm=%d]", evm.Version, version.Current, version.RPCChainVMProtocol)
runner.Run(versionString)
}

Some files were not shown because too many files have changed in this diff Show More