Files
proto/p/block/executor/helpers_test.go
T

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
}