github.com/ava-labs/avalanchego@v1.11.11/vms/platformvm/txs/executor/staker_tx_verification_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package executor
     5  
     6  import (
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  	"go.uber.org/mock/gomock"
    12  
    13  	"github.com/ava-labs/avalanchego/database"
    14  	"github.com/ava-labs/avalanchego/ids"
    15  	"github.com/ava-labs/avalanchego/snow"
    16  	"github.com/ava-labs/avalanchego/snow/snowtest"
    17  	"github.com/ava-labs/avalanchego/upgrade/upgradetest"
    18  	"github.com/ava-labs/avalanchego/utils"
    19  	"github.com/ava-labs/avalanchego/utils/constants"
    20  	"github.com/ava-labs/avalanchego/utils/timer/mockable"
    21  	"github.com/ava-labs/avalanchego/vms/components/avax"
    22  	"github.com/ava-labs/avalanchego/vms/components/verify"
    23  	"github.com/ava-labs/avalanchego/vms/platformvm/config"
    24  	"github.com/ava-labs/avalanchego/vms/platformvm/state"
    25  	"github.com/ava-labs/avalanchego/vms/platformvm/txs"
    26  	"github.com/ava-labs/avalanchego/vms/platformvm/txs/fee"
    27  	"github.com/ava-labs/avalanchego/vms/platformvm/utxo/utxomock"
    28  	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
    29  )
    30  
    31  func TestVerifyAddPermissionlessValidatorTx(t *testing.T) {
    32  	ctx := snowtest.Context(t, snowtest.PChainID)
    33  
    34  	type test struct {
    35  		name        string
    36  		backendF    func(*gomock.Controller) *Backend
    37  		stateF      func(*gomock.Controller) state.Chain
    38  		sTxF        func() *txs.Tx
    39  		txF         func() *txs.AddPermissionlessValidatorTx
    40  		expectedErr error
    41  	}
    42  
    43  	var (
    44  		// in the following tests we set the fork time for forks we want active
    45  		// to activeForkTime, which is ensured to be before any other time related
    46  		// quantity (based on now)
    47  		activeForkTime = time.Unix(0, 0)
    48  		now            = time.Now().Truncate(time.Second) // after activeForkTime
    49  
    50  		subnetID            = ids.GenerateTestID()
    51  		customAssetID       = ids.GenerateTestID()
    52  		unsignedTransformTx = &txs.TransformSubnetTx{
    53  			AssetID:           customAssetID,
    54  			MinValidatorStake: 1,
    55  			MaxValidatorStake: 2,
    56  			MinStakeDuration:  3,
    57  			MaxStakeDuration:  4,
    58  			MinDelegationFee:  5,
    59  		}
    60  		transformTx = txs.Tx{
    61  			Unsigned: unsignedTransformTx,
    62  			Creds:    []verify.Verifiable{},
    63  		}
    64  		// This tx already passed syntactic verification.
    65  		startTime  = now.Add(time.Second)
    66  		endTime    = startTime.Add(time.Second * time.Duration(unsignedTransformTx.MinStakeDuration))
    67  		verifiedTx = txs.AddPermissionlessValidatorTx{
    68  			BaseTx: txs.BaseTx{
    69  				SyntacticallyVerified: true,
    70  				BaseTx: avax.BaseTx{
    71  					NetworkID:    ctx.NetworkID,
    72  					BlockchainID: ctx.ChainID,
    73  					Outs:         []*avax.TransferableOutput{},
    74  					Ins:          []*avax.TransferableInput{},
    75  				},
    76  			},
    77  			Validator: txs.Validator{
    78  				NodeID: ids.GenerateTestNodeID(),
    79  				// Note: [Start] is not set here as it will be ignored
    80  				// Post-Durango in favor of the current chain time
    81  				End:  uint64(endTime.Unix()),
    82  				Wght: unsignedTransformTx.MinValidatorStake,
    83  			},
    84  			Subnet: subnetID,
    85  			StakeOuts: []*avax.TransferableOutput{
    86  				{
    87  					Asset: avax.Asset{
    88  						ID: customAssetID,
    89  					},
    90  				},
    91  			},
    92  			ValidatorRewardsOwner: &secp256k1fx.OutputOwners{
    93  				Addrs:     []ids.ShortID{ids.GenerateTestShortID()},
    94  				Threshold: 1,
    95  			},
    96  			DelegatorRewardsOwner: &secp256k1fx.OutputOwners{
    97  				Addrs:     []ids.ShortID{ids.GenerateTestShortID()},
    98  				Threshold: 1,
    99  			},
   100  			DelegationShares: 20_000,
   101  		}
   102  		verifiedSignedTx = txs.Tx{
   103  			Unsigned: &verifiedTx,
   104  			Creds:    []verify.Verifiable{},
   105  		}
   106  	)
   107  	verifiedSignedTx.SetBytes([]byte{1}, []byte{2})
   108  
   109  	tests := []test{
   110  		{
   111  			name: "fail syntactic verification",
   112  			backendF: func(*gomock.Controller) *Backend {
   113  				return &Backend{
   114  					Ctx: ctx,
   115  					Config: &config.Config{
   116  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   117  					},
   118  				}
   119  			},
   120  
   121  			stateF: func(ctrl *gomock.Controller) state.Chain {
   122  				mockState := state.NewMockChain(ctrl)
   123  				mockState.EXPECT().GetTimestamp().Return(now)
   124  				return mockState
   125  			},
   126  			sTxF: func() *txs.Tx {
   127  				return nil
   128  			},
   129  			txF: func() *txs.AddPermissionlessValidatorTx {
   130  				return nil
   131  			},
   132  			expectedErr: txs.ErrNilSignedTx,
   133  		},
   134  		{
   135  			name: "not bootstrapped",
   136  			backendF: func(*gomock.Controller) *Backend {
   137  				return &Backend{
   138  					Ctx: ctx,
   139  					Config: &config.Config{
   140  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   141  					},
   142  					Bootstrapped: &utils.Atomic[bool]{},
   143  				}
   144  			},
   145  			stateF: func(ctrl *gomock.Controller) state.Chain {
   146  				mockState := state.NewMockChain(ctrl)
   147  				mockState.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after Durango fork activation since now.After(activeForkTime)
   148  				return mockState
   149  			},
   150  			sTxF: func() *txs.Tx {
   151  				return &verifiedSignedTx
   152  			},
   153  			txF: func() *txs.AddPermissionlessValidatorTx {
   154  				return &txs.AddPermissionlessValidatorTx{}
   155  			},
   156  			expectedErr: nil,
   157  		},
   158  		{
   159  			name: "start time too early",
   160  			backendF: func(*gomock.Controller) *Backend {
   161  				bootstrapped := &utils.Atomic[bool]{}
   162  				bootstrapped.Set(true)
   163  				return &Backend{
   164  					Ctx: ctx,
   165  					Config: &config.Config{
   166  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Cortina, activeForkTime),
   167  					},
   168  					Bootstrapped: bootstrapped,
   169  				}
   170  			},
   171  			stateF: func(ctrl *gomock.Controller) state.Chain {
   172  				state := state.NewMockChain(ctrl)
   173  				state.EXPECT().GetTimestamp().Return(verifiedTx.StartTime()).Times(2)
   174  				return state
   175  			},
   176  			sTxF: func() *txs.Tx {
   177  				return &verifiedSignedTx
   178  			},
   179  			txF: func() *txs.AddPermissionlessValidatorTx {
   180  				return &verifiedTx
   181  			},
   182  			expectedErr: ErrTimestampNotBeforeStartTime,
   183  		},
   184  		{
   185  			name: "weight too low",
   186  			backendF: func(*gomock.Controller) *Backend {
   187  				bootstrapped := &utils.Atomic[bool]{}
   188  				bootstrapped.Set(true)
   189  				return &Backend{
   190  					Ctx: ctx,
   191  					Config: &config.Config{
   192  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   193  					},
   194  					Bootstrapped: bootstrapped,
   195  				}
   196  			},
   197  			stateF: func(ctrl *gomock.Controller) state.Chain {
   198  				state := state.NewMockChain(ctrl)
   199  				state.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   200  				state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   201  				return state
   202  			},
   203  			sTxF: func() *txs.Tx {
   204  				return &verifiedSignedTx
   205  			},
   206  			txF: func() *txs.AddPermissionlessValidatorTx {
   207  				tx := verifiedTx // Note that this copies [verifiedTx]
   208  				tx.Validator.Wght = unsignedTransformTx.MinValidatorStake - 1
   209  				return &tx
   210  			},
   211  			expectedErr: ErrWeightTooSmall,
   212  		},
   213  		{
   214  			name: "weight too high",
   215  			backendF: func(*gomock.Controller) *Backend {
   216  				bootstrapped := &utils.Atomic[bool]{}
   217  				bootstrapped.Set(true)
   218  				return &Backend{
   219  					Ctx: ctx,
   220  					Config: &config.Config{
   221  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   222  					},
   223  					Bootstrapped: bootstrapped,
   224  				}
   225  			},
   226  			stateF: func(ctrl *gomock.Controller) state.Chain {
   227  				state := state.NewMockChain(ctrl)
   228  				state.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   229  				state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   230  				return state
   231  			},
   232  			sTxF: func() *txs.Tx {
   233  				return &verifiedSignedTx
   234  			},
   235  			txF: func() *txs.AddPermissionlessValidatorTx {
   236  				tx := verifiedTx // Note that this copies [verifiedTx]
   237  				tx.Validator.Wght = unsignedTransformTx.MaxValidatorStake + 1
   238  				return &tx
   239  			},
   240  			expectedErr: ErrWeightTooLarge,
   241  		},
   242  		{
   243  			name: "insufficient delegation fee",
   244  			backendF: func(*gomock.Controller) *Backend {
   245  				bootstrapped := &utils.Atomic[bool]{}
   246  				bootstrapped.Set(true)
   247  				return &Backend{
   248  					Ctx: ctx,
   249  					Config: &config.Config{
   250  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   251  					},
   252  					Bootstrapped: bootstrapped,
   253  				}
   254  			},
   255  			stateF: func(ctrl *gomock.Controller) state.Chain {
   256  				state := state.NewMockChain(ctrl)
   257  				state.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   258  				state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   259  				return state
   260  			},
   261  			sTxF: func() *txs.Tx {
   262  				return &verifiedSignedTx
   263  			},
   264  			txF: func() *txs.AddPermissionlessValidatorTx {
   265  				tx := verifiedTx // Note that this copies [verifiedTx]
   266  				tx.Validator.Wght = unsignedTransformTx.MaxValidatorStake
   267  				tx.DelegationShares = unsignedTransformTx.MinDelegationFee - 1
   268  				return &tx
   269  			},
   270  			expectedErr: ErrInsufficientDelegationFee,
   271  		},
   272  		{
   273  			name: "duration too short",
   274  			backendF: func(*gomock.Controller) *Backend {
   275  				bootstrapped := &utils.Atomic[bool]{}
   276  				bootstrapped.Set(true)
   277  				return &Backend{
   278  					Ctx: ctx,
   279  					Config: &config.Config{
   280  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   281  					},
   282  					Bootstrapped: bootstrapped,
   283  				}
   284  			},
   285  			stateF: func(ctrl *gomock.Controller) state.Chain {
   286  				state := state.NewMockChain(ctrl)
   287  				state.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   288  				state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   289  				return state
   290  			},
   291  			sTxF: func() *txs.Tx {
   292  				return &verifiedSignedTx
   293  			},
   294  			txF: func() *txs.AddPermissionlessValidatorTx {
   295  				tx := verifiedTx // Note that this copies [verifiedTx]
   296  				tx.Validator.Wght = unsignedTransformTx.MaxValidatorStake
   297  				tx.DelegationShares = unsignedTransformTx.MinDelegationFee
   298  
   299  				// Note the duration is 1 less than the minimum
   300  				tx.Validator.End = tx.Validator.Start + uint64(unsignedTransformTx.MinStakeDuration) - 1
   301  				return &tx
   302  			},
   303  			expectedErr: ErrStakeTooShort,
   304  		},
   305  		{
   306  			name: "duration too long",
   307  			backendF: func(*gomock.Controller) *Backend {
   308  				bootstrapped := &utils.Atomic[bool]{}
   309  				bootstrapped.Set(true)
   310  				return &Backend{
   311  					Ctx: ctx,
   312  					Config: &config.Config{
   313  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   314  					},
   315  					Bootstrapped: bootstrapped,
   316  				}
   317  			},
   318  			stateF: func(ctrl *gomock.Controller) state.Chain {
   319  				state := state.NewMockChain(ctrl)
   320  				state.EXPECT().GetTimestamp().Return(time.Unix(1, 0)).Times(2) // chain time is after fork activation since time.Unix(1, 0).After(activeForkTime)
   321  				state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   322  				return state
   323  			},
   324  			sTxF: func() *txs.Tx {
   325  				return &verifiedSignedTx
   326  			},
   327  			txF: func() *txs.AddPermissionlessValidatorTx {
   328  				tx := verifiedTx // Note that this copies [verifiedTx]
   329  				tx.Validator.Wght = unsignedTransformTx.MaxValidatorStake
   330  				tx.DelegationShares = unsignedTransformTx.MinDelegationFee
   331  
   332  				// Note the duration is more than the maximum
   333  				tx.Validator.End = uint64(unsignedTransformTx.MaxStakeDuration) + 2
   334  				return &tx
   335  			},
   336  			expectedErr: ErrStakeTooLong,
   337  		},
   338  		{
   339  			name: "wrong assetID",
   340  			backendF: func(*gomock.Controller) *Backend {
   341  				bootstrapped := &utils.Atomic[bool]{}
   342  				bootstrapped.Set(true)
   343  				return &Backend{
   344  					Ctx: ctx,
   345  					Config: &config.Config{
   346  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   347  					},
   348  					Bootstrapped: bootstrapped,
   349  				}
   350  			},
   351  			stateF: func(ctrl *gomock.Controller) state.Chain {
   352  				mockState := state.NewMockChain(ctrl)
   353  				mockState.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   354  				mockState.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   355  				return mockState
   356  			},
   357  			sTxF: func() *txs.Tx {
   358  				return &verifiedSignedTx
   359  			},
   360  			txF: func() *txs.AddPermissionlessValidatorTx {
   361  				tx := verifiedTx // Note that this copies [verifiedTx]
   362  				tx.StakeOuts = []*avax.TransferableOutput{
   363  					{
   364  						Asset: avax.Asset{
   365  							ID: ids.GenerateTestID(),
   366  						},
   367  					},
   368  				}
   369  				return &tx
   370  			},
   371  			expectedErr: ErrWrongStakedAssetID,
   372  		},
   373  		{
   374  			name: "duplicate validator",
   375  			backendF: func(*gomock.Controller) *Backend {
   376  				bootstrapped := &utils.Atomic[bool]{}
   377  				bootstrapped.Set(true)
   378  				return &Backend{
   379  					Ctx: ctx,
   380  					Config: &config.Config{
   381  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   382  					},
   383  					Bootstrapped: bootstrapped,
   384  				}
   385  			},
   386  			stateF: func(ctrl *gomock.Controller) state.Chain {
   387  				mockState := state.NewMockChain(ctrl)
   388  				mockState.EXPECT().GetTimestamp().Return(now).Times(2) // chain time is after latest fork activation since now.After(activeForkTime)
   389  				mockState.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   390  				// State says validator exists
   391  				mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, nil)
   392  				return mockState
   393  			},
   394  			sTxF: func() *txs.Tx {
   395  				return &verifiedSignedTx
   396  			},
   397  			txF: func() *txs.AddPermissionlessValidatorTx {
   398  				return &verifiedTx
   399  			},
   400  			expectedErr: ErrDuplicateValidator,
   401  		},
   402  		{
   403  			name: "validator not subset of primary network validator",
   404  			backendF: func(*gomock.Controller) *Backend {
   405  				bootstrapped := &utils.Atomic[bool]{}
   406  				bootstrapped.Set(true)
   407  				return &Backend{
   408  					Ctx: ctx,
   409  					Config: &config.Config{
   410  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   411  					},
   412  					Bootstrapped: bootstrapped,
   413  				}
   414  			},
   415  			stateF: func(ctrl *gomock.Controller) state.Chain {
   416  				mockState := state.NewMockChain(ctrl)
   417  				mockState.EXPECT().GetTimestamp().Return(now).Times(3) // chain time is after latest fork activation since now.After(activeForkTime)
   418  				mockState.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   419  				mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   420  				mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   421  				// Validator time isn't subset of primary network validator time
   422  				primaryNetworkVdr := &state.Staker{
   423  					EndTime: verifiedTx.EndTime().Add(-1 * time.Second),
   424  				}
   425  				mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil)
   426  				return mockState
   427  			},
   428  			sTxF: func() *txs.Tx {
   429  				return &verifiedSignedTx
   430  			},
   431  			txF: func() *txs.AddPermissionlessValidatorTx {
   432  				return &verifiedTx
   433  			},
   434  			expectedErr: ErrPeriodMismatch,
   435  		},
   436  		{
   437  			name: "flow check fails",
   438  			backendF: func(ctrl *gomock.Controller) *Backend {
   439  				bootstrapped := &utils.Atomic[bool]{}
   440  				bootstrapped.Set(true)
   441  
   442  				flowChecker := utxomock.NewVerifier(ctrl)
   443  				flowChecker.EXPECT().VerifySpend(
   444  					gomock.Any(),
   445  					gomock.Any(),
   446  					gomock.Any(),
   447  					gomock.Any(),
   448  					gomock.Any(),
   449  					gomock.Any(),
   450  				).Return(ErrFlowCheckFailed)
   451  
   452  				return &Backend{
   453  					FlowChecker: flowChecker,
   454  					Config: &config.Config{
   455  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   456  						StaticFeeConfig: fee.StaticConfig{
   457  							AddSubnetValidatorFee: 1,
   458  						},
   459  					},
   460  					Ctx:          ctx,
   461  					Bootstrapped: bootstrapped,
   462  				}
   463  			},
   464  			stateF: func(ctrl *gomock.Controller) state.Chain {
   465  				mockState := state.NewMockChain(ctrl)
   466  				mockState.EXPECT().GetTimestamp().Return(now).Times(3) // chain time is after latest fork activation since now.After(activeForkTime)
   467  				mockState.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   468  				mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   469  				mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   470  				primaryNetworkVdr := &state.Staker{
   471  					EndTime: mockable.MaxTime,
   472  				}
   473  				mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil)
   474  				return mockState
   475  			},
   476  			sTxF: func() *txs.Tx {
   477  				return &verifiedSignedTx
   478  			},
   479  			txF: func() *txs.AddPermissionlessValidatorTx {
   480  				return &verifiedTx
   481  			},
   482  			expectedErr: ErrFlowCheckFailed,
   483  		},
   484  		{
   485  			name: "success",
   486  			backendF: func(ctrl *gomock.Controller) *Backend {
   487  				bootstrapped := &utils.Atomic[bool]{}
   488  				bootstrapped.Set(true)
   489  
   490  				flowChecker := utxomock.NewVerifier(ctrl)
   491  				flowChecker.EXPECT().VerifySpend(
   492  					gomock.Any(),
   493  					gomock.Any(),
   494  					gomock.Any(),
   495  					gomock.Any(),
   496  					gomock.Any(),
   497  					gomock.Any(),
   498  				).Return(nil)
   499  
   500  				return &Backend{
   501  					FlowChecker: flowChecker,
   502  					Config: &config.Config{
   503  						UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, activeForkTime),
   504  						StaticFeeConfig: fee.StaticConfig{
   505  							AddSubnetValidatorFee: 1,
   506  						},
   507  					},
   508  					Ctx:          ctx,
   509  					Bootstrapped: bootstrapped,
   510  				}
   511  			},
   512  			stateF: func(ctrl *gomock.Controller) state.Chain {
   513  				mockState := state.NewMockChain(ctrl)
   514  				mockState.EXPECT().GetTimestamp().Return(now).Times(3) // chain time is after Durango fork activation since now.After(activeForkTime)
   515  				mockState.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil)
   516  				mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   517  				mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound)
   518  				primaryNetworkVdr := &state.Staker{
   519  					EndTime: mockable.MaxTime,
   520  				}
   521  				mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil)
   522  				return mockState
   523  			},
   524  			sTxF: func() *txs.Tx {
   525  				return &verifiedSignedTx
   526  			},
   527  			txF: func() *txs.AddPermissionlessValidatorTx {
   528  				return &verifiedTx
   529  			},
   530  			expectedErr: nil,
   531  		},
   532  	}
   533  
   534  	for _, tt := range tests {
   535  		t.Run(tt.name, func(t *testing.T) {
   536  			ctrl := gomock.NewController(t)
   537  
   538  			var (
   539  				backend = tt.backendF(ctrl)
   540  				chain   = tt.stateF(ctrl)
   541  				sTx     = tt.sTxF()
   542  				tx      = tt.txF()
   543  			)
   544  
   545  			feeCalculator := state.PickFeeCalculator(backend.Config, chain)
   546  			err := verifyAddPermissionlessValidatorTx(backend, feeCalculator, chain, sTx, tx)
   547  			require.ErrorIs(t, err, tt.expectedErr)
   548  		})
   549  	}
   550  }
   551  
   552  func TestGetValidatorRules(t *testing.T) {
   553  	type test struct {
   554  		name          string
   555  		subnetID      ids.ID
   556  		backend       *Backend
   557  		chainStateF   func(*gomock.Controller) state.Chain
   558  		expectedRules *addValidatorRules
   559  		expectedErr   error
   560  	}
   561  
   562  	var (
   563  		config = &config.Config{
   564  			MinValidatorStake: 1,
   565  			MaxValidatorStake: 2,
   566  			MinStakeDuration:  time.Second,
   567  			MaxStakeDuration:  2 * time.Second,
   568  			MinDelegationFee:  1337,
   569  		}
   570  		avaxAssetID   = ids.GenerateTestID()
   571  		customAssetID = ids.GenerateTestID()
   572  		subnetID      = ids.GenerateTestID()
   573  	)
   574  
   575  	tests := []test{
   576  		{
   577  			name:     "primary network",
   578  			subnetID: constants.PrimaryNetworkID,
   579  			backend: &Backend{
   580  				Config: config,
   581  				Ctx: &snow.Context{
   582  					AVAXAssetID: avaxAssetID,
   583  				},
   584  			},
   585  			chainStateF: func(*gomock.Controller) state.Chain {
   586  				return nil
   587  			},
   588  			expectedRules: &addValidatorRules{
   589  				assetID:           avaxAssetID,
   590  				minValidatorStake: config.MinValidatorStake,
   591  				maxValidatorStake: config.MaxValidatorStake,
   592  				minStakeDuration:  config.MinStakeDuration,
   593  				maxStakeDuration:  config.MaxStakeDuration,
   594  				minDelegationFee:  config.MinDelegationFee,
   595  			},
   596  		},
   597  		{
   598  			name:     "can't get subnet transformation",
   599  			subnetID: subnetID,
   600  			backend:  nil,
   601  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   602  				state := state.NewMockChain(ctrl)
   603  				state.EXPECT().GetSubnetTransformation(subnetID).Return(nil, errTest)
   604  				return state
   605  			},
   606  			expectedRules: &addValidatorRules{},
   607  			expectedErr:   errTest,
   608  		},
   609  		{
   610  			name:     "invalid transformation tx",
   611  			subnetID: subnetID,
   612  			backend:  nil,
   613  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   614  				state := state.NewMockChain(ctrl)
   615  				tx := &txs.Tx{
   616  					Unsigned: &txs.AddDelegatorTx{},
   617  				}
   618  				state.EXPECT().GetSubnetTransformation(subnetID).Return(tx, nil)
   619  				return state
   620  			},
   621  			expectedRules: &addValidatorRules{},
   622  			expectedErr:   ErrIsNotTransformSubnetTx,
   623  		},
   624  		{
   625  			name:     "subnet",
   626  			subnetID: subnetID,
   627  			backend:  nil,
   628  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   629  				state := state.NewMockChain(ctrl)
   630  				tx := &txs.Tx{
   631  					Unsigned: &txs.TransformSubnetTx{
   632  						AssetID:           customAssetID,
   633  						MinValidatorStake: config.MinValidatorStake,
   634  						MaxValidatorStake: config.MaxValidatorStake,
   635  						MinStakeDuration:  1337,
   636  						MaxStakeDuration:  42,
   637  						MinDelegationFee:  config.MinDelegationFee,
   638  					},
   639  				}
   640  				state.EXPECT().GetSubnetTransformation(subnetID).Return(tx, nil)
   641  				return state
   642  			},
   643  			expectedRules: &addValidatorRules{
   644  				assetID:           customAssetID,
   645  				minValidatorStake: config.MinValidatorStake,
   646  				maxValidatorStake: config.MaxValidatorStake,
   647  				minStakeDuration:  1337 * time.Second,
   648  				maxStakeDuration:  42 * time.Second,
   649  				minDelegationFee:  config.MinDelegationFee,
   650  			},
   651  			expectedErr: nil,
   652  		},
   653  	}
   654  
   655  	for _, tt := range tests {
   656  		t.Run(tt.name, func(t *testing.T) {
   657  			require := require.New(t)
   658  			ctrl := gomock.NewController(t)
   659  
   660  			chainState := tt.chainStateF(ctrl)
   661  			rules, err := getValidatorRules(tt.backend, chainState, tt.subnetID)
   662  			if tt.expectedErr != nil {
   663  				require.ErrorIs(err, tt.expectedErr)
   664  				return
   665  			}
   666  			require.NoError(err)
   667  			require.Equal(tt.expectedRules, rules)
   668  		})
   669  	}
   670  }
   671  
   672  func TestGetDelegatorRules(t *testing.T) {
   673  	type test struct {
   674  		name          string
   675  		subnetID      ids.ID
   676  		backend       *Backend
   677  		chainStateF   func(*gomock.Controller) state.Chain
   678  		expectedRules *addDelegatorRules
   679  		expectedErr   error
   680  	}
   681  	var (
   682  		config = &config.Config{
   683  			MinDelegatorStake: 1,
   684  			MaxValidatorStake: 2,
   685  			MinStakeDuration:  time.Second,
   686  			MaxStakeDuration:  2 * time.Second,
   687  		}
   688  		avaxAssetID   = ids.GenerateTestID()
   689  		customAssetID = ids.GenerateTestID()
   690  		subnetID      = ids.GenerateTestID()
   691  	)
   692  	tests := []test{
   693  		{
   694  			name:     "primary network",
   695  			subnetID: constants.PrimaryNetworkID,
   696  			backend: &Backend{
   697  				Config: config,
   698  				Ctx: &snow.Context{
   699  					AVAXAssetID: avaxAssetID,
   700  				},
   701  			},
   702  			chainStateF: func(*gomock.Controller) state.Chain {
   703  				return nil
   704  			},
   705  			expectedRules: &addDelegatorRules{
   706  				assetID:                  avaxAssetID,
   707  				minDelegatorStake:        config.MinDelegatorStake,
   708  				maxValidatorStake:        config.MaxValidatorStake,
   709  				minStakeDuration:         config.MinStakeDuration,
   710  				maxStakeDuration:         config.MaxStakeDuration,
   711  				maxValidatorWeightFactor: MaxValidatorWeightFactor,
   712  			},
   713  		},
   714  		{
   715  			name:     "can't get subnet transformation",
   716  			subnetID: subnetID,
   717  			backend:  nil,
   718  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   719  				state := state.NewMockChain(ctrl)
   720  				state.EXPECT().GetSubnetTransformation(subnetID).Return(nil, errTest)
   721  				return state
   722  			},
   723  			expectedRules: &addDelegatorRules{},
   724  			expectedErr:   errTest,
   725  		},
   726  		{
   727  			name:     "invalid transformation tx",
   728  			subnetID: subnetID,
   729  			backend:  nil,
   730  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   731  				state := state.NewMockChain(ctrl)
   732  				tx := &txs.Tx{
   733  					Unsigned: &txs.AddDelegatorTx{},
   734  				}
   735  				state.EXPECT().GetSubnetTransformation(subnetID).Return(tx, nil)
   736  				return state
   737  			},
   738  			expectedRules: &addDelegatorRules{},
   739  			expectedErr:   ErrIsNotTransformSubnetTx,
   740  		},
   741  		{
   742  			name:     "subnet",
   743  			subnetID: subnetID,
   744  			backend:  nil,
   745  			chainStateF: func(ctrl *gomock.Controller) state.Chain {
   746  				state := state.NewMockChain(ctrl)
   747  				tx := &txs.Tx{
   748  					Unsigned: &txs.TransformSubnetTx{
   749  						AssetID:                  customAssetID,
   750  						MinDelegatorStake:        config.MinDelegatorStake,
   751  						MinValidatorStake:        config.MinValidatorStake,
   752  						MaxValidatorStake:        config.MaxValidatorStake,
   753  						MinStakeDuration:         1337,
   754  						MaxStakeDuration:         42,
   755  						MinDelegationFee:         config.MinDelegationFee,
   756  						MaxValidatorWeightFactor: 21,
   757  					},
   758  				}
   759  				state.EXPECT().GetSubnetTransformation(subnetID).Return(tx, nil)
   760  				return state
   761  			},
   762  			expectedRules: &addDelegatorRules{
   763  				assetID:                  customAssetID,
   764  				minDelegatorStake:        config.MinDelegatorStake,
   765  				maxValidatorStake:        config.MaxValidatorStake,
   766  				minStakeDuration:         1337 * time.Second,
   767  				maxStakeDuration:         42 * time.Second,
   768  				maxValidatorWeightFactor: 21,
   769  			},
   770  			expectedErr: nil,
   771  		},
   772  	}
   773  	for _, tt := range tests {
   774  		t.Run(tt.name, func(t *testing.T) {
   775  			require := require.New(t)
   776  			ctrl := gomock.NewController(t)
   777  
   778  			chainState := tt.chainStateF(ctrl)
   779  			rules, err := getDelegatorRules(tt.backend, chainState, tt.subnetID)
   780  			if tt.expectedErr != nil {
   781  				require.ErrorIs(err, tt.expectedErr)
   782  				return
   783  			}
   784  			require.NoError(err)
   785  			require.Equal(tt.expectedRules, rules)
   786  		})
   787  	}
   788  }