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