mirror of
https://github.com/luxfi/proto.git
synced 2026-07-27 07:04:45 +00:00
418 lines
10 KiB
Go
418 lines
10 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package executor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
metric "github.com/luxfi/metric"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/luxfi/atomic"
|
|
"github.com/luxfi/codec"
|
|
"github.com/luxfi/codec/linearcodec"
|
|
consensustest "github.com/luxfi/consensus/test/helpers"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/database/prefixdb"
|
|
"github.com/luxfi/database/versiondb"
|
|
"github.com/luxfi/ids"
|
|
log "github.com/luxfi/log"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/node/chains"
|
|
"github.com/luxfi/proto/p/config"
|
|
"github.com/luxfi/proto/p/fx"
|
|
"github.com/luxfi/proto/p/genesis/genesistest"
|
|
"github.com/luxfi/proto/p/metrics"
|
|
"github.com/luxfi/proto/p/reward"
|
|
"github.com/luxfi/proto/p/state"
|
|
"github.com/luxfi/proto/p/state/statetest"
|
|
"github.com/luxfi/proto/p/status"
|
|
"github.com/luxfi/proto/p/txs"
|
|
"github.com/luxfi/proto/p/txs/executor"
|
|
"github.com/luxfi/proto/txs/mempool"
|
|
"github.com/luxfi/timer/mockable"
|
|
"github.com/luxfi/upgrade/upgradetest"
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/validators/uptime"
|
|
chainatomic "github.com/luxfi/vm/chains/atomic"
|
|
|
|
"github.com/luxfi/proto/p/txs/txstest"
|
|
"github.com/luxfi/proto/p/utxo"
|
|
"github.com/luxfi/proto/p/validators/validatorstest"
|
|
"github.com/luxfi/sdk/wallet/chain/p/wallet"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
|
|
txmempool "github.com/luxfi/proto/txs/mempool"
|
|
)
|
|
|
|
const (
|
|
pending stakerStatus = iota
|
|
current
|
|
|
|
defaultMinStakingDuration = 24 * time.Hour
|
|
defaultMaxStakingDuration = 365 * 24 * time.Hour
|
|
|
|
defaultTxFee = 100 * constants.MicroLux
|
|
)
|
|
|
|
var testNet1 *txs.Tx
|
|
|
|
type stakerStatus uint
|
|
|
|
type staker struct {
|
|
nodeID ids.NodeID
|
|
rewardAddress ids.ShortID
|
|
startTime, endTime time.Time
|
|
}
|
|
|
|
type test struct {
|
|
description string
|
|
stakers []staker
|
|
chainStakers []staker
|
|
advanceTimeTo []time.Time
|
|
expectedStakers map[ids.NodeID]stakerStatus
|
|
expectedNetStakers map[ids.NodeID]stakerStatus
|
|
}
|
|
|
|
// testContext provides a mock context for testing
|
|
type testContext struct {
|
|
context.Context // embed stdlib context
|
|
NetworkID uint32
|
|
ChainID ids.ID
|
|
NodeID ids.NodeID
|
|
XChainID ids.ID
|
|
CChainID ids.ID
|
|
XAssetID ids.ID
|
|
Log log.Logger
|
|
Lock *sync.RWMutex
|
|
SharedMemory chainatomic.SharedMemory
|
|
}
|
|
|
|
type environment struct {
|
|
blkManager Manager
|
|
mempool txmempool.Mempool[*txs.Tx]
|
|
|
|
isBootstrapped *atomic.Atomic[bool]
|
|
config *config.Internal
|
|
clk *mockable.Clock
|
|
baseDB *versiondb.Database
|
|
ctx *testContext
|
|
fx fx.Fx
|
|
state state.State
|
|
mockedState *state.MockState
|
|
uptimes uptime.Calculator
|
|
utxosVerifier utxo.Verifier
|
|
backend *executor.Backend
|
|
}
|
|
|
|
func newEnvironment(t *testing.T, ctrl *gomock.Controller, f upgradetest.Fork) *environment {
|
|
res := &environment{
|
|
isBootstrapped: &atomic.Atomic[bool]{},
|
|
config: defaultConfig(f),
|
|
clk: defaultClock(),
|
|
}
|
|
res.isBootstrapped.Set(true)
|
|
|
|
res.baseDB = versiondb.New(memdb.New())
|
|
atomicDB := prefixdb.New([]byte{1}, res.baseDB)
|
|
m := chainatomic.NewMemory(atomicDB)
|
|
|
|
// Create consensus context from consensustest
|
|
rt := consensustest.Runtime(t, consensustest.PChainID)
|
|
|
|
// Build our testContext from the consensus context
|
|
res.ctx = &testContext{
|
|
Context: context.Background(),
|
|
NetworkID: rt.NetworkID,
|
|
ChainID: rt.ChainID,
|
|
NodeID: rt.NodeID,
|
|
XChainID: rt.XChainID,
|
|
CChainID: rt.CChainID,
|
|
XAssetID: rt.XAssetID,
|
|
Log: rt.Log.(log.Logger),
|
|
Lock: &rt.Lock,
|
|
SharedMemory: m.NewSharedMemory(rt.ChainID),
|
|
}
|
|
|
|
res.fx = defaultFx(res.clk, res.ctx.Log, res.isBootstrapped.Get())
|
|
|
|
rewardsCalc := reward.NewCalculator(res.config.RewardConfig)
|
|
|
|
// Create a node mockable clock for utxo handler
|
|
nodeClock := &mockable.Clock{}
|
|
nodeClock.Set(genesistest.DefaultValidatorStartTime)
|
|
|
|
if ctrl == nil {
|
|
res.state = statetest.New(t, statetest.Config{
|
|
DB: res.baseDB,
|
|
Genesis: genesistest.NewBytes(t, genesistest.Config{}),
|
|
Validators: res.config.Validators,
|
|
Context: rt,
|
|
Rewards: rewardsCalc,
|
|
})
|
|
|
|
res.uptimes = &uptime.NoOpCalculator{}
|
|
res.utxosVerifier = utxo.NewVerifier(res.clk, res.fx)
|
|
} else {
|
|
res.mockedState = state.NewMockState(ctrl)
|
|
res.uptimes = &uptime.NoOpCalculator{}
|
|
res.utxosVerifier = utxo.NewVerifier(res.clk, res.fx)
|
|
|
|
// setup expectations strictly needed for environment creation
|
|
res.mockedState.EXPECT().GetLastAccepted().Return(ids.GenerateTestID()).Times(1)
|
|
}
|
|
|
|
res.backend = &executor.Backend{
|
|
Config: res.config,
|
|
Ctx: rt,
|
|
Clk: res.clk,
|
|
Bootstrapped: res.isBootstrapped,
|
|
Fx: res.fx,
|
|
FlowChecker: res.utxosVerifier,
|
|
Uptimes: &uptime.NoOpCalculator{},
|
|
Rewards: rewardsCalc,
|
|
}
|
|
|
|
registerer := metric.NewRegistry()
|
|
|
|
platformMetrics := metrics.Noop
|
|
|
|
mempoolMetrics, err := mempool.NewMetrics("mempool", registerer)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to create mempool metrics: %w", err))
|
|
}
|
|
res.mempool = mempool.New[*txs.Tx](mempoolMetrics)
|
|
|
|
if ctrl == nil {
|
|
res.blkManager = NewManager(
|
|
res.mempool,
|
|
platformMetrics,
|
|
res.state,
|
|
res.backend,
|
|
validatorstest.Manager,
|
|
)
|
|
addNet(t, res)
|
|
} else {
|
|
res.blkManager = NewManager(
|
|
res.mempool,
|
|
platformMetrics,
|
|
res.mockedState,
|
|
res.backend,
|
|
validatorstest.Manager,
|
|
)
|
|
// we do not add any chain to state, since we can mock
|
|
// whatever we need
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
res.ctx.Lock.Lock()
|
|
defer res.ctx.Lock.Unlock()
|
|
|
|
if res.mockedState != nil {
|
|
// state is mocked, nothing to do here
|
|
return
|
|
}
|
|
|
|
require := require.New(t)
|
|
|
|
// NoOpCalculator doesn't track validators, so no cleanup needed
|
|
|
|
if res.state != nil {
|
|
require.NoError(res.state.Close())
|
|
}
|
|
|
|
require.NoError(res.baseDB.Close())
|
|
})
|
|
|
|
return res
|
|
}
|
|
|
|
type walletConfig struct {
|
|
keys []*secp256k1.PrivateKey
|
|
chainIDs []ids.ID
|
|
}
|
|
|
|
func newWallet(t testing.TB, e *environment, c walletConfig) wallet.Wallet {
|
|
if len(c.keys) == 0 {
|
|
c.keys = genesistest.DefaultFundedKeys
|
|
}
|
|
|
|
// Get consensus context
|
|
rt := consensustest.Runtime(t, consensustest.PChainID)
|
|
|
|
return txstest.NewWallet(
|
|
t,
|
|
rt,
|
|
&config.Config{
|
|
TrackedChains: e.config.TrackedChains,
|
|
SybilProtectionEnabled: e.config.SybilProtectionEnabled,
|
|
Chains: e.config.Chains,
|
|
},
|
|
e.state,
|
|
secp256k1fx.NewKeychain(c.keys...),
|
|
c.chainIDs,
|
|
nil, // validationIDs
|
|
[]ids.ID{e.ctx.CChainID, e.ctx.XChainID},
|
|
)
|
|
}
|
|
|
|
func addNet(t testing.TB, env *environment) {
|
|
require := require.New(t)
|
|
|
|
wallet := newWallet(t, env, walletConfig{
|
|
keys: genesistest.DefaultFundedKeys[:1],
|
|
})
|
|
|
|
var err error
|
|
testNet1, err = wallet.IssueCreateNetworkTx(
|
|
&secp256k1fx.OutputOwners{
|
|
Threshold: 2,
|
|
Addrs: []ids.ShortID{
|
|
genesistest.DefaultFundedKeys[0].Address(),
|
|
genesistest.DefaultFundedKeys[1].Address(),
|
|
genesistest.DefaultFundedKeys[2].Address(),
|
|
},
|
|
},
|
|
)
|
|
require.NoError(err)
|
|
|
|
genesisID := env.state.GetLastAccepted()
|
|
stateDiff, err := state.NewDiff(genesisID, env.blkManager)
|
|
require.NoError(err)
|
|
|
|
feeCalculator := state.PickFeeCalculator(env.config, stateDiff)
|
|
_, _, _, err = executor.StandardTx(
|
|
env.backend,
|
|
feeCalculator,
|
|
testNet1,
|
|
stateDiff,
|
|
)
|
|
require.NoError(err)
|
|
|
|
stateDiff.AddTx(testNet1, status.Committed)
|
|
require.NoError(stateDiff.Apply(env.state))
|
|
require.NoError(env.state.Commit())
|
|
}
|
|
|
|
func defaultConfig(f upgradetest.Fork) *config.Internal {
|
|
upgrades := upgradetest.GetConfigWithUpgradeTime(f, time.Time{})
|
|
// This package neglects fork ordering
|
|
upgradetest.SetTimesTo(
|
|
&upgrades,
|
|
min(f, upgradetest.ApricotPhase5),
|
|
genesistest.DefaultValidatorEndTime,
|
|
)
|
|
|
|
return &config.Internal{
|
|
Chains: chains.TestManager,
|
|
UptimeLockedCalculator: uptime.NewLockedCalculator(),
|
|
Validators: validators.NewManager(),
|
|
TrackedChains: set.Of(constants.PrimaryNetworkID),
|
|
MinValidatorStake: 5 * constants.MilliLux,
|
|
MaxValidatorStake: 500 * constants.MilliLux,
|
|
MinDelegatorStake: 1 * constants.MilliLux,
|
|
MinStakeDuration: defaultMinStakingDuration,
|
|
MaxStakeDuration: defaultMaxStakingDuration,
|
|
RewardConfig: reward.Config{
|
|
MaxConsumptionRate: .12 * reward.PercentDenominator,
|
|
MinConsumptionRate: .10 * reward.PercentDenominator,
|
|
MintingPeriod: 365 * 24 * time.Hour,
|
|
SupplyCap: 720 * constants.MegaLux,
|
|
},
|
|
UpgradeConfig: upgrades,
|
|
}
|
|
}
|
|
|
|
func defaultClock() *mockable.Clock {
|
|
clk := &mockable.Clock{}
|
|
clk.Set(genesistest.DefaultValidatorStartTime)
|
|
return clk
|
|
}
|
|
|
|
type fxVMInt struct {
|
|
registry codec.Registry
|
|
clk *mockable.Clock
|
|
log log.Logger
|
|
}
|
|
|
|
func (fvi *fxVMInt) CodecRegistry() codec.Registry {
|
|
return fvi.registry
|
|
}
|
|
|
|
func (fvi *fxVMInt) Clock() *mockable.Clock {
|
|
return fvi.clk
|
|
}
|
|
|
|
func (fvi *fxVMInt) Logger() log.Logger {
|
|
return fvi.log
|
|
}
|
|
|
|
func defaultFx(clk *mockable.Clock, log log.Logger, isBootstrapped bool) fx.Fx {
|
|
fxVMInt := &fxVMInt{
|
|
registry: linearcodec.NewDefault(),
|
|
clk: clk,
|
|
log: log,
|
|
}
|
|
res := &secp256k1fx.Fx{}
|
|
if err := res.Initialize(fxVMInt); err != nil {
|
|
panic(err)
|
|
}
|
|
if isBootstrapped {
|
|
if err := res.Bootstrapped(); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func addPendingValidator(
|
|
t testing.TB,
|
|
env *environment,
|
|
startTime time.Time,
|
|
endTime time.Time,
|
|
nodeID ids.NodeID,
|
|
rewardAddress ids.ShortID,
|
|
keys []*secp256k1.PrivateKey,
|
|
) *txs.Tx {
|
|
require := require.New(t)
|
|
|
|
wallet := newWallet(t, env, walletConfig{
|
|
keys: keys,
|
|
})
|
|
|
|
addValidatorTx, err := wallet.IssueAddValidatorTx(
|
|
&txs.Validator{
|
|
NodeID: nodeID,
|
|
Start: uint64(startTime.Unix()),
|
|
End: uint64(endTime.Unix()),
|
|
Wght: env.config.MinValidatorStake,
|
|
},
|
|
&secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{rewardAddress},
|
|
},
|
|
reward.PercentDenominator,
|
|
)
|
|
require.NoError(err)
|
|
|
|
staker, err := state.NewPendingStaker(
|
|
addValidatorTx.ID(),
|
|
addValidatorTx.Unsigned.(*txs.AddValidatorTx),
|
|
)
|
|
require.NoError(err)
|
|
|
|
require.NoError(env.state.PutPendingValidator(staker))
|
|
env.state.AddTx(addValidatorTx, status.Committed)
|
|
env.state.SetHeight(1)
|
|
require.NoError(env.state.Commit())
|
|
return addValidatorTx
|
|
}
|