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 }