diff --git a/vms/platformvm/state/staker.go b/vms/platformvm/state/staker.go index a9ba52595062..aa8f6f1a4cdd 100644 --- a/vms/platformvm/state/staker.go +++ b/vms/platformvm/state/staker.go @@ -66,21 +66,25 @@ type Staker struct { // 3. If the priorities are also the same, the one with the lesser txID is // lesser. func (s *Staker) Less(than *Staker) bool { + return s.Compare(than) == -1 +} + +func (s *Staker) Compare(than *Staker) int { if s.NextTime.Before(than.NextTime) { - return true + return -1 } if than.NextTime.Before(s.NextTime) { - return false + return 1 } if s.Priority < than.Priority { - return true + return -1 } if than.Priority < s.Priority { - return false + return 1 } - return bytes.Compare(s.TxID[:], than.TxID[:]) == -1 + return bytes.Compare(s.TxID[:], than.TxID[:]) } func NewCurrentStaker( diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index f787749f72df..1b2484235dc0 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -297,6 +297,7 @@ func (s *diffStakers) PutValidator(staker *Staker) { s.addedStakers = btree.NewG(defaultTreeDegree, (*Staker).Less) } s.addedStakers.ReplaceOrInsert(staker) + delete(s.deletedStakers, staker.TxID) } func (s *diffStakers) DeleteValidator(staker *Staker) { @@ -348,11 +349,13 @@ func (s *diffStakers) PutDelegator(staker *Staker) { validatorDiff.addedDelegators = btree.NewG(defaultTreeDegree, (*Staker).Less) } validatorDiff.addedDelegators.ReplaceOrInsert(staker) + delete(validatorDiff.deletedDelegators, staker.TxID) if s.addedStakers == nil { s.addedStakers = btree.NewG(defaultTreeDegree, (*Staker).Less) } s.addedStakers.ReplaceOrInsert(staker) + delete(s.deletedStakers, staker.TxID) } func (s *diffStakers) DeleteDelegator(staker *Staker) { diff --git a/vms/platformvm/state/stakers_helpers_test.go b/vms/platformvm/state/stakers_helpers_test.go new file mode 100644 index 000000000000..56bdb9a88f95 --- /dev/null +++ b/vms/platformvm/state/stakers_helpers_test.go @@ -0,0 +1,131 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "fmt" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/ava-labs/avalanchego/chains" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/json" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/api" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/metrics" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" +) + +var ( + _ Versions = (*versionsHolder)(nil) + + defaultMinStakingDuration = 24 * time.Hour + defaultMaxStakingDuration = 365 * 24 * time.Hour + defaultGenesisTime = time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC) + defaultValidateStartTime = defaultGenesisTime + defaultValidateEndTime = defaultValidateStartTime.Add(10 * defaultMinStakingDuration) + defaultTxFee = uint64(100) +) + +type stakerStatus int + +type versionsHolder struct { + baseState State +} + +func (h *versionsHolder) GetState(blkID ids.ID) (Chain, bool) { + return h.baseState, blkID == h.baseState.GetLastAccepted() +} + +func buildChainState(baseDB database.Database, trackedSubnets []ids.ID) (State, error) { + cfg := defaultConfig() + cfg.TrackedSubnets.Add(trackedSubnets...) + + execConfig, err := config.GetExecutionConfig(nil) + if err != nil { + return nil, err + } + + ctx := snowtest.Context(&testing.T{}, snowtest.PChainID) + + genesisBytes, err := buildGenesisTest(ctx) + if err != nil { + return nil, err + } + + rewardsCalc := reward.NewCalculator(cfg.RewardConfig) + return New( + baseDB, + genesisBytes, + prometheus.NewRegistry(), + cfg, + execConfig, + ctx, + metrics.Noop, + rewardsCalc, + ) +} + +func defaultConfig() *config.Config { + return &config.Config{ + Chains: chains.TestManager, + UptimeLockedCalculator: uptime.NewLockedCalculator(), + Validators: validators.NewManager(), + TxFee: defaultTxFee, + CreateSubnetTxFee: 100 * defaultTxFee, + CreateBlockchainTxFee: 100 * defaultTxFee, + MinValidatorStake: 5 * units.MilliAvax, + MaxValidatorStake: 500 * units.MilliAvax, + MinDelegatorStake: 1 * units.MilliAvax, + MinStakeDuration: defaultMinStakingDuration, + MaxStakeDuration: defaultMaxStakingDuration, + RewardConfig: reward.Config{ + MaxConsumptionRate: .12 * reward.PercentDenominator, + MinConsumptionRate: .10 * reward.PercentDenominator, + MintingPeriod: defaultMaxStakingDuration, + SupplyCap: 720 * units.MegaAvax, + }, + ApricotPhase3Time: defaultValidateEndTime, + ApricotPhase5Time: defaultValidateEndTime, + BanffTime: defaultValidateEndTime, + CortinaTime: defaultValidateEndTime, + DurangoTime: defaultValidateEndTime, + } +} + +func buildGenesisTest(ctx *snow.Context) ([]byte, error) { + buildGenesisArgs := api.BuildGenesisArgs{ + NetworkID: json.Uint32(constants.UnitTestID), + AvaxAssetID: ctx.AVAXAssetID, + UTXOs: nil, // no UTXOs in this genesis. Not relevant to package tests. + Validators: nil, // no validators in this genesis. Tests will handle them. + Chains: nil, + Time: json.Uint64(defaultGenesisTime.Unix()), + InitialSupply: json.Uint64(360 * units.MegaAvax), + Encoding: formatting.Hex, + } + + buildGenesisResponse := api.BuildGenesisReply{} + platformvmSS := api.StaticService{} + if err := platformvmSS.BuildGenesis(nil, &buildGenesisArgs, &buildGenesisResponse); err != nil { + return nil, fmt.Errorf("problem while building platform chain's genesis state: %w", err) + } + + genesisBytes, err := formatting.Decode(buildGenesisResponse.Encoding, buildGenesisResponse.Bytes) + if err != nil { + return nil, err + } + + return genesisBytes, nil +} diff --git a/vms/platformvm/state/stakers_model_generator_check_test.go b/vms/platformvm/state/stakers_model_generator_check_test.go new file mode 100644 index 000000000000..8271d3d75f9e --- /dev/null +++ b/vms/platformvm/state/stakers_model_generator_check_test.go @@ -0,0 +1,244 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + "fmt" + "math" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + errNotAStakerTx = errors.New("tx is not a stakerTx") + errWrongNodeID = errors.New("unexpected nodeID") +) + +// TestGeneratedStakersValidity tests the staker generator itself. +// It documents and verifies the invariants enforced by the staker generator. +func TestGeneratedStakersValidity(t *testing.T) { + properties := gopter.NewProperties(nil) + + ctx := snowtest.Context(&testing.T{}, snowtest.PChainID) + subnetID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + maxDelegatorWeight := uint64(2023) + + properties.Property("AddValidatorTx generator checks", prop.ForAll( + func(nonInitTx *txs.Tx) string { + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + if err := signedTx.SyntacticVerify(ctx); err != nil { + return err.Error() + } + + addValTx, ok := signedTx.Unsigned.(*txs.AddValidatorTx) + if !ok { + return errNotAStakerTx.Error() + } + + if nodeID != addValTx.NodeID() { + return errWrongNodeID.Error() + } + + currentVal, err := NewCurrentStaker(signedTx.ID(), addValTx, addValTx.StartTime(), uint64(100)) + if err != nil { + return err.Error() + } + + if currentVal.EndTime.Before(currentVal.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + currentVal.StartTime, currentVal.EndTime, currentVal) + } + + pendingVal, err := NewPendingStaker(signedTx.ID(), addValTx) + if err != nil { + return err.Error() + } + + if pendingVal.EndTime.Before(pendingVal.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + pendingVal.StartTime, pendingVal.EndTime, pendingVal) + } + + return "" + }, + addValidatorTxGenerator(ctx, &nodeID, math.MaxUint64), + )) + + properties.Property("AddDelegatorTx generator checks", prop.ForAll( + func(nonInitTx *txs.Tx) string { + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + if err := signedTx.SyntacticVerify(ctx); err != nil { + return err.Error() + } + + addDelTx, ok := signedTx.Unsigned.(*txs.AddDelegatorTx) + if !ok { + return errNotAStakerTx.Error() + } + + if nodeID != addDelTx.NodeID() { + return errWrongNodeID.Error() + } + + currentDel, err := NewCurrentStaker(signedTx.ID(), addDelTx, addDelTx.StartTime(), uint64(100)) + if err != nil { + return err.Error() + } + + if currentDel.EndTime.Before(currentDel.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + currentDel.StartTime, currentDel.EndTime, currentDel) + } + + if currentDel.Weight > maxDelegatorWeight { + return fmt.Sprintf("delegator weight %v above maximum %v, staker %v", + currentDel.Weight, maxDelegatorWeight, currentDel) + } + + pendingDel, err := NewPendingStaker(signedTx.ID(), addDelTx) + if err != nil { + return err.Error() + } + + if pendingDel.EndTime.Before(pendingDel.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + pendingDel.StartTime, pendingDel.EndTime, pendingDel) + } + + if pendingDel.Weight > maxDelegatorWeight { + return fmt.Sprintf("delegator weight %v above maximum %v, staker %v", + pendingDel.Weight, maxDelegatorWeight, pendingDel) + } + + return "" + }, + addDelegatorTxGenerator(ctx, &nodeID, maxDelegatorWeight), + )) + + properties.Property("addPermissionlessValidatorTx generator checks", prop.ForAll( + func(nonInitTx *txs.Tx) string { + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + if err := signedTx.SyntacticVerify(ctx); err != nil { + return err.Error() + } + + addValTx, ok := signedTx.Unsigned.(*txs.AddPermissionlessValidatorTx) + if !ok { + return errNotAStakerTx.Error() + } + + if nodeID != addValTx.NodeID() { + return errWrongNodeID.Error() + } + + if subnetID != addValTx.SubnetID() { + return "subnet not duly set" + } + + currentVal, err := NewCurrentStaker(signedTx.ID(), addValTx, addValTx.StartTime(), uint64(100)) + if err != nil { + return err.Error() + } + + if currentVal.EndTime.Before(currentVal.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + currentVal.StartTime, currentVal.EndTime, currentVal) + } + + pendingVal, err := NewPendingStaker(signedTx.ID(), addValTx) + if err != nil { + return err.Error() + } + + if pendingVal.EndTime.Before(pendingVal.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + pendingVal.StartTime, pendingVal.EndTime, pendingVal) + } + + return "" + }, + addPermissionlessValidatorTxGenerator(ctx, &subnetID, &nodeID, math.MaxUint64), + )) + + properties.Property("addPermissionlessDelegatorTx generator checks", prop.ForAll( + func(nonInitTx *txs.Tx) string { + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + if err := signedTx.SyntacticVerify(ctx); err != nil { + return err.Error() + } + + addDelTx, ok := signedTx.Unsigned.(*txs.AddPermissionlessDelegatorTx) + if !ok { + return errNotAStakerTx.Error() + } + + if nodeID != addDelTx.NodeID() { + return errWrongNodeID.Error() + } + + if subnetID != addDelTx.SubnetID() { + return "subnet not duly set" + } + + currentDel, err := NewCurrentStaker(signedTx.ID(), addDelTx, addDelTx.StartTime(), uint64(100)) + if err != nil { + return err.Error() + } + + if currentDel.EndTime.Before(currentDel.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + currentDel.StartTime, currentDel.EndTime, currentDel) + } + + if currentDel.Weight > maxDelegatorWeight { + return fmt.Sprintf("delegator weight %v above maximum %v, staker %v", + currentDel.Weight, maxDelegatorWeight, currentDel) + } + + pendingDel, err := NewPendingStaker(signedTx.ID(), addDelTx) + if err != nil { + return err.Error() + } + + if pendingDel.EndTime.Before(pendingDel.StartTime) { + return fmt.Sprintf("startTime %v not before endTime %v, staker %v", + pendingDel.StartTime, pendingDel.EndTime, pendingDel) + } + + if pendingDel.Weight > maxDelegatorWeight { + return fmt.Sprintf("delegator weight %v above maximum %v, staker %v", + pendingDel.Weight, maxDelegatorWeight, pendingDel) + } + + return "" + }, + addPermissionlessDelegatorTxGenerator(ctx, &subnetID, &nodeID, maxDelegatorWeight), + )) + + properties.TestingRun(t) +} diff --git a/vms/platformvm/state/stakers_model_generator_test.go b/vms/platformvm/state/stakers_model_generator_test.go new file mode 100644 index 000000000000..add9810069b2 --- /dev/null +++ b/vms/platformvm/state/stakers_model_generator_test.go @@ -0,0 +1,405 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "fmt" + "reflect" + "time" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/gen" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + blst "github.com/supranational/blst/bindings/go" +) + +type generatorPriorityType uint8 + +const ( + permissionlessValidator generatorPriorityType = iota + permissionedValidator + permissionlessDelegator + permissionedDelegator +) + +// stakerTxGenerator helps creating random yet reproducible txs.StakerTx, +// which can be used in our property tests. stakerTxGenerator returns txs.StakerTx +// as the Unsigned attribute of a txs.Tx just to work around the inability of +// generators to return interface. The holding txs.Tx signing is deferred to tests +// to allow them modifying stakers parameters without breaking txID. +// A full txs.StakerTx is returned, instead of a Staker object, in order to extend +// property testing to stakers reload (which starts from the transaction). The tx is filled +// just enough to rebuild staker state (inputs/outputs utxos are neglected). +// TestGeneratedStakersValidity documents and verifies the enforced invariants. +func stakerTxGenerator( + ctx *snow.Context, + priority generatorPriorityType, + subnetID *ids.ID, + nodeID *ids.NodeID, + maxWeight uint64, // helps avoiding overflows in delegator tests +) gopter.Gen { + switch priority { + case permissionedValidator: + return addValidatorTxGenerator(ctx, nodeID, maxWeight) + case permissionedDelegator: + return addDelegatorTxGenerator(ctx, nodeID, maxWeight) + case permissionlessValidator: + return addPermissionlessValidatorTxGenerator(ctx, subnetID, nodeID, maxWeight) + case permissionlessDelegator: + return addPermissionlessDelegatorTxGenerator(ctx, subnetID, nodeID, maxWeight) + default: + panic(fmt.Sprintf("unhandled tx priority %v", priority)) + } +} + +func addPermissionlessValidatorTxGenerator( + ctx *snow.Context, + subnetID *ids.ID, + nodeID *ids.NodeID, + maxWeight uint64, +) gopter.Gen { + return stakerDataGenerator(nodeID, maxWeight).FlatMap( + func(v interface{}) gopter.Gen { + genStakerSubnetID := subnetIDGen + if subnetID != nil { + genStakerSubnetID = gen.Const(*subnetID) + } + + // always return a non-empty bls key here. Will drop it + // below, in txs.Tx generator if needed. + fullBlsKeyGen := gen.SliceOfN(32, gen.UInt8()).FlatMap( + func(v interface{}) gopter.Gen { + bytes := v.([]byte) + sk1 := blst.KeyGen(bytes) + return gen.Const(signer.NewProofOfPossession(sk1)) + }, + reflect.TypeOf(&signer.ProofOfPossession{}), + ) + + stakerData := v.(txs.Validator) + + specificGen := gen.StructPtr(reflect.TypeOf(&txs.AddPermissionlessValidatorTx{}), map[string]gopter.Gen{ + "BaseTx": gen.Const(txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{}, + Outs: []*avax.TransferableOutput{}, + }, + }), + "Validator": gen.Const(stakerData), + "Subnet": genStakerSubnetID, + "Signer": fullBlsKeyGen, + "StakeOuts": gen.Const([]*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: stakerData.Weight(), + }, + }, + }), + "ValidatorRewardsOwner": gen.Const( + &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{}, + }, + ), + "DelegatorRewardsOwner": gen.Const( + &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{}, + }, + ), + "DelegationShares": gen.UInt32Range(0, reward.PercentDenominator), + }) + + return specificGen.FlatMap( + func(v interface{}) gopter.Gen { + stakerTx := v.(*txs.AddPermissionlessValidatorTx) + + // drop Signer if needed + if stakerTx.Subnet != constants.PlatformChainID { + stakerTx.Signer = &signer.Empty{} + } + + if err := stakerTx.SyntacticVerify(ctx); err != nil { + panic(fmt.Errorf("failed syntax verification in tx generator, %w", err)) + } + + // Note: we don't sign the tx here, since we want the freedom to modify + // the stakerTx just before testing while avoid having the wrong txID. + // We use txs.Tx as a box to return a txs.StakerTx interface. + sTx := &txs.Tx{Unsigned: stakerTx} + + return gen.Const(sTx) + }, + reflect.TypeOf(&txs.AddPermissionlessValidatorTx{}), + ) + }, + reflect.TypeOf(&txs.AddPermissionlessValidatorTx{}), + ) +} + +func addValidatorTxGenerator( + ctx *snow.Context, + nodeID *ids.NodeID, + maxWeight uint64, +) gopter.Gen { + return stakerDataGenerator(nodeID, maxWeight).FlatMap( + func(v interface{}) gopter.Gen { + stakerData := v.(txs.Validator) + + specificGen := gen.StructPtr(reflect.TypeOf(&txs.AddValidatorTx{}), map[string]gopter.Gen{ + "BaseTx": gen.Const(txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{}, + Outs: []*avax.TransferableOutput{}, + }, + }), + "Validator": gen.Const(stakerData), + "StakeOuts": gen.Const([]*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: stakerData.Weight(), + }, + }, + }), + "RewardsOwner": gen.Const( + &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{}, + }, + ), + "DelegationShares": gen.UInt32Range(0, reward.PercentDenominator), + }) + + return specificGen.FlatMap( + func(v interface{}) gopter.Gen { + stakerTx := v.(*txs.AddValidatorTx) + + if err := stakerTx.SyntacticVerify(ctx); err != nil { + panic(fmt.Errorf("failed syntax verification in tx generator, %w", err)) + } + + // Note: we don't sign the tx here, since we want the freedom to modify + // the stakerTx just before testing while avoid having the wrong txID. + // We use txs.Tx as a box to return a txs.StakerTx interface. + sTx := &txs.Tx{Unsigned: stakerTx} + + return gen.Const(sTx) + }, + reflect.TypeOf(&txs.AddValidatorTx{}), + ) + }, + reflect.TypeOf(txs.Validator{}), + ) +} + +func addPermissionlessDelegatorTxGenerator( + ctx *snow.Context, + subnetID *ids.ID, + nodeID *ids.NodeID, + maxWeight uint64, // helps avoiding overflows in delegator tests +) gopter.Gen { + return stakerDataGenerator(nodeID, maxWeight).FlatMap( + func(v interface{}) gopter.Gen { + genStakerSubnetID := subnetIDGen + if subnetID != nil { + genStakerSubnetID = gen.Const(*subnetID) + } + + stakerData := v.(txs.Validator) + delGen := gen.StructPtr(reflect.TypeOf(txs.AddPermissionlessDelegatorTx{}), map[string]gopter.Gen{ + "BaseTx": gen.Const(txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{}, + Outs: []*avax.TransferableOutput{}, + }, + }), + "Validator": gen.Const(stakerData), + "Subnet": genStakerSubnetID, + "StakeOuts": gen.Const([]*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: stakerData.Weight(), + }, + }, + }), + "DelegationRewardsOwner": gen.Const( + &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{}, + }, + ), + }) + + return delGen.FlatMap( + func(v interface{}) gopter.Gen { + stakerTx := v.(*txs.AddPermissionlessDelegatorTx) + + if err := stakerTx.SyntacticVerify(ctx); err != nil { + panic(fmt.Errorf("failed syntax verification in tx generator, %w", err)) + } + + // Note: we don't sign the tx here, since we want the freedom to modify + // the stakerTx just before testing while avoid having the wrong txID. + // We use txs.Tx as a box to return a txs.StakerTx interface. + sTx := &txs.Tx{Unsigned: stakerTx} + + return gen.Const(sTx) + }, + reflect.TypeOf(&txs.AddPermissionlessDelegatorTx{}), + ) + }, + reflect.TypeOf(txs.Validator{}), + ) +} + +func addDelegatorTxGenerator( + ctx *snow.Context, + nodeID *ids.NodeID, + maxWeight uint64, // helps avoiding overflows in delegator tests +) gopter.Gen { + return stakerDataGenerator(nodeID, maxWeight).FlatMap( + func(v interface{}) gopter.Gen { + stakerData := v.(txs.Validator) + delGen := gen.StructPtr(reflect.TypeOf(txs.AddDelegatorTx{}), map[string]gopter.Gen{ + "BaseTx": gen.Const(txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: []*avax.TransferableInput{}, + Outs: []*avax.TransferableOutput{}, + }, + }), + "Validator": gen.Const(stakerData), + "StakeOuts": gen.Const([]*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: stakerData.Weight(), + }, + }, + }), + "DelegationRewardsOwner": gen.Const( + &secp256k1fx.OutputOwners{ + Addrs: []ids.ShortID{}, + }, + ), + }) + + return delGen.FlatMap( + func(v interface{}) gopter.Gen { + stakerTx := v.(*txs.AddDelegatorTx) + + if err := stakerTx.SyntacticVerify(ctx); err != nil { + panic(fmt.Errorf("failed syntax verification in tx generator, %w", err)) + } + + // Note: we don't sign the tx here, since we want the freedom to modify + // the stakerTx just before testing while avoid having the wrong txID. + // We use txs.Tx as a box to return a txs.StakerTx interface. + sTx := &txs.Tx{Unsigned: stakerTx} + + return gen.Const(sTx) + }, + reflect.TypeOf(&txs.AddDelegatorTx{}), + ) + }, + reflect.TypeOf(txs.Validator{}), + ) +} + +func stakerDataGenerator( + nodeID *ids.NodeID, + maxWeight uint64, // helps avoiding overflows in delegator tests +) gopter.Gen { + return genStakerTimeData().FlatMap( + func(v interface{}) gopter.Gen { + stakerData := v.(stakerTimeData) + + genStakerNodeID := genNodeID + if nodeID != nil { + genStakerNodeID = gen.Const(*nodeID) + } + + return gen.Struct(reflect.TypeOf(txs.Validator{}), map[string]gopter.Gen{ + "NodeID": genStakerNodeID, + "Start": gen.Const(uint64(stakerData.StartTime.Unix())), + "End": gen.Const(uint64(stakerData.StartTime.Add(time.Duration(stakerData.Duration)).Unix())), + "Wght": gen.UInt64Range(1, maxWeight), + }) + }, + reflect.TypeOf(stakerTimeData{}), + ) +} + +// stakerTimeData holds seed attributes to generate a random-yet-reproducible txs.Validator +type stakerTimeData struct { + StartTime time.Time + Duration int64 +} + +// genStakerTimeData is the helper to generate stakerMicroData +func genStakerTimeData() gopter.Gen { + return gen.Struct(reflect.TypeOf(&stakerTimeData{}), map[string]gopter.Gen{ + "StartTime": gen.Time(), + "Duration": gen.Int64Range(int64(time.Hour), int64(365*24*time.Hour)), + }) +} + +const ( + lengthID = 32 + lengthNodeID = 20 +) + +// subnetIDGen is the helper generator for subnetID, duly skewed towards primary network +var subnetIDGen = gen.Weighted([]gen.WeightedGen{ + { + Weight: 50, + Gen: gen.Const(constants.PrimaryNetworkID), + }, + { + Weight: 50, + Gen: gen.SliceOfN(lengthID, gen.UInt8()).FlatMap( + func(v interface{}) gopter.Gen { + byteSlice := v.([]byte) + var byteArray [lengthID]byte + copy(byteArray[:], byteSlice) + return gen.Const(ids.ID(byteArray)) + }, + reflect.TypeOf([]byte{}), + ), + }, +}) + +// genNodeID is the helper generator for ids.NodeID objects +var genNodeID = gen.SliceOfN(lengthNodeID, gen.UInt8()).FlatMap( + func(v interface{}) gopter.Gen { + byteSlice := v.([]byte) + var byteArray [lengthNodeID]byte + copy(byteArray[:], byteSlice) + return gen.Const(ids.NodeID(byteArray)) + }, + reflect.TypeOf([]byte{}), +) diff --git a/vms/platformvm/state/stakers_model_storage.go b/vms/platformvm/state/stakers_model_storage.go new file mode 100644 index 000000000000..0e4d456581d4 --- /dev/null +++ b/vms/platformvm/state/stakers_model_storage.go @@ -0,0 +1,248 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + + "golang.org/x/exp/maps" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" +) + +var ( + _ Stakers = (*stakersStorageModel)(nil) + _ StakerIterator = (*stakersStorageIteratorModel)(nil) +) + +// stakersStorageModel is the executable reference model of how we expect +// P-chain state and diffs to behave with respect to stakers. +// stakersStorageModel abstracts away the complexity related to +// P-chain state persistence and to the Diff flushing mechanisms. +// stakersStorageModel represents how we expect Diff and State to behave +// in a single threaded environment when stakers are written to or read from them. +// The utility of stakersStorageModel as an executable reference model is that +// we can write automatic tests asserting that Diff and State conform +// to stakersStorageModel. + +type subnetNodeKey struct { + subnetID ids.ID + nodeID ids.NodeID +} + +type stakersStorageModel struct { + currentValidators map[subnetNodeKey]*Staker + currentDelegators map[subnetNodeKey](map[ids.ID]*Staker) // -> (txID -> Staker) + + pendingValidators map[subnetNodeKey]*Staker + pendingDelegators map[subnetNodeKey](map[ids.ID]*Staker) // -> (txID -> Staker) +} + +func newStakersStorageModel() *stakersStorageModel { + return &stakersStorageModel{ + currentValidators: make(map[subnetNodeKey]*Staker), + currentDelegators: make(map[subnetNodeKey]map[ids.ID]*Staker), + pendingValidators: make(map[subnetNodeKey]*Staker), + pendingDelegators: make(map[subnetNodeKey]map[ids.ID]*Staker), + } +} + +func (m *stakersStorageModel) GetCurrentValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { + return getValidator(subnetID, nodeID, m.currentValidators) +} + +func (m *stakersStorageModel) GetPendingValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { + return getValidator(subnetID, nodeID, m.pendingValidators) +} + +func getValidator(subnetID ids.ID, nodeID ids.NodeID, domain map[subnetNodeKey]*Staker) (*Staker, error) { + key := subnetNodeKey{ + subnetID: subnetID, + nodeID: nodeID, + } + res, found := domain[key] + if !found { + return nil, database.ErrNotFound + } + return res, nil +} + +func (m *stakersStorageModel) PutCurrentValidator(staker *Staker) { + putValidator(staker, m.currentValidators) +} + +func (m *stakersStorageModel) PutPendingValidator(staker *Staker) { + putValidator(staker, m.pendingValidators) +} + +func putValidator(staker *Staker, domain map[subnetNodeKey]*Staker) { + key := subnetNodeKey{ + subnetID: staker.SubnetID, + nodeID: staker.NodeID, + } + + // overwrite validator even if already exist. In prod code, + // it's up to block verification to check that we do not overwrite + // a validator existing on state or lower diffs. + domain[key] = staker +} + +func (m *stakersStorageModel) DeleteCurrentValidator(staker *Staker) { + deleteValidator(staker, m.currentValidators) +} + +func (m *stakersStorageModel) DeletePendingValidator(staker *Staker) { + deleteValidator(staker, m.pendingValidators) +} + +func deleteValidator(staker *Staker, domain map[subnetNodeKey]*Staker) { + key := subnetNodeKey{ + subnetID: staker.SubnetID, + nodeID: staker.NodeID, + } + delete(domain, key) +} + +func (m *stakersStorageModel) GetCurrentDelegatorIterator(subnetID ids.ID, nodeID ids.NodeID) (StakerIterator, error) { + return getDelegatorIterator(subnetID, nodeID, m.currentDelegators), nil +} + +func (m *stakersStorageModel) GetPendingDelegatorIterator(subnetID ids.ID, nodeID ids.NodeID) (StakerIterator, error) { + return getDelegatorIterator(subnetID, nodeID, m.pendingDelegators), nil +} + +func getDelegatorIterator(subnetID ids.ID, nodeID ids.NodeID, domain map[subnetNodeKey](map[ids.ID]*Staker)) StakerIterator { + key := subnetNodeKey{ + subnetID: subnetID, + nodeID: nodeID, + } + dels, found := domain[key] + if !found { + return EmptyIterator + } + + sortedDels := maps.Values(dels) + utils.Sort(sortedDels) + return &stakersStorageIteratorModel{ + current: nil, + sortedStakers: sortedDels, + } +} + +func (m *stakersStorageModel) PutCurrentDelegator(staker *Staker) { + putDelegator(staker, m.currentDelegators) +} + +func (m *stakersStorageModel) PutPendingDelegator(staker *Staker) { + putDelegator(staker, m.pendingDelegators) +} + +func putDelegator(staker *Staker, domain map[subnetNodeKey]map[ids.ID]*Staker) { + key := subnetNodeKey{ + subnetID: staker.SubnetID, + nodeID: staker.NodeID, + } + + dels, found := domain[key] + if !found { + dels = make(map[ids.ID]*Staker) + domain[key] = dels + } + dels[staker.TxID] = staker +} + +func (m *stakersStorageModel) DeleteCurrentDelegator(staker *Staker) { + deleteDelegator(staker, m.currentDelegators) +} + +func (m *stakersStorageModel) DeletePendingDelegator(staker *Staker) { + deleteDelegator(staker, m.pendingDelegators) +} + +func deleteDelegator(staker *Staker, domain map[subnetNodeKey]map[ids.ID]*Staker) { + key := subnetNodeKey{ + subnetID: staker.SubnetID, + nodeID: staker.NodeID, + } + + dels, found := domain[key] + if !found { + return + } + delete(dels, staker.TxID) + + // prune + if len(dels) == 0 { + delete(domain, key) + } +} + +func (m *stakersStorageModel) GetCurrentStakerIterator() (StakerIterator, error) { + return getCurrentStakerIterator(m.currentValidators, m.currentDelegators), nil +} + +func (m *stakersStorageModel) GetPendingStakerIterator() (StakerIterator, error) { + return getCurrentStakerIterator(m.pendingValidators, m.pendingDelegators), nil +} + +func getCurrentStakerIterator( + validators map[subnetNodeKey]*Staker, + delegators map[subnetNodeKey](map[ids.ID]*Staker), +) StakerIterator { + allStakers := maps.Values(validators) + for _, dels := range delegators { + allStakers = append(allStakers, maps.Values(dels)...) + } + utils.Sort(allStakers) + return &stakersStorageIteratorModel{ + current: nil, + sortedStakers: allStakers, + } +} + +func (*stakersStorageModel) SetDelegateeReward( + ids.ID, + ids.NodeID, + uint64, +) error { + return errors.New("method non implemented in model") +} + +func (*stakersStorageModel) GetDelegateeReward( + ids.ID, + ids.NodeID, +) (uint64, error) { + return 0, errors.New("method non implemented in model") +} + +type stakersStorageIteratorModel struct { + current *Staker + + // sortedStakers contains the sorted list of stakers + // as it should be returned by iteration. + // sortedStakers must be sorted upon stakersStorageIteratorModel creation. + // Stakers are evicted from sortedStakers as Next() is called. + sortedStakers []*Staker +} + +func (i *stakersStorageIteratorModel) Next() bool { + if len(i.sortedStakers) == 0 { + return false + } + + i.current = i.sortedStakers[0] + i.sortedStakers = i.sortedStakers[1:] + return true +} + +func (i *stakersStorageIteratorModel) Value() *Staker { + return i.current +} + +func (i *stakersStorageIteratorModel) Release() { + i.current = nil + i.sortedStakers = nil +} diff --git a/vms/platformvm/state/stakers_model_storage_test.go b/vms/platformvm/state/stakers_model_storage_test.go new file mode 100644 index 000000000000..d02c03ca9dbd --- /dev/null +++ b/vms/platformvm/state/stakers_model_storage_test.go @@ -0,0 +1,1004 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "fmt" + "reflect" + "sync/atomic" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/commands" + "github.com/leanovate/gopter/gen" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/platformvm/status" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + _ Versions = (*sysUnderTest)(nil) + _ commands.Command = (*putCurrentValidatorCommand)(nil) + _ commands.Command = (*deleteCurrentValidatorCommand)(nil) + _ commands.Command = (*putCurrentDelegatorCommand)(nil) + _ commands.Command = (*deleteCurrentDelegatorCommand)(nil) + _ commands.Command = (*addTopDiffCommand)(nil) + _ commands.Command = (*applyAndCommitBottomDiffCommand)(nil) + _ commands.Command = (*rebuildStateCommand)(nil) + + commandsCtx = snowtest.Context(&testing.T{}, snowtest.PChainID) +) + +// TestStateAndDiffComparisonToStorageModel verifies that a production-like +// system made of a stack of Diffs built on top of a State conforms to +// our stakersStorageModel. It achieves this by: +// 1. randomly generating a sequence of stakers writes as well as +// some persistence operations (commit/diff apply), +// 2. applying the sequence to both our stakersStorageModel and the production-like system. +// 3. checking that both stakersStorageModel and the production-like system have +// the same state after each operation. +// +// The following invariants are required for stakers state to properly work: +// 1. No stakers add/update/delete ops are performed directly on baseState, but on at least a diff +// 2. Any number of stakers ops can be carried out on a single diff +// 3. Diffs work in FIFO fashion: they are added on top of current state and only +// bottom diff is applied to base state. +// 4. The bottom diff applied to base state is immediately committed. +func TestStateAndDiffComparisonToStorageModel(t *testing.T) { + properties := gopter.NewProperties(nil) + + // // to reproduce a given scenario do something like this: + // parameters := gopter.DefaultTestParametersWithSeed(1688641048828490074) + // properties := gopter.NewProperties(parameters) + + properties.Property("state comparison to storage model", commands.Prop(stakersCommands)) + properties.TestingRun(t) +} + +// stakersCommands creates/destroy the system under test and generates +// commands and initial states (stakersStorageModel) +var stakersCommands = &commands.ProtoCommands{ + NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest { + model := initialState.(*stakersStorageModel) + + baseDB := versiondb.New(memdb.New()) + baseState, err := buildChainState(baseDB, nil) + if err != nil { + panic(err) + } + + // fillup baseState with model initial content + for _, staker := range model.currentValidators { + baseState.PutCurrentValidator(staker) + } + for _, delegators := range model.currentDelegators { + for _, staker := range delegators { + baseState.PutCurrentDelegator(staker) + } + } + for _, staker := range model.pendingValidators { + baseState.PutPendingValidator(staker) + } + for _, delegators := range model.currentDelegators { + for _, staker := range delegators { + baseState.PutPendingDelegator(staker) + } + } + if err := baseState.Commit(); err != nil { + panic(err) + } + + return newSysUnderTest(baseDB, baseState) + }, + DestroySystemUnderTestFunc: func(sut commands.SystemUnderTest) { + // retrieve base state and close it + sys := sut.(*sysUnderTest) + err := sys.baseState.Close() + if err != nil { + panic(err) + } + }, + // a trick to force command regeneration at each sampling. + // gen.Const would not allow it + InitialStateGen: gen.IntRange(1, 2).Map( + func(int) *stakersStorageModel { + return newStakersStorageModel() + }, + ), + + InitialPreConditionFunc: func(state commands.State) bool { + return true // nothing to do for now + }, + GenCommandFunc: func(state commands.State) gopter.Gen { + return gen.OneGenOf( + genPutCurrentValidatorCommand, + genDeleteCurrentValidatorCommand, + + genPutCurrentDelegatorCommand, + genDeleteCurrentDelegatorCommand, + + genAddTopDiffCommand, + genApplyAndCommitBottomDiffCommand, + genRebuildStateCommand, + ) + }, +} + +// PutCurrentValidator section +type putCurrentValidatorCommand struct { + sTx *txs.Tx + err error +} + +func (cmd *putCurrentValidatorCommand) Run(sut commands.SystemUnderTest) commands.Result { + sTx := cmd.sTx + sys := sut.(*sysUnderTest) + + if err := sys.checkThereIsADiff(); err != nil { + return sys // state checks later on should spot missing validator + } + + stakerTx := sTx.Unsigned.(txs.StakerTx) + startTime := sTx.Unsigned.(txs.ScheduledStaker).StartTime() + currentVal, err := NewCurrentStaker(sTx.ID(), stakerTx, startTime, uint64(1000)) + if err != nil { + return sys // state checks later on should spot missing validator + } + + topChainState := sys.getTopChainState() + topChainState.PutCurrentValidator(currentVal) + topChainState.AddTx(sTx, status.Committed) + return sys +} + +func (cmd *putCurrentValidatorCommand) NextState(cmdState commands.State) commands.State { + sTx := cmd.sTx + stakerTx := sTx.Unsigned.(txs.StakerTx) + startTime := sTx.Unsigned.(txs.ScheduledStaker).StartTime() + + currentVal, err := NewCurrentStaker(sTx.ID(), stakerTx, startTime, uint64(1000)) + if err != nil { + return cmdState // state checks later on should spot missing validator + } + + cmdState.(*stakersStorageModel).PutCurrentValidator(currentVal) + return cmdState +} + +func (*putCurrentValidatorCommand) PreCondition(commands.State) bool { + // We allow inserting the same validator twice + return true +} + +func (cmd *putCurrentValidatorCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (cmd *putCurrentValidatorCommand) String() string { + stakerTx := cmd.sTx.Unsigned.(txs.StakerTx) + return fmt.Sprintf("\nputCurrentValidator(subnetID: %v, nodeID: %v, txID: %v, priority: %v, unixStartTime: %v, unixEndTime: %v)", + stakerTx.SubnetID(), + stakerTx.NodeID(), + cmd.sTx.TxID, + stakerTx.CurrentPriority(), + stakerTx.(txs.ScheduledStaker).StartTime().Unix(), + stakerTx.EndTime().Unix(), + ) +} + +var genPutCurrentValidatorCommand = addPermissionlessValidatorTxGenerator(commandsCtx, nil, nil, 1000).Map( + func(nonInitTx *txs.Tx) commands.Command { + sTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + cmd := &putCurrentValidatorCommand{ + sTx: sTx, + err: nil, + } + return cmd + }, +) + +// DeleteCurrentValidator section +type deleteCurrentValidatorCommand struct { + err error +} + +func (cmd *deleteCurrentValidatorCommand) Run(sut commands.SystemUnderTest) commands.Result { + // delete first validator without delegators, if any + sys := sut.(*sysUnderTest) + + if err := sys.checkThereIsADiff(); err != nil { + return sys // state checks later on should spot missing validator + } + + topDiff := sys.getTopChainState() + + stakerIt, err := topDiff.GetCurrentStakerIterator() + if err != nil { + cmd.err = err + return sys + } + + var ( + found = false + validator *Staker + ) + for stakerIt.Next() { + validator = stakerIt.Value() + if !validator.Priority.IsCurrentValidator() { + continue // checks next validator + } + + // check validator has no delegators + delIt, err := topDiff.GetCurrentDelegatorIterator(validator.SubnetID, validator.NodeID) + if err != nil { + cmd.err = err + stakerIt.Release() + return sys + } + + hadDelegator := delIt.Next() + delIt.Release() + if !hadDelegator { + found = true + break // found + } else { + continue // checks next validator + } + } + + if !found { + stakerIt.Release() + return sys // no current validator to delete + } + stakerIt.Release() // release before modifying stakers collection + + topDiff.DeleteCurrentValidator(validator) + return sys // returns sys to allow comparison with state in PostCondition +} + +func (cmd *deleteCurrentValidatorCommand) NextState(cmdState commands.State) commands.State { + // delete first validator without delegators, if any + model := cmdState.(*stakersStorageModel) + stakerIt, err := model.GetCurrentStakerIterator() + if err != nil { + cmd.err = err + return cmdState + } + + var ( + found = false + validator *Staker + ) + for stakerIt.Next() { + validator = stakerIt.Value() + if !validator.Priority.IsCurrentValidator() { + continue // checks next validator + } + + // check validator has no delegators + delIt, err := model.GetCurrentDelegatorIterator(validator.SubnetID, validator.NodeID) + if err != nil { + cmd.err = err + stakerIt.Release() + return cmdState + } + + hadDelegator := delIt.Next() + delIt.Release() + if !hadDelegator { + found = true + break // found + } else { + continue // checks next validator + } + } + + if !found { + stakerIt.Release() + return cmdState // no current validator to add delegator to + } + stakerIt.Release() // release before modifying stakers collection + + model.DeleteCurrentValidator(validator) + return cmdState +} + +func (*deleteCurrentValidatorCommand) PreCondition(commands.State) bool { + // We allow deleting an un-existing validator + return true +} + +func (cmd *deleteCurrentValidatorCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (*deleteCurrentValidatorCommand) String() string { + return "\ndeleteCurrentValidator" +} + +// a trick to force command regeneration at each sampling. +// gen.Const would not allow it +var genDeleteCurrentValidatorCommand = gen.IntRange(1, 2).Map( + func(int) commands.Command { + return &deleteCurrentValidatorCommand{} + }, +) + +// PutCurrentDelegator section +type putCurrentDelegatorCommand struct { + sTx *txs.Tx + err error +} + +func (cmd *putCurrentDelegatorCommand) Run(sut commands.SystemUnderTest) commands.Result { + candidateDelegator := cmd.sTx + sys := sut.(*sysUnderTest) + + if err := sys.checkThereIsADiff(); err != nil { + return sys // state checks later on should spot missing validator + } + + err := addCurrentDelegatorInSystem(sys, candidateDelegator.Unsigned) + if err != nil { + cmd.err = err + } + return sys +} + +func addCurrentDelegatorInSystem(sys *sysUnderTest, candidateDelegatorTx txs.UnsignedTx) error { + // 1. check if there is a current validator, already inserted. If not return + // 2. Update candidateDelegatorTx attributes to make it delegator of selected validator + // 3. Add delegator to picked validator + chain := sys.getTopChainState() + + // 1. check if there is a current validator. If not, nothing to do + stakerIt, err := chain.GetCurrentStakerIterator() + if err != nil { + return err + } + + var ( + found = false + validator *Staker + ) + for !found && stakerIt.Next() { + validator = stakerIt.Value() + if validator.Priority.IsCurrentValidator() { + found = true + break + } + } + if !found { + stakerIt.Release() + return nil // no current validator to add delegator to + } + stakerIt.Release() // release before modifying stakers collection + + // 2. Add a delegator to it + addPermissionlessDelTx := candidateDelegatorTx.(*txs.AddPermissionlessDelegatorTx) + addPermissionlessDelTx.Subnet = validator.SubnetID + addPermissionlessDelTx.Validator.NodeID = validator.NodeID + + signedTx, err := txs.NewSigned(addPermissionlessDelTx, txs.Codec, nil) + if err != nil { + return fmt.Errorf("failed signing tx, %w", err) + } + + startTime := signedTx.Unsigned.(txs.ScheduledStaker).StartTime() + delegator, err := NewCurrentStaker(signedTx.ID(), signedTx.Unsigned.(txs.Staker), startTime, uint64(1000)) + if err != nil { + return fmt.Errorf("failed generating staker, %w", err) + } + + chain.PutCurrentDelegator(delegator) + chain.AddTx(signedTx, status.Committed) + return nil +} + +func (cmd *putCurrentDelegatorCommand) NextState(cmdState commands.State) commands.State { + candidateDelegator := cmd.sTx + model := cmdState.(*stakersStorageModel) + err := addCurrentDelegatorInModel(model, candidateDelegator.Unsigned) + if err != nil { + cmd.err = err + } + return cmdState +} + +func addCurrentDelegatorInModel(model *stakersStorageModel, candidateDelegatorTx txs.UnsignedTx) error { + // 1. check if there is a current validator, already inserted. If not return + // 2. Update candidateDelegator attributes to make it delegator of selected validator + // 3. Add delegator to picked validator + + // 1. check if there is a current validator. If not, nothing to do + stakerIt, err := model.GetCurrentStakerIterator() + if err != nil { + return err + } + + var ( + found = false + validator *Staker + ) + for !found && stakerIt.Next() { + validator = stakerIt.Value() + if validator.Priority.IsCurrentValidator() { + found = true + break + } + } + if !found { + stakerIt.Release() + return nil // no current validator to add delegator to + } + stakerIt.Release() // release before modifying stakers collection + + // 2. Add a delegator to it + addPermissionlessDelTx := candidateDelegatorTx.(*txs.AddPermissionlessDelegatorTx) + addPermissionlessDelTx.Subnet = validator.SubnetID + addPermissionlessDelTx.Validator.NodeID = validator.NodeID + + signedTx, err := txs.NewSigned(addPermissionlessDelTx, txs.Codec, nil) + if err != nil { + return fmt.Errorf("failed signing tx, %w", err) + } + + startTime := signedTx.Unsigned.(txs.ScheduledStaker).StartTime() + delegator, err := NewCurrentStaker(signedTx.ID(), signedTx.Unsigned.(txs.Staker), startTime, uint64(1000)) + if err != nil { + return fmt.Errorf("failed generating staker, %w", err) + } + + model.PutCurrentDelegator(delegator) + return nil +} + +func (*putCurrentDelegatorCommand) PreCondition(commands.State) bool { + return true +} + +func (cmd *putCurrentDelegatorCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (cmd *putCurrentDelegatorCommand) String() string { + stakerTx := cmd.sTx.Unsigned.(txs.StakerTx) + return fmt.Sprintf("\nputCurrentDelegator(subnetID: %v, nodeID: %v, txID: %v, priority: %v, unixStartTime: %v, unixEndTime: %v)", + stakerTx.SubnetID(), + stakerTx.NodeID(), + cmd.sTx.TxID, + stakerTx.CurrentPriority(), + stakerTx.(txs.ScheduledStaker).StartTime().Unix(), + stakerTx.EndTime().Unix()) +} + +var genPutCurrentDelegatorCommand = addPermissionlessDelegatorTxGenerator(commandsCtx, nil, nil, 1000).Map( + func(nonInitTx *txs.Tx) commands.Command { + sTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx, %w", err)) + } + + cmd := &putCurrentDelegatorCommand{ + sTx: sTx, + } + return cmd + }, +) + +// DeleteCurrentDelegator section +type deleteCurrentDelegatorCommand struct { + err error +} + +func (cmd *deleteCurrentDelegatorCommand) Run(sut commands.SystemUnderTest) commands.Result { + // delete first delegator, if any + sys := sut.(*sysUnderTest) + + if err := sys.checkThereIsADiff(); err != nil { + return sys // state checks later on should spot missing validator + } + + _, err := deleteCurrentDelegator(sys) + if err != nil { + cmd.err = err + } + return sys // returns sys to allow comparison with state in PostCondition +} + +func deleteCurrentDelegator(sys *sysUnderTest) (bool, error) { + // delete first validator, if any + topDiff := sys.getTopChainState() + + stakerIt, err := topDiff.GetCurrentStakerIterator() + if err != nil { + return false, err + } + + var ( + found = false + delegator *Staker + ) + for !found && stakerIt.Next() { + delegator = stakerIt.Value() + if delegator.Priority.IsCurrentDelegator() { + found = true + break + } + } + if !found { + stakerIt.Release() + return false, nil // no current validator to delete + } + stakerIt.Release() // release before modifying stakers collection + + topDiff.DeleteCurrentDelegator(delegator) + return true, nil +} + +func (*deleteCurrentDelegatorCommand) NextState(cmdState commands.State) commands.State { + model := cmdState.(*stakersStorageModel) + stakerIt, err := model.GetCurrentStakerIterator() + if err != nil { + return err + } + + var ( + found = false + delegator *Staker + ) + for !found && stakerIt.Next() { + delegator = stakerIt.Value() + if delegator.Priority.IsCurrentDelegator() { + found = true + break + } + } + if !found { + stakerIt.Release() + return cmdState // no current validator to add delegator to + } + stakerIt.Release() // release before modifying stakers collection + + model.DeleteCurrentDelegator(delegator) + return cmdState +} + +func (*deleteCurrentDelegatorCommand) PreCondition(commands.State) bool { + return true +} + +func (cmd *deleteCurrentDelegatorCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (*deleteCurrentDelegatorCommand) String() string { + return "\ndeleteCurrentDelegator" +} + +// a trick to force command regeneration at each sampling. +// gen.Const would not allow it +var genDeleteCurrentDelegatorCommand = gen.IntRange(1, 2).Map( + func(int) commands.Command { + return &deleteCurrentDelegatorCommand{} + }, +) + +// addTopDiffCommand section +type addTopDiffCommand struct { + err error +} + +func (cmd *addTopDiffCommand) Run(sut commands.SystemUnderTest) commands.Result { + sys := sut.(*sysUnderTest) + err := sys.addDiffOnTop() + if err != nil { + cmd.err = err + } + return sys +} + +func (*addTopDiffCommand) NextState(cmdState commands.State) commands.State { + return cmdState // model has no diffs +} + +func (*addTopDiffCommand) PreCondition(commands.State) bool { + return true +} + +func (cmd *addTopDiffCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (*addTopDiffCommand) String() string { + return "\naddTopDiffCommand" +} + +// a trick to force command regeneration at each sampling. +// gen.Const would not allow it +var genAddTopDiffCommand = gen.IntRange(1, 2).Map( + func(int) commands.Command { + return &addTopDiffCommand{} + }, +) + +// applyAndCommitBottomDiffCommand section +type applyAndCommitBottomDiffCommand struct { + err error +} + +func (cmd *applyAndCommitBottomDiffCommand) Run(sut commands.SystemUnderTest) commands.Result { + sys := sut.(*sysUnderTest) + if _, err := sys.flushBottomDiff(); err != nil { + cmd.err = err + return sys + } + + if err := sys.baseState.Commit(); err != nil { + cmd.err = err + return sys + } + + return sys +} + +func (*applyAndCommitBottomDiffCommand) NextState(cmdState commands.State) commands.State { + return cmdState // model has no diffs +} + +func (*applyAndCommitBottomDiffCommand) PreCondition(commands.State) bool { + return true +} + +func (cmd *applyAndCommitBottomDiffCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (*applyAndCommitBottomDiffCommand) String() string { + return "\napplyAndCommitBottomDiffCommand" +} + +// a trick to force command regeneration at each sampling. +// gen.Const would not allow it +var genApplyAndCommitBottomDiffCommand = gen.IntRange(1, 2).Map( + func(int) commands.Command { + return &applyAndCommitBottomDiffCommand{} + }, +) + +// rebuildStateCommand section +type rebuildStateCommand struct { + err error +} + +func (cmd *rebuildStateCommand) Run(sut commands.SystemUnderTest) commands.Result { + sys := sut.(*sysUnderTest) + + // 1. Persist all outstanding changes + for { + diffFound, err := sys.flushBottomDiff() + if err != nil { + cmd.err = err + return sys + } + if !diffFound { + break + } + + if err := sys.baseState.Commit(); err != nil { + cmd.err = err + return sys + } + } + + if err := sys.baseState.Commit(); err != nil { + cmd.err = err + return sys + } + + // 2. Rebuild the state from the db + baseState, err := buildChainState(sys.baseDB, nil) + if err != nil { + cmd.err = err + return sys + } + sys.baseState = baseState + sys.diffsMap = map[ids.ID]Diff{} + sys.sortedDiffIDs = []ids.ID{} + + return sys +} + +func (*rebuildStateCommand) NextState(cmdState commands.State) commands.State { + return cmdState // model has no diffs +} + +func (*rebuildStateCommand) PreCondition(commands.State) bool { + return true +} + +func (cmd *rebuildStateCommand) PostCondition(cmdState commands.State, res commands.Result) *gopter.PropResult { + if cmd.err != nil { + cmd.err = nil // reset for next runs + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkSystemAndModelContent(cmdState, res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + if !checkValidatorSetContent(res) { + return &gopter.PropResult{Status: gopter.PropFalse} + } + + return &gopter.PropResult{Status: gopter.PropTrue} +} + +func (*rebuildStateCommand) String() string { + return "\nrebuildStateCommand" +} + +// a trick to force command regeneration at each sampling. +// gen.Const would not allow it +var genRebuildStateCommand = gen.IntRange(1, 2).Map( + func(int) commands.Command { + return &rebuildStateCommand{} + }, +) + +func checkSystemAndModelContent(cmdState commands.State, res commands.Result) bool { + model := cmdState.(*stakersStorageModel) + sys := res.(*sysUnderTest) + + // top view content must always match model content + topDiff := sys.getTopChainState() + + modelIt, err := model.GetCurrentStakerIterator() + if err != nil { + return false + } + sysIt, err := topDiff.GetCurrentStakerIterator() + if err != nil { + return false + } + + modelStakers := make([]*Staker, 0) + for modelIt.Next() { + modelStakers = append(modelStakers, modelIt.Value()) + } + modelIt.Release() + + sysStakers := make([]*Staker, 0) + for sysIt.Next() { + sysStakers = append(sysStakers, sysIt.Value()) + } + sysIt.Release() + + if len(modelStakers) != len(sysStakers) { + return false + } + + for idx, modelStaker := range modelStakers { + sysStaker := sysStakers[idx] + if modelStaker == nil || sysStaker == nil || !reflect.DeepEqual(modelStaker, sysStaker) { + return false + } + } + + return true +} + +// checkValidatorSetContent compares ValidatorsSet with P-chain base-state data and +// makes sure they are coherent. +func checkValidatorSetContent(res commands.Result) bool { + sys := res.(*sysUnderTest) + valSet := sys.baseState.(*state).cfg.Validators + + sysIt, err := sys.baseState.GetCurrentStakerIterator() + if err != nil { + return false + } + + // valContent subnetID -> nodeID -> aggregate weight (validator's own weight + delegators' weight) + valContent := make(map[ids.ID]map[ids.NodeID]uint64) + for sysIt.Next() { + val := sysIt.Value() + if val.SubnetID != constants.PrimaryNetworkID { + continue + } + nodes, found := valContent[val.SubnetID] + if !found { + nodes = make(map[ids.NodeID]uint64) + valContent[val.SubnetID] = nodes + } + nodes[val.NodeID] += val.Weight + } + sysIt.Release() + + for subnetID, nodes := range valContent { + for nodeID, weight := range nodes { + val, found := valSet.GetValidator(subnetID, nodeID) + if !found { + return false + } + if weight != val.Weight { + return false + } + } + } + return true +} + +type sysUnderTest struct { + diffBlkIDSeed uint64 + baseDB database.Database + baseState State + sortedDiffIDs []ids.ID + diffsMap map[ids.ID]Diff +} + +func newSysUnderTest(baseDB database.Database, baseState State) *sysUnderTest { + sys := &sysUnderTest{ + baseDB: baseDB, + baseState: baseState, + diffsMap: map[ids.ID]Diff{}, + sortedDiffIDs: []ids.ID{}, + } + return sys +} + +func (s *sysUnderTest) GetState(blkID ids.ID) (Chain, bool) { + if state, found := s.diffsMap[blkID]; found { + return state, found + } + return s.baseState, blkID == s.baseState.GetLastAccepted() +} + +func (s *sysUnderTest) addDiffOnTop() error { + newTopBlkID := ids.Empty.Prefix(atomic.AddUint64(&s.diffBlkIDSeed, 1)) + var topBlkID ids.ID + if len(s.sortedDiffIDs) == 0 { + topBlkID = s.baseState.GetLastAccepted() + } else { + topBlkID = s.sortedDiffIDs[len(s.sortedDiffIDs)-1] + } + newTopDiff, err := NewDiff(topBlkID, s) + if err != nil { + return err + } + s.sortedDiffIDs = append(s.sortedDiffIDs, newTopBlkID) + s.diffsMap[newTopBlkID] = newTopDiff + return nil +} + +// getTopChainState returns top diff or baseState +func (s *sysUnderTest) getTopChainState() Chain { + var topChainStateID ids.ID + if len(s.sortedDiffIDs) != 0 { + topChainStateID = s.sortedDiffIDs[len(s.sortedDiffIDs)-1] + } else { + topChainStateID = s.baseState.GetLastAccepted() + } + + topChainState, _ := s.GetState(topChainStateID) + return topChainState +} + +// flushBottomDiff applies bottom diff if available +func (s *sysUnderTest) flushBottomDiff() (bool, error) { + if len(s.sortedDiffIDs) == 0 { + return false, nil + } + bottomDiffID := s.sortedDiffIDs[0] + diffToApply := s.diffsMap[bottomDiffID] + + err := diffToApply.Apply(s.baseState) + if err != nil { + return true, err + } + s.baseState.SetLastAccepted(bottomDiffID) + + s.sortedDiffIDs = s.sortedDiffIDs[1:] + delete(s.diffsMap, bottomDiffID) + return true, nil +} + +// checkThereIsADiff must be called before any stakers op. It makes +// sure that ops are carried out on at least a diff, as it happens +// in production code. +func (s *sysUnderTest) checkThereIsADiff() error { + if len(s.sortedDiffIDs) != 0 { + return nil // there is a diff + } + + return s.addDiffOnTop() +} diff --git a/vms/platformvm/state/stakers_properties_test.go b/vms/platformvm/state/stakers_properties_test.go new file mode 100644 index 000000000000..7813cd9a85ed --- /dev/null +++ b/vms/platformvm/state/stakers_properties_test.go @@ -0,0 +1,571 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + "fmt" + "math" + "reflect" + "testing" + "time" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/gen" + "github.com/leanovate/gopter/prop" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/versiondb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +const ( + pending stakerStatus = 0 + current stakerStatus = 1 +) + +// TestGeneralStakerContainersProperties checks that State and Diff conform our stakersStorageModel. +// TestGeneralStakerContainersProperties tests State and Diff in isolation, over simple operations. +// TestStateAndDiffComparisonToStorageModel carries a more involved verification over a production-like +// mix of State and Diffs. +func TestGeneralStakerContainersProperties(t *testing.T) { + storeCreators := map[string]func() (Stakers, error){ + "base state": func() (Stakers, error) { + baseDB := versiondb.New(memdb.New()) + return buildChainState(baseDB, nil) + }, + "diff": func() (Stakers, error) { + diff, _, err := buildDiffOnTopOfBaseState(nil) + return diff, err + }, + "storage model": func() (Stakers, error) { //nolint:golint,unparam + return newStakersStorageModel(), nil + }, + } + + for storeType, storeCreatorF := range storeCreators { + t.Run(storeType, func(t *testing.T) { + properties := generalStakerContainersProperties(storeCreatorF) + properties.TestingRun(t) + }) + } +} + +func generalStakerContainersProperties(storeCreatorF func() (Stakers, error)) *gopter.Properties { + properties := gopter.NewProperties(nil) + + ctx := snowtest.Context(&testing.T{}, snowtest.PChainID) + dummyStartTime := time.Now() + + properties.Property("add, delete and query current validators", prop.ForAll( + func(nonInitTx *txs.Tx) string { + store, err := storeCreatorF() + if err != nil { + return fmt.Sprintf("unexpected error while creating staker store, err %v", err) + } + + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + stakerTx := signedTx.Unsigned.(txs.StakerTx) + staker, err := NewCurrentStaker(signedTx.ID(), stakerTx, dummyStartTime, uint64(100)) + if err != nil { + return err.Error() + } + + // no staker before insertion + _, err = store.GetCurrentValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, current) + if err != nil { + return err.Error() + } + + // it's fine deleting unknown validator + store.DeleteCurrentValidator(staker) + _, err = store.GetCurrentValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, current) + if err != nil { + return err.Error() + } + + // insert the staker and show it can be found + store.PutCurrentValidator(staker) + retrievedStaker, err := store.GetCurrentValidator(staker.SubnetID, staker.NodeID) + if err != nil { + return fmt.Sprintf("expected no error, got %v", err) + } + if !reflect.DeepEqual(staker, retrievedStaker) { + return fmt.Sprintf("wrong staker retrieved expected %v, got %v", staker, retrievedStaker) + } + err = checkStakersContent(store, []*Staker{staker}, current) + if err != nil { + return err.Error() + } + + // delete the staker and show it's not found anymore + store.DeleteCurrentValidator(staker) + _, err = store.GetCurrentValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, current) + if err != nil { + return err.Error() + } + + return "" + }, + stakerTxGenerator(ctx, permissionedValidator, &constants.PrimaryNetworkID, nil, math.MaxUint64), + )) + + properties.Property("add, delete and query pending validators", prop.ForAll( + func(nonInitTx *txs.Tx) string { + store, err := storeCreatorF() + if err != nil { + return fmt.Sprintf("unexpected error while creating staker store, err %v", err) + } + + signedTx, err := txs.NewSigned(nonInitTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + staker, err := NewPendingStaker(signedTx.ID(), signedTx.Unsigned.(txs.ScheduledStaker)) + if err != nil { + return err.Error() + } + + // no staker before insertion + _, err = store.GetPendingValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, pending) + if err != nil { + return err.Error() + } + + // it's fine deleting unknown validator + store.DeletePendingValidator(staker) + _, err = store.GetPendingValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, pending) + if err != nil { + return err.Error() + } + + // insert the staker and show it can be found + store.PutPendingValidator(staker) + retrievedStaker, err := store.GetPendingValidator(staker.SubnetID, staker.NodeID) + if err != nil { + return fmt.Sprintf("expected no error, got %v", err) + } + if !reflect.DeepEqual(staker, retrievedStaker) { + return fmt.Sprintf("wrong staker retrieved expected %v, got %v", staker, retrievedStaker) + } + err = checkStakersContent(store, []*Staker{staker}, pending) + if err != nil { + return err.Error() + } + + // delete the staker and show it's found anymore + store.DeletePendingValidator(staker) + _, err = store.GetPendingValidator(staker.SubnetID, staker.NodeID) + if err != database.ErrNotFound { + return fmt.Sprintf("unexpected error %v, got %v", database.ErrNotFound, err) + } + err = checkStakersContent(store, []*Staker{}, pending) + if err != nil { + return err.Error() + } + + return "" + }, + stakerTxGenerator(ctx, permissionedValidator, &constants.PrimaryNetworkID, nil, math.MaxUint64), + )) + + var ( + subnetID = ids.GenerateTestID() + nodeID = ids.GenerateTestNodeID() + ) + properties.Property("add, delete and query current delegators", prop.ForAll( + func(nonInitValTx *txs.Tx, nonInitDelTxs []*txs.Tx) string { + store, err := storeCreatorF() + if err != nil { + return fmt.Sprintf("unexpected error while creating staker store, err %v", err) + } + + signedValTx, err := txs.NewSigned(nonInitValTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + val, err := NewCurrentStaker(signedValTx.ID(), signedValTx.Unsigned.(txs.StakerTx), dummyStartTime, uint64(1000)) + if err != nil { + return err.Error() + } + + dels := make([]*Staker, 0, len(nonInitDelTxs)) + for _, nonInitDelTx := range nonInitDelTxs { + signedDelTx, err := txs.NewSigned(nonInitDelTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + del, err := NewCurrentStaker(signedDelTx.ID(), signedDelTx.Unsigned.(txs.StakerTx), dummyStartTime, uint64(1000)) + if err != nil { + return err.Error() + } + + dels = append(dels, del) + } + + // store validator + store.PutCurrentValidator(val) + retrievedValidator, err := store.GetCurrentValidator(val.SubnetID, val.NodeID) + if err != nil { + return fmt.Sprintf("expected no error, got %v", err) + } + if !reflect.DeepEqual(val, retrievedValidator) { + return fmt.Sprintf("wrong staker retrieved expected %v, got %v", &val, retrievedValidator) + } + err = checkStakersContent(store, []*Staker{val}, current) + if err != nil { + return err.Error() + } + + // store delegators + for _, del := range dels { + cpy := *del + + // it's fine deleting unknown delegator + store.DeleteCurrentDelegator(&cpy) + + // finally store the delegator + store.PutCurrentDelegator(&cpy) + } + + // check no missing delegators by subnetID, nodeID + for _, del := range dels { + found := false + delIt, err := store.GetCurrentDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in current delegators iterator creation, error %v", err) + } + for delIt.Next() { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + delIt.Release() + + if !found { + return fmt.Sprintf("missing delegator %v", del) + } + } + + // check no extra delegator by subnetID, nodeID + delIt, err := store.GetCurrentDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in current delegators iterator creation, error %v", err) + } + for delIt.Next() { + found := false + for _, del := range dels { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + if !found { + return fmt.Sprintf("found extra delegator %v", delIt.Value()) + } + } + delIt.Release() + + // check no missing delegators in the whole staker set + stakersSet := dels + stakersSet = append(stakersSet, val) + err = checkStakersContent(store, stakersSet, current) + if err != nil { + return err.Error() + } + + // delete delegators + for _, del := range dels { + cpy := *del + store.DeleteCurrentDelegator(&cpy) + + // check deleted delegator is not there anymore + delIt, err := store.GetCurrentDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in current delegators iterator creation, error %v", err) + } + + found := false + for delIt.Next() { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + delIt.Release() + if found { + return fmt.Sprintf("found deleted delegator %v", del) + } + } + + return "" + }, + stakerTxGenerator(ctx, + permissionlessValidator, + &subnetID, + &nodeID, + math.MaxUint64, + ), + gen.SliceOfN(10, + stakerTxGenerator(ctx, + permissionlessDelegator, + &subnetID, + &nodeID, + 1000, + ), + ), + )) + + properties.Property("add, delete and query pending delegators", prop.ForAll( + func(nonInitValTx *txs.Tx, nonInitDelTxs []*txs.Tx) string { + store, err := storeCreatorF() + if err != nil { + return fmt.Sprintf("unexpected error while creating staker store, err %v", err) + } + + signedValTx, err := txs.NewSigned(nonInitValTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + val, err := NewCurrentStaker(signedValTx.ID(), signedValTx.Unsigned.(txs.StakerTx), dummyStartTime, uint64(1000)) + if err != nil { + return err.Error() + } + + dels := make([]*Staker, 0, len(nonInitDelTxs)) + for _, nonInitDelTx := range nonInitDelTxs { + signedDelTx, err := txs.NewSigned(nonInitDelTx.Unsigned, txs.Codec, nil) + if err != nil { + panic(fmt.Errorf("failed signing tx in tx generator, %w", err)) + } + + del, err := NewCurrentStaker(signedDelTx.ID(), signedDelTx.Unsigned.(txs.StakerTx), dummyStartTime, uint64(1000)) + if err != nil { + return err.Error() + } + + dels = append(dels, del) + } + + // store validator + store.PutCurrentValidator(val) + retrievedValidator, err := store.GetCurrentValidator(val.SubnetID, val.NodeID) + if err != nil { + return fmt.Sprintf("expected no error, got %v", err) + } + if !reflect.DeepEqual(val, retrievedValidator) { + return fmt.Sprintf("wrong staker retrieved expected %v, got %v", &val, retrievedValidator) + } + + err = checkStakersContent(store, []*Staker{val}, current) + if err != nil { + return err.Error() + } + + // store delegators + for _, del := range dels { + cpy := *del + + // it's fine deleting unknown delegator + store.DeletePendingDelegator(&cpy) + + // finally store the delegator + store.PutPendingDelegator(&cpy) + } + + // check no missing delegators by subnetID, nodeID + for _, del := range dels { + found := false + delIt, err := store.GetPendingDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in pending delegators iterator creation, error %v", err) + } + for delIt.Next() { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + delIt.Release() + + if !found { + return fmt.Sprintf("missing delegator %v", del) + } + } + + // check no extra delegators by subnetID, nodeID + delIt, err := store.GetPendingDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in pending delegators iterator creation, error %v", err) + } + for delIt.Next() { + found := false + for _, del := range dels { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + if !found { + return fmt.Sprintf("found extra delegator %v", delIt.Value()) + } + } + delIt.Release() + + // check no missing delegators in the whole staker set + err = checkStakersContent(store, dels, pending) + if err != nil { + return err.Error() + } + + // delete delegators + for _, del := range dels { + cpy := *del + store.DeletePendingDelegator(&cpy) + + // check deleted delegator is not there anymore + delIt, err := store.GetPendingDelegatorIterator(subnetID, nodeID) + if err != nil { + return fmt.Sprintf("unexpected failure in pending delegators iterator creation, error %v", err) + } + + found := false + for delIt.Next() { + if reflect.DeepEqual(delIt.Value(), del) { + found = true + break + } + } + delIt.Release() + if found { + return fmt.Sprintf("found deleted delegator %v", del) + } + } + + return "" + }, + stakerTxGenerator(ctx, + permissionlessValidator, + &subnetID, + &nodeID, + math.MaxUint64, + ), + gen.SliceOfN(10, + stakerTxGenerator(ctx, + permissionlessDelegator, + &subnetID, + &nodeID, + 1000, + ), + ), + )) + + return properties +} + +func buildDiffOnTopOfBaseState(trackedSubnets []ids.ID) (Diff, State, error) { + baseDB := versiondb.New(memdb.New()) + baseState, err := buildChainState(baseDB, trackedSubnets) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error while creating chain base state, err %w", err) + } + + genesisID := baseState.GetLastAccepted() + versions := &versionsHolder{ + baseState: baseState, + } + diff, err := NewDiff(genesisID, versions) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error while creating diff, err %w", err) + } + return diff, baseState, nil +} + +// [checkStakersContent] verifies whether store contains exactly the stakers specified in the list. +// stakers order does not matter. stakers slice gets consumed while checking. +func checkStakersContent(store Stakers, stakers []*Staker, stakersType stakerStatus) error { + var ( + it StakerIterator + err error + ) + + switch stakersType { + case current: + it, err = store.GetCurrentStakerIterator() + case pending: + it, err = store.GetPendingStakerIterator() + default: + return errors.New("Unhandled stakers status") + } + if err != nil { + return fmt.Errorf("unexpected failure in staker iterator creation, error %w", err) + } + defer it.Release() + + if len(stakers) == 0 { + if it.Next() { + return fmt.Errorf("expected empty iterator, got at least element %v", it.Value()) + } + return nil + } + + for it.Next() { + var ( + staker = it.Value() + found = false + + retrievedStakerIdx = 0 + ) + + for idx, s := range stakers { + if reflect.DeepEqual(staker, s) { + retrievedStakerIdx = idx + found = true + } + } + if !found { + return fmt.Errorf("found extra staker %v", staker) + } + stakers[retrievedStakerIdx] = stakers[len(stakers)-1] // order does not matter + stakers = stakers[:len(stakers)-1] + } + + if len(stakers) != 0 { + return errors.New("missing stakers") + } + return nil +}