code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethverifier/verifier_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 ethverifier_test 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "math/big" 23 "strconv" 24 "testing" 25 "time" 26 27 mocks2 "code.vegaprotocol.io/vega/core/broker/mocks" 28 "code.vegaprotocol.io/vega/core/client/eth" 29 "code.vegaprotocol.io/vega/core/datasource/common" 30 "code.vegaprotocol.io/vega/core/datasource/external/ethcall" 31 "code.vegaprotocol.io/vega/core/datasource/external/ethverifier" 32 "code.vegaprotocol.io/vega/core/datasource/external/ethverifier/mocks" 33 omocks "code.vegaprotocol.io/vega/core/datasource/spec/mocks" 34 "code.vegaprotocol.io/vega/core/events" 35 "code.vegaprotocol.io/vega/core/validators" 36 "code.vegaprotocol.io/vega/logging" 37 vegapb "code.vegaprotocol.io/vega/protos/vega" 38 datapb "code.vegaprotocol.io/vega/protos/vega/data/v1" 39 40 "github.com/golang/mock/gomock" 41 "github.com/stretchr/testify/assert" 42 ) 43 44 type verifierTest struct { 45 *ethverifier.Verifier 46 47 ctrl *gomock.Controller 48 witness *mocks.MockWitness 49 ts *omocks.MockTimeService 50 oracleBroadcaster *mocks.MockOracleDataBroadcaster 51 ethCallEngine *mocks.MockEthCallEngine 52 ethConfirmations *mocks.MockEthereumConfirmations 53 broker *mocks2.MockBroker 54 55 onTick func(context.Context, time.Time) 56 } 57 58 func getTestEthereumOracleVerifier(t *testing.T) *verifierTest { 59 t.Helper() 60 ctrl := gomock.NewController(t) 61 log := logging.NewTestLogger() 62 witness := mocks.NewMockWitness(ctrl) 63 ts := omocks.NewMockTimeService(ctrl) 64 broadcaster := mocks.NewMockOracleDataBroadcaster(ctrl) 65 ethCallEngine := mocks.NewMockEthCallEngine(ctrl) 66 ethConfirmations := mocks.NewMockEthereumConfirmations(ctrl) 67 broker := mocks2.NewMockBroker(ctrl) 68 69 evt := &verifierTest{ 70 Verifier: ethverifier.New(log, witness, ts, broker, broadcaster, ethCallEngine, ethConfirmations, true), 71 ctrl: ctrl, 72 witness: witness, 73 ts: ts, 74 oracleBroadcaster: broadcaster, 75 ethCallEngine: ethCallEngine, 76 ethConfirmations: ethConfirmations, 77 broker: broker, 78 } 79 evt.onTick = evt.Verifier.OnTick 80 81 return evt 82 } 83 84 func TestVerifier(t *testing.T) { 85 t.Run("testProcessEthereumOracleQueryOK", testProcessEthereumOracleQueryOK) 86 t.Run("testProcessEthereumOracleQueryResultMismatch", testProcessEthereumOracleQueryResultMismatch) 87 t.Run("testProcessEthereumOracleFilterMismatch", testProcessEthereumOracleFilterMismatch) 88 t.Run("testProcessEthereumOracleInsufficientConfirmations", testProcessEthereumOracleInsufficientConfirmations) 89 t.Run("testProcessEthereumOracleQueryDuplicateIgnored", testProcessEthereumOracleQueryDuplicateIgnored) 90 t.Run("testProcessEthereumOracleChainEventWithGlobalError", testProcessEthereumOracleChainEventWithGlobalError) 91 t.Run("testProcessEthereumOracleChainEventWithLocalError", testProcessEthereumOracleChainEventWithLocalError) 92 t.Run("testProcessEthereumOracleChainEventWithMismatchedError", testProcessEthereumOracleChainEventWithMismatchedError) 93 t.Run("testProcessEthereumOracleQueryWithBlockTimeBeforeInitialTime", testProcessEthereumOracleQueryWithBlockTimeBeforeInitialTime) 94 t.Run("testSpoofedEthTimeFails", testSpoofedEthTimeFails) 95 t.Run("testProcessEthereumHeartbeat", testProcessEthereumHeartbeat) 96 } 97 98 func testSpoofedEthTimeFails(t *testing.T) { 99 eov := getTestEthereumOracleVerifier(t) 100 defer eov.ctrl.Finish() 101 assert.NotNil(t, eov) 102 103 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(50), nil) 104 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 105 eov.ts.EXPECT().GetTimeNow().Times(2) 106 107 var checkResult error 108 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 109 Times(1). 110 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 111 checkResult = toCheck.Check(context.Background()) 112 return nil 113 }) 114 115 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 116 assert.NoError(t, err) 117 assert.Error(t, checkResult) 118 } 119 120 func testProcessEthereumOracleChainEventWithGlobalError(t *testing.T) { 121 eov := getTestEthereumOracleVerifier(t) 122 defer eov.ctrl.Finish() 123 assert.NotNil(t, eov) 124 125 now := time.Now() 126 127 testError := "test error" 128 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(now.Unix()), nil) 129 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(ethcall.Result{}, errors.New(testError)) 130 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 131 eov.ts.EXPECT().GetTimeNow().Return(now).Times(2) 132 133 var onQueryResultVerified func(interface{}, bool) 134 var checkResult error 135 var resourceToCheck interface{} 136 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 137 Times(1). 138 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 139 resourceToCheck = toCheck 140 onQueryResultVerified = fn 141 checkResult = toCheck.Check(context.Background()) 142 return nil 143 }) 144 145 errCallEvent := ethcall.ContractCallEvent{ 146 BlockHeight: 1, 147 BlockTime: uint64(now.Unix()), 148 SpecId: "testspec", 149 Result: nil, 150 Error: &testError, 151 } 152 153 err := eov.ProcessEthereumContractCallResult(errCallEvent) 154 assert.NoError(t, err) 155 assert.NoError(t, checkResult) 156 157 // result verified 158 onQueryResultVerified(resourceToCheck, true) 159 160 tickTime := time.Unix(10, 0) 161 162 dataProto := vegapb.OracleData{ 163 ExternalData: &datapb.ExternalData{ 164 Data: &datapb.Data{ 165 MatchedSpecIds: []string{"testspec"}, 166 BroadcastAt: tickTime.UnixNano(), 167 Error: &testError, 168 MetaData: []*datapb.Property{ 169 { 170 Name: "vega-time", 171 Value: strconv.FormatInt(tickTime.Unix(), 10), 172 }, 173 }, 174 }, 175 }, 176 } 177 eov.broker.EXPECT().Send(events.NewOracleDataEvent(context.Background(), vegapb.OracleData{ExternalData: dataProto.ExternalData})) 178 179 eov.onTick(context.Background(), tickTime) 180 } 181 182 func testProcessEthereumOracleChainEventWithLocalError(t *testing.T) { 183 eov := getTestEthereumOracleVerifier(t) 184 defer eov.ctrl.Finish() 185 assert.NotNil(t, eov) 186 187 now := time.Now() 188 testError := "test error" 189 190 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(now.Unix()), nil) 191 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(ethcall.Result{}, nil) 192 193 eov.ts.EXPECT().GetTimeNow().Return(now).Times(2) 194 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 195 196 var checkResult error 197 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 198 Times(1). 199 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 200 checkResult = toCheck.Check(context.Background()) 201 return nil 202 }) 203 204 errCallEvent := ethcall.ContractCallEvent{ 205 BlockHeight: 1, 206 BlockTime: uint64(now.Unix()), 207 SpecId: "testspec", 208 Result: nil, 209 Error: &testError, 210 } 211 212 err := eov.ProcessEthereumContractCallResult(errCallEvent) 213 assert.NoError(t, err) 214 assert.Error(t, checkResult) 215 } 216 217 func testProcessEthereumOracleChainEventWithMismatchedError(t *testing.T) { 218 eov := getTestEthereumOracleVerifier(t) 219 defer eov.ctrl.Finish() 220 assert.NotNil(t, eov) 221 222 now := time.Now() 223 testError := "test error" 224 225 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(now.Unix()), nil) 226 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(ethcall.Result{}, errors.New("another error")) 227 228 eov.ts.EXPECT().GetTimeNow().Return(now).Times(2) 229 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 230 231 var checkResult error 232 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 233 Times(1). 234 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 235 checkResult = toCheck.Check(context.Background()) 236 return nil 237 }) 238 239 errCallEvent := ethcall.ContractCallEvent{ 240 BlockHeight: 1, 241 BlockTime: uint64(now.Unix()), 242 SpecId: "testspec", 243 Result: nil, 244 Error: &testError, 245 } 246 247 err := eov.ProcessEthereumContractCallResult(errCallEvent) 248 assert.NoError(t, err) 249 assert.Error(t, checkResult) 250 } 251 252 func testProcessEthereumOracleQueryOK(t *testing.T) { 253 eov := getTestEthereumOracleVerifier(t) 254 defer eov.ctrl.Finish() 255 assert.NotNil(t, eov) 256 257 result := okResult() 258 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 259 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 260 eov.ethCallEngine.EXPECT().MakeResult("testspec", []byte("testbytes")).Return(result, nil) 261 262 eov.ethCallEngine.EXPECT().GetRequiredConfirmations("testspec").Return(uint64(5), nil).Times(2) 263 264 eov.ts.EXPECT().GetTimeNow().Times(2) 265 eov.ethCallEngine.EXPECT().GetInitialTriggerTime("testspec").Return(uint64(90), nil) 266 eov.ethConfirmations.EXPECT().CheckRequiredConfirmations(uint64(1), uint64(5)).Return(nil) 267 268 var onQueryResultVerified func(interface{}, bool) 269 var checkResult error 270 var resourceToCheck interface{} 271 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 272 Times(1). 273 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 274 resourceToCheck = toCheck 275 onQueryResultVerified = fn 276 checkResult = toCheck.Check(context.Background()) 277 return nil 278 }) 279 280 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 281 assert.NoError(t, err) 282 assert.NoError(t, checkResult) 283 284 // result verified 285 onQueryResultVerified(resourceToCheck, true) 286 287 tick := time.Unix(10, 0) 288 oracleData := common.Data{ 289 EthKey: "testspec", 290 Signers: nil, 291 Data: okResult().Normalised, 292 MetaData: map[string]string{ 293 "eth-block-height": "1", 294 "eth-block-time": "100", 295 "vega-time": strconv.FormatInt(tick.Unix(), 10), 296 }, 297 } 298 299 eov.oracleBroadcaster.EXPECT().BroadcastData(gomock.Any(), oracleData) 300 301 eov.onTick(context.Background(), tick) 302 } 303 304 func testProcessEthereumOracleQueryWithBlockTimeBeforeInitialTime(t *testing.T) { 305 eov := getTestEthereumOracleVerifier(t) 306 defer eov.ctrl.Finish() 307 assert.NotNil(t, eov) 308 309 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 310 result := okResult() 311 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 312 313 eov.ts.EXPECT().GetTimeNow().Times(2) 314 eov.ethCallEngine.EXPECT().GetInitialTriggerTime("testspec").Return(uint64(110), nil) 315 316 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 317 318 var checkResult error 319 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 320 Times(1). 321 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 322 checkResult = toCheck.Check(context.Background()) 323 return nil 324 }) 325 326 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 327 assert.NoError(t, err) 328 assert.ErrorContains(t, checkResult, "is before the specification's initial time") 329 } 330 331 func testProcessEthereumOracleQueryResultMismatch(t *testing.T) { 332 eov := getTestEthereumOracleVerifier(t) 333 defer eov.ctrl.Finish() 334 assert.NotNil(t, eov) 335 336 result := okResult() 337 338 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 339 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 340 eov.ts.EXPECT().GetTimeNow().Times(2) 341 eov.ethCallEngine.EXPECT().GetRequiredConfirmations(gomock.Any()).Return(uint64(0), nil) 342 343 var checkResult error 344 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 345 Times(1). 346 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 347 checkResult = toCheck.Check(context.Background()) 348 return nil 349 }) 350 351 err := eov.ProcessEthereumContractCallResult(generateIncorrectDummyCallEvent()) 352 assert.NoError(t, err) 353 assert.ErrorContains(t, checkResult, "mismatched") 354 } 355 356 func testProcessEthereumOracleFilterMismatch(t *testing.T) { 357 eov := getTestEthereumOracleVerifier(t) 358 defer eov.ctrl.Finish() 359 assert.NotNil(t, eov) 360 361 result := filterMismatchResult() 362 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 363 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 364 eov.ethCallEngine.EXPECT().GetRequiredConfirmations("testspec").Return(uint64(5), nil).Times(2) 365 366 eov.ts.EXPECT().GetTimeNow().Times(2) 367 eov.ethCallEngine.EXPECT().GetInitialTriggerTime("testspec").Return(uint64(90), nil) 368 eov.ethConfirmations.EXPECT().CheckRequiredConfirmations(uint64(1), uint64(5)).Return(nil) 369 370 var checkResult error 371 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 372 Times(1). 373 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 374 checkResult = toCheck.Check(context.Background()) 375 return nil 376 }) 377 378 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 379 assert.NoError(t, err) 380 assert.ErrorContains(t, checkResult, "failed filter") 381 } 382 383 func testProcessEthereumOracleInsufficientConfirmations(t *testing.T) { 384 eov := getTestEthereumOracleVerifier(t) 385 defer eov.ctrl.Finish() 386 assert.NotNil(t, eov) 387 388 result := okResult() 389 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 390 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 391 eov.ethCallEngine.EXPECT().GetRequiredConfirmations("testspec").Return(uint64(5), nil).Times(2) 392 393 eov.ts.EXPECT().GetTimeNow().Times(2) 394 eov.ethCallEngine.EXPECT().GetInitialTriggerTime("testspec").Return(uint64(90), nil) 395 eov.ethConfirmations.EXPECT().CheckRequiredConfirmations(uint64(1), uint64(5)).Return(eth.ErrMissingConfirmations) 396 397 var checkResult error 398 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 399 Times(1). 400 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 401 checkResult = toCheck.Check(context.Background()) 402 return nil 403 }) 404 405 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 406 407 assert.ErrorIs(t, checkResult, eth.ErrMissingConfirmations) 408 assert.Nil(t, err) 409 } 410 411 func testProcessEthereumOracleQueryDuplicateIgnored(t *testing.T) { 412 eov := getTestEthereumOracleVerifier(t) 413 defer eov.ctrl.Finish() 414 assert.NotNil(t, eov) 415 416 result := okResult() 417 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 418 eov.ethCallEngine.EXPECT().CallSpec(gomock.Any(), "testspec", uint64(1)).Return(result, nil) 419 eov.ethCallEngine.EXPECT().GetRequiredConfirmations("testspec").Return(uint64(5), nil).Times(2) 420 421 eov.ts.EXPECT().GetTimeNow().Times(3) 422 eov.ethCallEngine.EXPECT().GetInitialTriggerTime("testspec").Return(uint64(90), nil) 423 eov.ethConfirmations.EXPECT().CheckRequiredConfirmations(uint64(1), uint64(5)).Return(nil) 424 425 var checkResult error 426 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 427 Times(1). 428 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 429 checkResult = toCheck.Check(context.Background()) 430 return nil 431 }) 432 433 err := eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 434 assert.NoError(t, checkResult) 435 assert.NoError(t, err) 436 437 err = eov.ProcessEthereumContractCallResult(generateDummyCallEvent()) 438 assert.ErrorContains(t, err, "duplicated") 439 } 440 441 func testProcessEthereumHeartbeat(t *testing.T) { 442 eov := getTestEthereumOracleVerifier(t) 443 defer eov.ctrl.Finish() 444 assert.NotNil(t, eov) 445 446 eov.ethCallEngine.EXPECT().GetEthTime(gomock.Any(), uint64(1)).Return(uint64(100), nil) 447 eov.ethConfirmations.EXPECT().GetConfirmations().Return(uint64(5)).Times(1) 448 449 eov.ts.EXPECT().GetTimeNow().Times(3) 450 eov.ethConfirmations.EXPECT().Check(uint64(1)).Return(nil) 451 452 var checkResult error 453 eov.witness.EXPECT().StartCheckWithDelay(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 454 Times(1). 455 DoAndReturn(func(toCheck validators.Resource, fn func(interface{}, bool), _ time.Time, _ int64) error { 456 checkResult = toCheck.Check(context.Background()) 457 return nil 458 }) 459 460 callEvent := ethcall.ContractCallEvent{ 461 BlockHeight: 1, 462 BlockTime: 100, 463 Heartbeat: true, 464 } 465 466 err := eov.ProcessEthereumContractCallResult(callEvent) 467 assert.NoError(t, checkResult) 468 assert.NoError(t, err) 469 470 err = eov.ProcessEthereumContractCallResult(callEvent) 471 assert.ErrorContains(t, err, "duplicated") 472 } 473 474 func generateDummyCallEvent() ethcall.ContractCallEvent { 475 return ethcall.ContractCallEvent{ 476 BlockHeight: 1, 477 BlockTime: 100, 478 SpecId: "testspec", 479 Result: []byte("testbytes"), 480 } 481 } 482 483 func generateIncorrectDummyCallEvent() ethcall.ContractCallEvent { 484 res := generateDummyCallEvent() 485 res.Result = []byte("otherbytes") 486 return res 487 } 488 489 func okResult() ethcall.Result { 490 return ethcall.Result{ 491 Bytes: []byte("testbytes"), 492 Values: []any{big.NewInt(42)}, 493 Normalised: map[string]string{"price": fmt.Sprintf("%s", big.NewInt(42))}, 494 PassesFilters: true, 495 } 496 } 497 498 func filterMismatchResult() ethcall.Result { 499 r := okResult() 500 r.PassesFilters = false 501 return r 502 }