code.vegaprotocol.io/vega@v0.79.0/core/evtforward/ethereum/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 ethereum 17 18 import ( 19 "context" 20 "errors" 21 "sync" 22 "time" 23 24 "code.vegaprotocol.io/vega/core/types" 25 "code.vegaprotocol.io/vega/logging" 26 "code.vegaprotocol.io/vega/protos/vega" 27 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 28 ) 29 30 const ( 31 engineLogger = "engine" 32 ) 33 34 var ErrInvalidHeartbeat = errors.New("forwarded heartbeat is invalid") 35 36 //go:generate go run github.com/golang/mock/mockgen -destination mocks/forwarder_mock.go -package mocks code.vegaprotocol.io/vega/core/evtforward/ethereum Forwarder 37 type Forwarder interface { 38 ForwardFromSelf(*commandspb.ChainEvent) 39 } 40 41 //go:generate go run github.com/golang/mock/mockgen -destination mocks/filterer_mock.go -package mocks code.vegaprotocol.io/vega/core/evtforward/ethereum Filterer 42 type Filterer interface { 43 FilterCollateralEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound) 44 FilterStakingEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound) 45 FilterVestingEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound) 46 FilterMultisigControlEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound) 47 CurrentHeight(context.Context) uint64 48 GetEthTime(ctx context.Context, atBlock uint64) (uint64, error) 49 } 50 51 // Contract wrapper around EthereumContract to keep track of the block heights we've checked. 52 type Contract struct { 53 types.EthereumContract 54 next uint64 // the block height we will next check for events, all block heights less than this will have events sent in 55 last uint64 // the block height we last sent out an event for this contract, including heartbeats 56 } 57 58 type Engine struct { 59 cfg Config 60 log *logging.Logger 61 poller *poller 62 63 filterer Filterer 64 forwarder Forwarder 65 66 chainID string 67 68 stakingDeployment *Contract 69 vestingDeployment *Contract 70 collateralDeployment *Contract 71 multisigDeployment *Contract 72 mu sync.Mutex 73 74 cancelEthereumQueries context.CancelFunc 75 76 // the number of blocks between heartbeats 77 heartbeatInterval uint64 78 } 79 80 type fwdWrapper struct { 81 f Forwarder 82 chainID string 83 } 84 85 func (f fwdWrapper) ForwardFromSelf(event *commandspb.ChainEvent) { 86 // add the chainID of the source on events where this is necessary 87 switch ev := event.Event.(type) { 88 case *commandspb.ChainEvent_Erc20: 89 ev.Erc20.ChainId = f.chainID 90 case *commandspb.ChainEvent_Erc20Multisig: 91 ev.Erc20Multisig.ChainId = f.chainID 92 default: 93 // do nothing 94 } 95 96 f.f.ForwardFromSelf(event) 97 } 98 99 func NewEngine( 100 cfg Config, 101 log *logging.Logger, 102 filterer Filterer, 103 forwarder Forwarder, 104 stakingDeployment types.EthereumContract, 105 vestingDeployment types.EthereumContract, 106 multiSigDeployment types.EthereumContract, 107 collateralDeployment types.EthereumContract, 108 chainID string, 109 blockTime time.Duration, 110 ) *Engine { 111 l := log.Named(engineLogger) 112 113 // given that the EVM bridge configs are and array the "unset" values do not get populated 114 // with reasonable defaults so we need to make sure they are set to something reasonable 115 // if they are left out 116 cfg.setDefaults() 117 118 // calculate the number of blocks in an hour, this will be the interval we send out heartbeats 119 heartbeatTime := cfg.HeartbeatIntervalForTestOnlyDoNotChange.Duration 120 heartbeatInterval := heartbeatTime.Seconds() / blockTime.Seconds() 121 122 return &Engine{ 123 cfg: cfg, 124 log: l, 125 poller: newPoller(cfg.PollEventRetryDuration.Get()), 126 filterer: filterer, 127 forwarder: fwdWrapper{forwarder, chainID}, 128 stakingDeployment: &Contract{stakingDeployment, stakingDeployment.DeploymentBlockHeight(), stakingDeployment.DeploymentBlockHeight()}, 129 vestingDeployment: &Contract{vestingDeployment, vestingDeployment.DeploymentBlockHeight(), vestingDeployment.DeploymentBlockHeight()}, 130 multisigDeployment: &Contract{multiSigDeployment, multiSigDeployment.DeploymentBlockHeight(), multiSigDeployment.DeploymentBlockHeight()}, 131 collateralDeployment: &Contract{collateralDeployment, collateralDeployment.DeploymentBlockHeight(), collateralDeployment.DeploymentBlockHeight()}, 132 chainID: chainID, 133 heartbeatInterval: uint64(heartbeatInterval), 134 } 135 } 136 137 func (e *Engine) UpdateCollateralStartingBlock(b uint64) { 138 e.collateralDeployment.next = b 139 } 140 141 func (e *Engine) UpdateStakingStartingBlock(b uint64) { 142 e.vestingDeployment.next = b 143 e.stakingDeployment.next = b 144 } 145 146 func (e *Engine) UpdateMultiSigControlStartingBlock(b uint64) { 147 e.multisigDeployment.next = b 148 } 149 150 func (e *Engine) ReloadConf(cfg Config) { 151 e.log.Info("Reloading configuration") 152 153 if e.log.GetLevel() != cfg.Level.Get() { 154 e.log.Debug("Updating log level", 155 logging.String("old", e.log.GetLevel().String()), 156 logging.String("new", cfg.Level.String()), 157 ) 158 e.log.SetLevel(cfg.Level.Get()) 159 } 160 } 161 162 // Start starts the polling of the Ethereum bridges, listens to the events 163 // they emit and forward it to the network. 164 func (e *Engine) Start() { 165 ctx, cancelEthereumQueries := context.WithCancel(context.Background()) 166 defer cancelEthereumQueries() 167 168 e.cancelEthereumQueries = cancelEthereumQueries 169 if e.log.IsDebug() { 170 e.log.Debug("Start listening for Ethereum events from") 171 } 172 173 e.poller.Loop(func() { 174 if e.log.IsDebug() { 175 e.log.Debug("Clock is ticking, gathering Ethereum events", 176 logging.String("chain-id", e.chainID), 177 logging.Uint64("next-collateral-block-number", e.collateralDeployment.next), 178 logging.Uint64("next-multisig-control-block-number", e.multisigDeployment.next), 179 logging.Uint64("next-staking-block-number", e.stakingDeployment.next), 180 ) 181 } 182 e.gatherEvents(ctx) 183 }) 184 } 185 186 func issueFilteringRequest(from, to, nBlocks uint64) (ok bool, actualTo uint64) { 187 if from > to { 188 return false, 0 189 } 190 return true, min(from+nBlocks, to) 191 } 192 193 func min(a, b uint64) uint64 { 194 if a < b { 195 return a 196 } 197 return b 198 } 199 200 func (e *Engine) gatherEvents(ctx context.Context) { 201 nBlocks := e.cfg.MaxEthereumBlocks 202 currentHeight := e.filterer.CurrentHeight(ctx) 203 e.mu.Lock() 204 defer e.mu.Unlock() 205 206 // Ensure we are not issuing a filtering request for non-existing block. 207 if ok, nextHeight := issueFilteringRequest(e.collateralDeployment.next, currentHeight, nBlocks); ok { 208 e.filterer.FilterCollateralEvents(ctx, e.collateralDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) { 209 e.forwarder.ForwardFromSelf(event) 210 e.collateralDeployment.last = h 211 }) 212 e.collateralDeployment.next = nextHeight + 1 213 e.sendHeartbeat(e.collateralDeployment) 214 } 215 216 // Ensure we are not issuing a filtering request for non-existing block. 217 if e.stakingDeployment.HasAddress() { 218 if ok, nextHeight := issueFilteringRequest(e.stakingDeployment.next, currentHeight, nBlocks); ok { 219 e.filterer.FilterStakingEvents(ctx, e.stakingDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) { 220 e.forwarder.ForwardFromSelf(event) 221 e.stakingDeployment.last = h 222 }) 223 e.stakingDeployment.next = nextHeight + 1 224 e.sendHeartbeat(e.stakingDeployment) 225 } 226 } 227 228 // Ensure we are not issuing a filtering request for non-existing block. 229 if e.vestingDeployment.HasAddress() { 230 if ok, nextHeight := issueFilteringRequest(e.vestingDeployment.next, currentHeight, nBlocks); ok { 231 e.filterer.FilterVestingEvents(ctx, e.vestingDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) { 232 e.forwarder.ForwardFromSelf(event) 233 e.vestingDeployment.last = h 234 }) 235 e.vestingDeployment.next = nextHeight + 1 236 e.sendHeartbeat(e.vestingDeployment) 237 } 238 } 239 240 // Ensure we are not issuing a filtering request for non-existing block. 241 if ok, nextHeight := issueFilteringRequest(e.multisigDeployment.next, currentHeight, nBlocks); ok { 242 e.filterer.FilterMultisigControlEvents(ctx, e.multisigDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) { 243 e.forwarder.ForwardFromSelf(event) 244 e.multisigDeployment.last = h 245 }) 246 e.multisigDeployment.next = nextHeight + 1 247 e.sendHeartbeat(e.multisigDeployment) 248 } 249 } 250 251 // sendHeartbeat checks whether it has been more than and hour since the validator sent a chain event for the given contract 252 // and if it has will send a heartbeat chain event so that core has an recent view on the last block checked for new events. 253 func (e *Engine) sendHeartbeat(contract *Contract) { 254 // how many heartbeat intervals between the last sent event, and the block height we're checking next 255 n := (contract.next - contract.last) / e.heartbeatInterval 256 if n == 0 { 257 return 258 } 259 260 height := contract.last + n*e.heartbeatInterval 261 time, err := e.filterer.GetEthTime(context.Background(), height) 262 if err != nil { 263 e.log.Error("unable to find eth-time for contract heartbeat", 264 logging.Uint64("height", height), 265 logging.String("chain-id", e.chainID), 266 logging.Error(err), 267 ) 268 return 269 } 270 271 e.forwarder.ForwardFromSelf( 272 &commandspb.ChainEvent{ 273 TxId: "internal", // NA 274 Nonce: 0, // NA 275 Event: &commandspb.ChainEvent_Heartbeat{ 276 Heartbeat: &vega.ERC20Heartbeat{ 277 ContractAddress: contract.HexAddress(), 278 BlockHeight: height, 279 SourceChainId: e.chainID, 280 BlockTime: time, 281 }, 282 }, 283 }, 284 ) 285 contract.last = height 286 } 287 288 // VerifyHeart checks that the block height of the heartbeat exists and contains the correct block time. It also 289 // checks that this node has checked the logs of the given contract address up to at least the given height. 290 func (e *Engine) VerifyHeartbeat(ctx context.Context, height uint64, chainID string, address string, blockTime uint64) error { 291 e.mu.Lock() 292 defer e.mu.Unlock() 293 294 t, err := e.filterer.GetEthTime(ctx, height) 295 if err != nil { 296 return err 297 } 298 299 if t != blockTime { 300 return ErrInvalidHeartbeat 301 } 302 303 var lastChecked uint64 304 if e.collateralDeployment.HexAddress() == address { 305 lastChecked = e.collateralDeployment.next - 1 306 } 307 308 if e.multisigDeployment.HexAddress() == address { 309 lastChecked = e.multisigDeployment.next - 1 310 } 311 312 if e.stakingDeployment.HexAddress() == address { 313 lastChecked = e.stakingDeployment.next - 1 314 } 315 316 if e.vestingDeployment.HexAddress() == address { 317 lastChecked = e.vestingDeployment.next - 1 318 } 319 320 // if the heartbeat block height is higher than the last block *this* node has checked for logs 321 // on the contract, then fail the verification 322 if lastChecked < height { 323 return ErrInvalidHeartbeat 324 } 325 return nil 326 } 327 328 // UpdateStartingBlock sets the height that we should starting looking for new events from for the given bridge contract address. 329 func (e *Engine) UpdateStartingBlock(address string, block uint64) { 330 e.mu.Lock() 331 defer e.mu.Unlock() 332 333 if block == 0 { 334 return 335 } 336 337 if e.collateralDeployment.HexAddress() == address { 338 e.collateralDeployment.last = block 339 e.collateralDeployment.next = block 340 return 341 } 342 343 if e.multisigDeployment.HexAddress() == address { 344 e.multisigDeployment.last = block 345 e.multisigDeployment.next = block 346 return 347 } 348 349 if e.stakingDeployment.HexAddress() == address { 350 e.stakingDeployment.last = block 351 e.stakingDeployment.next = block 352 return 353 } 354 355 if e.vestingDeployment.HexAddress() == address { 356 e.vestingDeployment.last = block 357 e.vestingDeployment.next = block 358 return 359 } 360 361 e.log.Warn("unexpected contract address starting block", 362 logging.String("chain-id", e.chainID), 363 logging.String("contract-address", address), 364 ) 365 } 366 367 // Stop stops the engine, its polling and event forwarding. 368 func (e *Engine) Stop() { 369 // Notify to stop on next iteration. 370 e.poller.Stop() 371 // Cancel any ongoing queries against Ethereum. 372 if e.cancelEthereumQueries != nil { 373 e.cancelEthereumQueries() 374 } 375 } 376 377 // poller wraps a poller that ticks every durationBetweenTwoEventFiltering. 378 type poller struct { 379 ticker *time.Ticker 380 done chan bool 381 durationBetweenTwoRetry time.Duration 382 } 383 384 func newPoller(durationBetweenTwoRetry time.Duration) *poller { 385 return &poller{ 386 ticker: time.NewTicker(durationBetweenTwoRetry), 387 done: make(chan bool, 1), 388 durationBetweenTwoRetry: durationBetweenTwoRetry, 389 } 390 } 391 392 // Loop starts the poller loop until it's broken, using the Stop method. 393 func (s *poller) Loop(fn func()) { 394 defer func() { 395 s.ticker.Stop() 396 s.ticker.Reset(s.durationBetweenTwoRetry) 397 }() 398 399 for { 400 select { 401 case <-s.done: 402 return 403 case <-s.ticker.C: 404 fn() 405 } 406 } 407 } 408 409 // Stop stops the poller loop. 410 func (s *poller) Stop() { 411 s.done <- true 412 }