mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
Clean up project
This commit is contained in:
+14
-14
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ðConf)
|
||||
}
|
||||
// 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, ðConf, 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 := ðparams.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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()) }
|
||||
@@ -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()) }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 = ðDbWrapper{}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
// (c) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package acp176
|
||||
package lip176
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user