code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethcall/engine.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 ethcall 17 18 import ( 19 "context" 20 "fmt" 21 "log" 22 "math/big" 23 "reflect" 24 "strconv" 25 "sync" 26 "sync/atomic" 27 "time" 28 29 "code.vegaprotocol.io/vega/core/datasource" 30 "code.vegaprotocol.io/vega/core/datasource/external/ethcall/common" 31 "code.vegaprotocol.io/vega/libs/ptr" 32 "code.vegaprotocol.io/vega/logging" 33 "code.vegaprotocol.io/vega/protos/vega" 34 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 35 36 "github.com/ethereum/go-ethereum" 37 ) 38 39 type EthReaderCaller interface { 40 ethereum.ContractCaller 41 ethereum.ChainReader 42 ChainID(context.Context) (*big.Int, error) 43 } 44 45 //go:generate go run github.com/golang/mock/mockgen -destination mocks/forwarder_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethcall Forwarder 46 type Forwarder interface { 47 ForwardFromSelf(*commandspb.ChainEvent) 48 } 49 50 type blockish interface { 51 NumberU64() uint64 52 Time() uint64 53 } 54 55 type blockIndex struct { 56 number uint64 57 time uint64 58 } 59 60 func (b blockIndex) NumberU64() uint64 { 61 return b.number 62 } 63 64 func (b blockIndex) Time() uint64 { 65 return b.time 66 } 67 68 type Engine struct { 69 log *logging.Logger 70 cfg Config 71 isValidator bool 72 client EthReaderCaller 73 calls map[string]Call 74 forwarder Forwarder 75 prevEthBlock blockish 76 cancelEthereumQueries context.CancelFunc 77 poller *poller 78 mu sync.Mutex 79 80 chainID atomic.Uint64 81 blockInterval atomic.Uint64 82 lastSent blockish 83 heartbeatInterval time.Duration 84 } 85 86 func NewEngine(log *logging.Logger, cfg Config, isValidator bool, client EthReaderCaller, forwarder Forwarder) *Engine { 87 e := &Engine{ 88 log: log, 89 cfg: cfg, 90 isValidator: isValidator, 91 client: client, 92 forwarder: forwarder, 93 calls: make(map[string]Call), 94 poller: newPoller(cfg.PollEvery.Get()), 95 heartbeatInterval: cfg.HeartbeatIntervalForTestOnlyDoNotChange.Get(), 96 } 97 98 // default to 1 block interval 99 e.blockInterval.Store(1) 100 101 return e 102 } 103 104 // EnsureChainID tells the engine which chainID it should be related to, and it confirms this against the its client. 105 func (e *Engine) EnsureChainID(ctx context.Context, chainID string, blockInterval uint64, confirmWithClient bool) { 106 chainIDU, _ := strconv.ParseUint(chainID, 10, 64) 107 e.chainID.Store(chainIDU) 108 e.blockInterval.Store(blockInterval) 109 // cover backward compatibility for L2 110 if e.blockInterval.Load() == 0 { 111 e.blockInterval.Store(1) 112 } 113 114 // if the node is a validator, we now check the chainID against the chain the client is connected to. 115 if confirmWithClient { 116 cid, err := e.client.ChainID(ctx) 117 if err != nil { 118 log.Panic("could not load chain ID", logging.Error(err)) 119 } 120 121 if cid.Uint64() != e.chainID.Load() { 122 log.Panic("chain ID mismatch between ethCall engine and EVM client", 123 logging.Uint64("client-chain-id", cid.Uint64()), 124 logging.Uint64("engine-chain-id", e.chainID.Load()), 125 ) 126 } 127 } 128 } 129 130 // Start starts the polling of the Ethereum bridges, listens to the events 131 // they emit and forward it to the network. 132 func (e *Engine) Start() { 133 if e.isValidator && !reflect.ValueOf(e.client).IsNil() { 134 go func() { 135 ctx, cancelEthereumQueries := context.WithCancel(context.Background()) 136 defer cancelEthereumQueries() 137 138 e.cancelEthereumQueries = cancelEthereumQueries 139 e.log.Info("Starting ethereum contract call polling engine", logging.Uint64("chain-id", e.chainID.Load())) 140 141 e.poller.Loop(func() { 142 e.Poll(ctx, time.Now()) 143 }) 144 }() 145 } 146 } 147 148 func (e *Engine) StartAtHeight(height uint64, time uint64) { 149 e.prevEthBlock = blockIndex{number: height, time: time} 150 e.lastSent = blockIndex{number: height, time: time} 151 e.Start() 152 } 153 154 func (e *Engine) getCalls() map[string]Call { 155 e.mu.Lock() 156 defer e.mu.Unlock() 157 calls := map[string]Call{} 158 for specID, call := range e.calls { 159 calls[specID] = call 160 } 161 return calls 162 } 163 164 func (e *Engine) Stop() { 165 // Notify to stop on next iteration. 166 e.poller.Stop() 167 // Cancel any ongoing queries against Ethereum. 168 if e.cancelEthereumQueries != nil { 169 e.cancelEthereumQueries() 170 } 171 } 172 173 func (e *Engine) GetSpec(id string) (common.Spec, bool) { 174 e.mu.Lock() 175 defer e.mu.Unlock() 176 if source, ok := e.calls[id]; ok { 177 return source.spec, true 178 } 179 180 return common.Spec{}, false 181 } 182 183 func (e *Engine) MakeResult(specID string, bytes []byte) (Result, error) { 184 e.mu.Lock() 185 defer e.mu.Unlock() 186 call, ok := e.calls[specID] 187 if !ok { 188 return Result{}, fmt.Errorf("no such specification: %v", specID) 189 } 190 return newResult(call, bytes) 191 } 192 193 func (e *Engine) CallSpec(ctx context.Context, id string, atBlock uint64) (Result, error) { 194 e.mu.Lock() 195 call, ok := e.calls[id] 196 if !ok { 197 e.mu.Unlock() 198 return Result{}, fmt.Errorf("no such specification: %v", id) 199 } 200 e.mu.Unlock() 201 202 return call.Call(ctx, e.client, atBlock) 203 } 204 205 func (e *Engine) GetEthTime(ctx context.Context, atBlock uint64) (uint64, error) { 206 blockNum := big.NewInt(0).SetUint64(atBlock) 207 header, err := e.client.HeaderByNumber(ctx, blockNum) 208 if err != nil { 209 return 0, fmt.Errorf("failed to get block header: %w", err) 210 } 211 212 if header == nil { 213 return 0, fmt.Errorf("nil block header: %w", err) 214 } 215 216 return header.Time, nil 217 } 218 219 func (e *Engine) GetRequiredConfirmations(id string) (uint64, error) { 220 e.mu.Lock() 221 call, ok := e.calls[id] 222 if !ok { 223 e.mu.Unlock() 224 return 0, fmt.Errorf("no such specification: %v", id) 225 } 226 e.mu.Unlock() 227 228 return call.spec.RequiredConfirmations, nil 229 } 230 231 func (e *Engine) GetInitialTriggerTime(id string) (uint64, error) { 232 e.mu.Lock() 233 call, ok := e.calls[id] 234 if !ok { 235 e.mu.Unlock() 236 return 0, fmt.Errorf("no such specification: %v", id) 237 } 238 e.mu.Unlock() 239 240 return call.initialTime(), nil 241 } 242 243 func (e *Engine) OnSpecActivated(ctx context.Context, spec datasource.Spec) error { 244 e.mu.Lock() 245 defer e.mu.Unlock() 246 switch d := spec.Data.Content().(type) { 247 case common.Spec: 248 id := spec.ID 249 if _, ok := e.calls[id]; ok { 250 return fmt.Errorf("duplicate spec: %s", id) 251 } 252 253 ethCall, err := NewCall(d) 254 if err != nil { 255 return fmt.Errorf("failed to create data source: %w", err) 256 } 257 258 // here ensure we are on the engine with the right network ID 259 // not an error, just return 260 if e.chainID.Load() != d.SourceChainID { 261 return nil 262 } 263 264 e.calls[id] = ethCall 265 } 266 267 return nil 268 } 269 270 func (e *Engine) OnSpecDeactivated(ctx context.Context, spec datasource.Spec) { 271 e.mu.Lock() 272 defer e.mu.Unlock() 273 switch spec.Data.Content().(type) { 274 case common.Spec: 275 id := spec.ID 276 delete(e.calls, id) 277 } 278 } 279 280 // Poll is called by the poller in it's own goroutine; it isn't part of the abci code path. 281 func (e *Engine) Poll(ctx context.Context, wallTime time.Time) { 282 // Don't take the mutex here to avoid blocking abci engine while doing potentially lengthy ethereum calls 283 // Instead call methods on the engine that take the mutex for a small time where needed. 284 // We do need to make use direct use of of e.log, e.client and e.forwarder; but these are static after creation 285 // and the methods used are safe for concurrent access. 286 lastEthBlock, err := e.client.HeaderByNumber(ctx, nil) 287 if err != nil { 288 e.log.Error("failed to get current block header", logging.Error(err)) 289 return 290 } 291 292 e.log.Info("tick", 293 logging.Uint64("chainID", e.chainID.Load()), 294 logging.Time("wallTime", wallTime), 295 logging.BigInt("ethBlock", lastEthBlock.Number), 296 logging.Time("ethTime", time.Unix(int64(lastEthBlock.Time), 0))) 297 298 // If the previous eth block has not been set, set it to the current eth block 299 if e.prevEthBlock == nil { 300 e.prevEthBlock = blockIndex{number: lastEthBlock.Number.Uint64(), time: lastEthBlock.Time} 301 e.lastSent = blockIndex{number: lastEthBlock.Number.Uint64(), time: lastEthBlock.Time} 302 } 303 304 // Go through an eth blocks one at a time until we get to the most recent one 305 for prevEthBlock := e.prevEthBlock; prevEthBlock.NumberU64() < lastEthBlock.Number.Uint64(); prevEthBlock = e.prevEthBlock { 306 nextBlockNum := big.NewInt(0).SetUint64(prevEthBlock.NumberU64() + e.blockInterval.Load()) 307 nextEthBlock, err := e.client.HeaderByNumber(ctx, nextBlockNum) 308 if err != nil { 309 e.log.Error("failed to get next block header", 310 logging.Error(err), 311 logging.Uint64("chain-id", e.chainID.Load()), 312 logging.Uint64("prev-block", prevEthBlock.NumberU64()), 313 logging.Uint64("last-block", lastEthBlock.Number.Uint64()), 314 logging.Uint64("expect-next-block", nextBlockNum.Uint64()), 315 ) 316 return 317 } 318 319 nextEthBlockIsh := blockIndex{number: nextEthBlock.Number.Uint64(), time: nextEthBlock.Time} 320 for specID, call := range e.getCalls() { 321 if call.triggered(prevEthBlock, nextEthBlockIsh) { 322 res, err := call.Call(ctx, e.client, nextEthBlock.Number.Uint64()) 323 if err != nil { 324 e.log.Error("failed to call contract", logging.Error(err), logging.String("spec-id", specID), logging.Uint64("chain-id", e.chainID.Load())) 325 event := makeErrorChainEvent(err.Error(), specID, nextEthBlockIsh, e.chainID.Load()) 326 e.forwarder.ForwardFromSelf(event) 327 e.lastSent = nextEthBlockIsh 328 continue 329 } 330 331 if res.PassesFilters { 332 event := makeChainEvent(res, specID, nextEthBlockIsh, e.chainID.Load()) 333 e.forwarder.ForwardFromSelf(event) 334 e.lastSent = nextEthBlockIsh 335 } 336 } 337 } 338 339 if e.sendHeartbeat(nextEthBlockIsh) { 340 // we've not forwarded an ethcall result for a while, send a dummy heartbeat 341 event := makeHeartbeat(nextEthBlockIsh, e.chainID.Load()) 342 e.forwarder.ForwardFromSelf(event) 343 e.lastSent = nextEthBlockIsh 344 } 345 346 e.prevEthBlock = nextEthBlockIsh 347 } 348 } 349 350 // sendHeartbeat returns true if the difference in block time between the current eth block and the last sent even is 351 // above a given threshold. 352 func (e *Engine) sendHeartbeat(block blockish) bool { 353 now := time.Unix(int64(block.Time()), 0) 354 last := time.Unix(int64(e.lastSent.Time()), 0) 355 return last.Add(e.heartbeatInterval).Before(now) 356 } 357 358 func makeHeartbeat(block blockish, chainID uint64) *commandspb.ChainEvent { 359 ce := commandspb.ChainEvent{ 360 TxId: "internal", // NA 361 Nonce: 0, // NA 362 Event: &commandspb.ChainEvent_ContractCall{ 363 ContractCall: &vega.EthContractCallEvent{ 364 BlockHeight: block.NumberU64(), 365 BlockTime: block.Time(), 366 SourceChainId: ptr.From(chainID), 367 Heartbeat: true, 368 }, 369 }, 370 } 371 372 return &ce 373 } 374 375 func makeChainEvent(res Result, specID string, block blockish, chainID uint64) *commandspb.ChainEvent { 376 ce := commandspb.ChainEvent{ 377 TxId: "internal", // NA 378 Nonce: 0, // NA 379 Event: &commandspb.ChainEvent_ContractCall{ 380 ContractCall: &vega.EthContractCallEvent{ 381 SpecId: specID, 382 BlockHeight: block.NumberU64(), 383 BlockTime: block.Time(), 384 Result: res.Bytes, 385 SourceChainId: ptr.From(chainID), 386 Heartbeat: false, 387 }, 388 }, 389 } 390 391 return &ce 392 } 393 394 func makeErrorChainEvent(errMsg string, specID string, block blockish, chainID uint64) *commandspb.ChainEvent { 395 ce := commandspb.ChainEvent{ 396 TxId: "internal", // NA 397 Nonce: 0, // NA 398 Event: &commandspb.ChainEvent_ContractCall{ 399 ContractCall: &vega.EthContractCallEvent{ 400 SpecId: specID, 401 BlockHeight: block.NumberU64(), 402 BlockTime: block.Time(), 403 Error: &errMsg, 404 SourceChainId: ptr.From(chainID), 405 }, 406 }, 407 } 408 409 return &ce 410 } 411 412 func (e *Engine) ReloadConf(cfg Config) { 413 e.log.Info("Reloading configuration") 414 415 if e.log.GetLevel() != cfg.Level.Get() { 416 e.log.Debug("Updating log level", 417 logging.String("old", e.log.GetLevel().String()), 418 logging.String("new", cfg.Level.String()), 419 ) 420 e.log.SetLevel(cfg.Level.Get()) 421 } 422 } 423 424 // This is copy-pasted from the ethereum engine; at some point this two should probably be folded into one, 425 // but just for now keep them separate to ensure we don't break existing functionality. 426 type poller struct { 427 done chan bool 428 pollEvery time.Duration 429 } 430 431 func newPoller(pollEvery time.Duration) *poller { 432 return &poller{ 433 done: make(chan bool, 1), 434 pollEvery: pollEvery, 435 } 436 } 437 438 // Loop starts the poller loop until it's broken, using the Stop method. 439 func (s *poller) Loop(fn func()) { 440 ticker := time.NewTicker(s.pollEvery) 441 defer func() { 442 ticker.Stop() 443 ticker.Reset(s.pollEvery) 444 }() 445 446 for { 447 select { 448 case <-s.done: 449 return 450 case <-ticker.C: 451 fn() 452 } 453 } 454 } 455 456 // Stop stops the poller loop. 457 func (s *poller) Stop() { 458 s.done <- true 459 }