code.vegaprotocol.io/vega@v0.79.0/core/validators/topology_eth_key_rotate_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package validators_test
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/validators"
    25  	"code.vegaprotocol.io/vega/core/validators/mocks"
    26  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  func TestTopologyEthereumKeyRotate(t *testing.T) {
    34  	t.Run("rotate ethereum key - success", testRotateEthereumKeySuccess)
    35  	t.Run("rotate ethereum key - fails when rotating to the same key", testRotateEthereumKeyFailsRotatingToSameKey)
    36  	t.Run("rotate ethereum key - fails if pending rotation already exists", testRotateEthereumKeyFailsIfPendingRotationExists)
    37  	t.Run("rotate ethereum key - fails when node does not exists", testRotateEthereumKeyFailsOnNonExistingNode)
    38  	t.Run("rotate ethereum key - fails when target block height is less then current block height", testRotateEthereumKeyFailsWhenTargetBlockHeightIsLessThenCurrentBlockHeight)
    39  	t.Run("rotate ethereum key - fails when current address does not match", testRotateEthereumKeyFailsWhenCurrentAddressDoesNotMatch)
    40  	t.Run("ethereum key rotation begin block - success", testEthereumKeyRotationBeginBlock)
    41  	t.Run("ethereum key rotation begin block with submitter - success", TestEthereumKeyRotationBeginBlockWithSubmitter)
    42  	t.Run("ethereum key rotation by pending or ersatz does not generate signatures", testNoSignaturesForNonTendermint)
    43  }
    44  
    45  func testRotateEthereumKeySuccess(t *testing.T) {
    46  	top := getTestTopWithMockedSignatures(t)
    47  	defer top.ctrl.Finish()
    48  
    49  	nr := commandspb.AnnounceNode{
    50  		Id:              "vega-master-pubkey",
    51  		ChainPubKey:     tmPubKey,
    52  		VegaPubKey:      "vega-key",
    53  		EthereumAddress: "eth-address",
    54  	}
    55  	ctx := context.Background()
    56  	err := top.AddNewNode(ctx, &nr, validators.ValidatorStatusTendermint)
    57  	require.NoError(t, err)
    58  
    59  	ekr := newEthereumKeyRotationSubmission(nr.EthereumAddress, "new-eth-address", 15, "")
    60  
    61  	toRemove := []validators.NodeIDAddress{{NodeID: nr.Id, EthAddress: nr.EthereumAddress}}
    62  
    63  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
    64  	top.signatures.EXPECT().PrepareValidatorSignatures(
    65  		gomock.Any(),
    66  		toRemove,
    67  		gomock.Any(),
    68  		gomock.Any(),
    69  	).Times(1)
    70  
    71  	err = top.ProcessEthereumKeyRotation(ctx, nr.VegaPubKey, ekr, MockVerify)
    72  	require.NoError(t, err)
    73  }
    74  
    75  func testRotateEthereumKeyFailsIfPendingRotationExists(t *testing.T) {
    76  	top := getTestTopWithMockedSignatures(t)
    77  	defer top.ctrl.Finish()
    78  
    79  	nr := commandspb.AnnounceNode{
    80  		Id:              "vega-master-pubkey",
    81  		ChainPubKey:     tmPubKey,
    82  		VegaPubKey:      "vega-key",
    83  		EthereumAddress: "eth-address",
    84  	}
    85  	ctx := context.Background()
    86  	err := top.AddNewNode(ctx, &nr, validators.ValidatorStatusTendermint)
    87  	require.NoError(t, err)
    88  
    89  	ekr := newEthereumKeyRotationSubmission(nr.EthereumAddress, "new-eth-address", 15, "")
    90  
    91  	toRemove := []validators.NodeIDAddress{{NodeID: nr.Id, EthAddress: nr.EthereumAddress}}
    92  
    93  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
    94  	top.signatures.EXPECT().PrepareValidatorSignatures(
    95  		gomock.Any(),
    96  		toRemove,
    97  		gomock.Any(),
    98  		gomock.Any(),
    99  	).Times(1)
   100  
   101  	err = top.ProcessEthereumKeyRotation(ctx, nr.VegaPubKey, ekr, MockVerify)
   102  	require.NoError(t, err)
   103  
   104  	// now push in another rotation submission
   105  	err = top.ProcessEthereumKeyRotation(ctx, nr.VegaPubKey, ekr, MockVerify)
   106  	require.Error(t, err, validators.ErrNodeAlreadyHasPendingKeyRotation)
   107  }
   108  
   109  func testRotateEthereumKeyFailsRotatingToSameKey(t *testing.T) {
   110  	top := getTestTopWithMockedSignatures(t)
   111  	defer top.ctrl.Finish()
   112  
   113  	nr := commandspb.AnnounceNode{
   114  		Id:              "vega-master-pubkey",
   115  		ChainPubKey:     tmPubKey,
   116  		VegaPubKey:      "vega-key",
   117  		EthereumAddress: "eth-address",
   118  	}
   119  	ctx := context.Background()
   120  	err := top.AddNewNode(ctx, &nr, validators.ValidatorStatusTendermint)
   121  	require.NoError(t, err)
   122  
   123  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
   124  	ekr := newEthereumKeyRotationSubmission(nr.EthereumAddress, nr.EthereumAddress, 15, "")
   125  	err = top.ProcessEthereumKeyRotation(ctx, nr.VegaPubKey, ekr, MockVerify)
   126  	require.Error(t, err, validators.ErrCannotRotateToSameKey)
   127  }
   128  
   129  func testRotateEthereumKeyFailsOnNonExistingNode(t *testing.T) {
   130  	top := getTestTopWithDefaultValidator(t)
   131  	defer top.ctrl.Finish()
   132  
   133  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
   134  	err := top.ProcessEthereumKeyRotation(
   135  		context.Background(),
   136  		"vega-nonexisting-pubkey",
   137  		newEthereumKeyRotationSubmission("", "new-eth-addr", 10, ""),
   138  		MockVerify,
   139  	)
   140  
   141  	assert.Error(t, err)
   142  	assert.EqualError(t, err, "failed to rotate ethereum key for non existing validator \"vega-nonexisting-pubkey\"")
   143  }
   144  
   145  func testRotateEthereumKeyFailsWhenTargetBlockHeightIsLessThenCurrentBlockHeight(t *testing.T) {
   146  	top := getTestTopWithMockedSignatures(t)
   147  	defer top.ctrl.Finish()
   148  
   149  	nr := commandspb.AnnounceNode{
   150  		Id:              "vega-master-pubkey",
   151  		ChainPubKey:     tmPubKey,
   152  		VegaPubKey:      "vega-key",
   153  		EthereumAddress: "eth-address",
   154  	}
   155  
   156  	err := top.AddNewNode(context.Background(), &nr, validators.ValidatorStatusTendermint)
   157  	require.NoError(t, err)
   158  
   159  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
   160  	err = top.ProcessEthereumKeyRotation(
   161  		context.Background(),
   162  		nr.VegaPubKey,
   163  		newEthereumKeyRotationSubmission("eth-address", "new-eth-addr", 5, ""),
   164  		MockVerify,
   165  	)
   166  	assert.ErrorIs(t, err, validators.ErrTargetBlockHeightMustBeGreaterThanCurrentHeight)
   167  }
   168  
   169  func testRotateEthereumKeyFailsWhenCurrentAddressDoesNotMatch(t *testing.T) {
   170  	top := getTestTopWithMockedSignatures(t)
   171  	defer top.ctrl.Finish()
   172  
   173  	nr := commandspb.AnnounceNode{
   174  		Id:              "vega-master-pubkey",
   175  		ChainPubKey:     tmPubKey,
   176  		VegaPubKey:      "vega-key",
   177  		EthereumAddress: "eth-address",
   178  		VegaPubKeyIndex: 1,
   179  	}
   180  	err := top.AddNewNode(context.Background(), &nr, validators.ValidatorStatusTendermint)
   181  	require.NoError(t, err)
   182  
   183  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
   184  	err = top.ProcessEthereumKeyRotation(
   185  		context.Background(),
   186  		nr.VegaPubKey,
   187  		newEthereumKeyRotationSubmission("random-key", "new-eth-key", 20, ""),
   188  		MockVerify,
   189  	)
   190  	assert.ErrorIs(t, err, validators.ErrCurrentEthAddressDoesNotMatch)
   191  }
   192  
   193  func newEthereumKeyRotationSubmission(currentAddr, newAddr string, targetBlock uint64, submitter string) *commandspb.EthereumKeyRotateSubmission {
   194  	return &commandspb.EthereumKeyRotateSubmission{
   195  		CurrentAddress:   currentAddr,
   196  		NewAddress:       newAddr,
   197  		TargetBlock:      targetBlock,
   198  		SubmitterAddress: submitter,
   199  		EthereumSignature: &commandspb.Signature{
   200  			Value: "deadbeef",
   201  		},
   202  	}
   203  }
   204  
   205  func testEthereumKeyRotationBeginBlock(t *testing.T) {
   206  	top := getTestTopWithMockedSignatures(t)
   207  	defer top.ctrl.Finish()
   208  
   209  	chainValidators := []string{"tm-pubkey-1", "tm-pubkey-2", "tm-pubkey-3", "tm-pubkey-4"}
   210  
   211  	ctx := context.Background()
   212  	for i := 0; i < len(chainValidators); i++ {
   213  		j := i + 1
   214  		id := fmt.Sprintf("vega-key-%d", j)
   215  		nr := commandspb.AnnounceNode{
   216  			Id:              fmt.Sprintf("vega-master-pubkey-%d", j),
   217  			ChainPubKey:     chainValidators[i],
   218  			VegaPubKey:      id,
   219  			EthereumAddress: fmt.Sprintf("eth-address-%d", j),
   220  		}
   221  
   222  		err := top.AddNewNode(ctx, &nr, validators.ValidatorStatusTendermint)
   223  		require.NoErrorf(t, err, "failed to add node registation %s", id)
   224  	}
   225  	top.timeService.EXPECT().GetTimeNow().AnyTimes()
   226  	top.multisigTop.EXPECT().IsSigner(gomock.Any()).AnyTimes().Return(false)
   227  	top.signatures.EXPECT().ClearStaleSignatures().AnyTimes()
   228  	top.signatures.EXPECT().SetNonce(gomock.Any()).Times(2)
   229  	top.signatures.EXPECT().PrepareValidatorSignatures(
   230  		gomock.Any(),
   231  		gomock.Any(),
   232  		gomock.Any(),
   233  		gomock.Any(),
   234  	).Times(2 * len(chainValidators))
   235  
   236  	// add ethereum key rotations
   237  	err := top.ProcessEthereumKeyRotation(ctx, "vega-key-1", newEthereumKeyRotationSubmission("eth-address-1", "new-eth-address-1", 11, ""), MockVerify)
   238  	require.NoError(t, err)
   239  	err = top.ProcessEthereumKeyRotation(ctx, "vega-key-2", newEthereumKeyRotationSubmission("eth-address-2", "new-eth-address-2", 11, ""), MockVerify)
   240  	require.NoError(t, err)
   241  	err = top.ProcessEthereumKeyRotation(ctx, "vega-key-3", newEthereumKeyRotationSubmission("eth-address-3", "new-eth-address-3", 13, ""), MockVerify)
   242  	require.NoError(t, err)
   243  	err = top.ProcessEthereumKeyRotation(ctx, "vega-key-4", newEthereumKeyRotationSubmission("eth-address-4", "new-eth-address-4", 13, ""), MockVerify)
   244  	require.NoError(t, err)
   245  
   246  	// when
   247  	top.BeginBlock(ctx, 11, "")
   248  	// then
   249  	data1 := top.Get("vega-master-pubkey-1")
   250  	require.NotNil(t, data1)
   251  	assert.Equal(t, "new-eth-address-1", data1.EthereumAddress)
   252  	data2 := top.Get("vega-master-pubkey-2")
   253  	require.NotNil(t, data2)
   254  	assert.Equal(t, "new-eth-address-2", data2.EthereumAddress)
   255  	data3 := top.Get("vega-master-pubkey-3")
   256  	require.NotNil(t, data3)
   257  	assert.Equal(t, "eth-address-3", data3.EthereumAddress)
   258  	data4 := top.Get("vega-master-pubkey-4")
   259  	require.NotNil(t, data4)
   260  	assert.Equal(t, "eth-address-4", data4.EthereumAddress)
   261  
   262  	// when
   263  	top.BeginBlock(ctx, 13, "")
   264  	// then
   265  	data3 = top.Get("vega-master-pubkey-3")
   266  	require.NotNil(t, data3)
   267  	assert.Equal(t, "new-eth-address-3", data3.EthereumAddress)
   268  	data4 = top.Get("vega-master-pubkey-4")
   269  	require.NotNil(t, data4)
   270  	assert.Equal(t, "new-eth-address-4", data4.EthereumAddress)
   271  }
   272  
   273  func TestEthereumKeyRotationBeginBlockWithSubmitter(t *testing.T) {
   274  	top := getTestTopWithMockedSignatures(t)
   275  	defer top.ctrl.Finish()
   276  
   277  	chainValidators := []string{"tm-pubkey-1", "tm-pubkey-2", "tm-pubkey-3", "tm-pubkey-4"}
   278  
   279  	ctx := context.Background()
   280  	for i := 0; i < len(chainValidators); i++ {
   281  		j := i + 1
   282  		id := fmt.Sprintf("vega-master-pubkey-%d", j)
   283  		nr := commandspb.AnnounceNode{
   284  			Id:              id,
   285  			ChainPubKey:     chainValidators[i],
   286  			VegaPubKey:      fmt.Sprintf("vega-key-%d", j),
   287  			EthereumAddress: fmt.Sprintf("eth-address-%d", j),
   288  		}
   289  
   290  		err := top.AddNewNode(ctx, &nr, validators.ValidatorStatusTendermint)
   291  		require.NoErrorf(t, err, "failed to add node registation %s", id)
   292  	}
   293  	submitter := "some-eth-address"
   294  
   295  	top.multisigTop.EXPECT().ChainID().Times(1)
   296  	top.multisigTop2.EXPECT().ChainID().Times(1)
   297  	top.signatures.EXPECT().PrepareValidatorSignatures(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(3)
   298  	top.signatures.EXPECT().EmitValidatorRemovedSignatures(gomock.Any(), submitter, gomock.Any(), gomock.Any(), gomock.Any()).Times(4)
   299  	top.signatures.EXPECT().EmitValidatorAddedSignatures(gomock.Any(), submitter, gomock.Any(), gomock.Any(), gomock.Any()).Times(2)
   300  	top.signatures.EXPECT().ClearStaleSignatures().AnyTimes()
   301  	top.timeService.EXPECT().GetTimeNow().Times(2)
   302  
   303  	// add ethereum key rotations
   304  	err := top.ProcessEthereumKeyRotation(ctx, "vega-key-1", newEthereumKeyRotationSubmission("eth-address-1", "new-eth-address-1", 11, submitter), MockVerify)
   305  	require.NoError(t, err)
   306  
   307  	// when
   308  	now := time.Unix(666, 666)
   309  	top.signatures.EXPECT().SetNonce(now).Times(1)
   310  	top.timeService.EXPECT().GetTimeNow().Times(6).Return(now)
   311  	top.multisigTop.EXPECT().ChainID().Times(1)
   312  	top.multisigTop2.EXPECT().ChainID().Times(1)
   313  	top.BeginBlock(ctx, 11, "")
   314  
   315  	// then
   316  	data1 := top.Get("vega-master-pubkey-1")
   317  	require.NotNil(t, data1)
   318  	assert.Equal(t, "new-eth-address-1", data1.EthereumAddress)
   319  
   320  	// now try to add a new rotation before resolving the contract
   321  	err = top.ProcessEthereumKeyRotation(ctx, "vega-key-1", newEthereumKeyRotationSubmission("eth-address-1", "new-eth-address-1", 13, submitter), MockVerify)
   322  	require.Error(t, err, validators.ErrNodeHasUnresolvedRotation)
   323  
   324  	// Now make it look like the old key is removed from the multisig contract
   325  	top.multisigTop.EXPECT().IsSigner(gomock.Any()).Return(false).Times(1)
   326  	top.multisigTop.EXPECT().IsSigner(gomock.Any()).Return(true).Times(1)
   327  
   328  	now = now.Add(time.Second)
   329  	top.signatures.EXPECT().SetNonce(now).Times(1)
   330  	top.timeService.EXPECT().GetTimeNow().Times(6).Return(now)
   331  	top.multisigTop.EXPECT().ChainID().Times(1)
   332  	top.multisigTop2.EXPECT().ChainID().Times(1)
   333  	top.BeginBlock(ctx, 140, "")
   334  
   335  	// try to submit again
   336  	err = top.ProcessEthereumKeyRotation(ctx, "vega-key-1", newEthereumKeyRotationSubmission("new-eth-address-1", "new-eth-address-2", 150, submitter), MockVerify)
   337  	require.NoError(t, err)
   338  }
   339  
   340  func testNoSignaturesForNonTendermint(t *testing.T) {
   341  	ctx := context.Background()
   342  
   343  	tcs := []struct {
   344  		name   string
   345  		status validators.ValidatorStatus
   346  	}{
   347  		{
   348  			name:   "no signatures when pending",
   349  			status: validators.ValidatorStatusPending,
   350  		},
   351  		{
   352  			name:   "no signatures when ersatz",
   353  			status: validators.ValidatorStatusErsatz,
   354  		},
   355  	}
   356  
   357  	for _, tc := range tcs {
   358  		t.Run(tc.name, func(tt *testing.T) {
   359  			top := getTestTopWithMockedSignatures(t)
   360  			defer top.ctrl.Finish()
   361  
   362  			nr := &commandspb.AnnounceNode{
   363  				Id:              "vega-master-pubkey",
   364  				ChainPubKey:     tmPubKey,
   365  				VegaPubKey:      "vega-key",
   366  				EthereumAddress: "eth-address",
   367  			}
   368  
   369  			err := top.AddNewNode(ctx, nr, tc.status)
   370  			require.NoError(t, err)
   371  
   372  			ekr := newEthereumKeyRotationSubmission(nr.EthereumAddress, "new-eth-address", 150, "")
   373  			err = top.ProcessEthereumKeyRotation(ctx, nr.VegaPubKey, ekr, MockVerify)
   374  			require.NoError(t, err)
   375  		})
   376  	}
   377  }
   378  
   379  type testTopWithSignatures struct {
   380  	*testTop
   381  	signatures *mocks.MockSignatures
   382  }
   383  
   384  func getTestTopWithMockedSignatures(t *testing.T) *testTopWithSignatures {
   385  	t.Helper()
   386  
   387  	top := getTestTopWithDefaultValidator(t)
   388  	signatures := mocks.NewMockSignatures(top.ctrl)
   389  
   390  	top.SetSignatures(signatures)
   391  
   392  	// set a reasonable block height
   393  	top.timeService.EXPECT().GetTimeNow().Times(3)
   394  	signatures.EXPECT().ClearStaleSignatures().Times(1)
   395  	signatures.EXPECT().SetNonce(gomock.Any()).Times(1)
   396  	signatures.EXPECT().OfferSignatures().AnyTimes()
   397  	top.BeginBlock(context.Background(), 10, "")
   398  
   399  	return &testTopWithSignatures{
   400  		testTop:    top,
   401  		signatures: signatures,
   402  	}
   403  }
   404  
   405  func MockVerify(message, signature []byte, hexAddress string) error {
   406  	return nil
   407  }