code.vegaprotocol.io/vega@v0.79.0/core/evtforward/tracker.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 evtforward 17 18 import ( 19 "context" 20 "encoding/hex" 21 "fmt" 22 "sort" 23 "strconv" 24 "time" 25 26 "code.vegaprotocol.io/vega/core/types" 27 "code.vegaprotocol.io/vega/core/validators" 28 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 29 "code.vegaprotocol.io/vega/libs/proto" 30 "code.vegaprotocol.io/vega/logging" 31 snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 32 33 "golang.org/x/exp/maps" 34 ) 35 36 //go:generate go run github.com/golang/mock/mockgen -destination mocks/witness_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier Witness 37 type Witness interface { 38 StartCheck(validators.Resource, func(interface{}, bool), time.Time) error 39 RestoreResource(validators.Resource, func(interface{}, bool)) error 40 } 41 42 type EVMEngine interface { 43 VerifyHeartbeat(context context.Context, height uint64, chainID string, contract string, blockTime uint64) error 44 UpdateStartingBlock(string, uint64) 45 } 46 47 type bridge struct { 48 // map from contract address -> block height of the last seen address 49 contractAddresses map[string]uint64 50 engine EVMEngine 51 } 52 53 type Tracker struct { 54 log *logging.Logger 55 witness Witness 56 timeService TimeService 57 58 // map from chain-id -> bridge contract information 59 bridges map[string]*bridge 60 61 pendingHeartbeats []*pendingHeartbeat 62 finalizedHeartbeats []*pendingHeartbeat 63 } 64 65 type pendingHeartbeat struct { 66 height uint64 67 blockTime uint64 68 chainID string 69 contractAddress string 70 check func(ctx context.Context) error 71 } 72 73 func (p pendingHeartbeat) GetID() string { 74 h := strconv.FormatUint(p.height, 10) 75 t := strconv.FormatUint(p.blockTime, 10) 76 bytes := []byte(h + t + p.chainID + p.contractAddress) 77 return hex.EncodeToString(vgcrypto.Hash(bytes)) 78 } 79 80 func (p pendingHeartbeat) GetChainID() string { 81 return p.chainID 82 } 83 84 func (p pendingHeartbeat) GetType() types.NodeVoteType { 85 return types.NodeVoteTypeEthereumHeartbeat 86 } 87 88 func (p *pendingHeartbeat) Check(ctx context.Context) error { return p.check(ctx) } 89 90 func NewTracker(log *logging.Logger, witness Witness, ts TimeService) *Tracker { 91 return &Tracker{ 92 log: log, 93 bridges: map[string]*bridge{}, 94 timeService: ts, 95 witness: witness, 96 } 97 } 98 99 func (t *Tracker) RegisterForwarder(fwd EVMEngine, chainID string, addresses ...string) { 100 contracts := map[string]uint64{} 101 for _, address := range addresses { 102 contracts[address] = 0 103 } 104 105 t.bridges[chainID] = &bridge{ 106 engine: fwd, 107 contractAddresses: contracts, 108 } 109 } 110 111 func (t *Tracker) ProcessHeartbeat(address, chainID string, height uint64, blockTime uint64) error { 112 // check if the heartbeat is too old, we don't care if we've already seen something at a higher block height 113 114 bridge, ok := t.bridges[chainID] 115 if !ok { 116 return fmt.Errorf("bridge does not exist for chain-id: %s", chainID) 117 } 118 119 last, ok := bridge.contractAddresses[address] 120 if !ok { 121 return fmt.Errorf("contract address does not correspond to a bridge contract: %s", address) 122 } 123 124 if height <= last { 125 return fmt.Errorf("heartbeat is stale") 126 } 127 128 fwd := bridge.engine 129 130 // continue with verification 131 pending := &pendingHeartbeat{ 132 height: height, 133 blockTime: blockTime, 134 chainID: chainID, 135 contractAddress: address, 136 check: func(ctx context.Context) error { return fwd.VerifyHeartbeat(ctx, height, chainID, address, blockTime) }, 137 } 138 139 t.pendingHeartbeats = append(t.pendingHeartbeats, pending) 140 141 t.log.Info("bridge heartbeat received, starting validation", 142 logging.String("chain-id", chainID), 143 logging.String("contract-address", address), 144 logging.Uint64("height", height), 145 ) 146 147 err := t.witness.StartCheck( 148 pending, t.onVerified, t.timeService.GetTimeNow().Add(30*time.Minute)) 149 if err != nil { 150 t.log.Error("could not start witness routine", logging.String("id", pending.GetID())) 151 t.removeHeartbeat(pending.GetID()) 152 } 153 return nil 154 } 155 156 func (t *Tracker) removeHeartbeat(id string) error { 157 for i, v := range t.pendingHeartbeats { 158 if v.GetID() == id { 159 t.pendingHeartbeats = t.pendingHeartbeats[:i+copy(t.pendingHeartbeats[i:], t.pendingHeartbeats[i+1:])] 160 return nil 161 } 162 } 163 return fmt.Errorf("could not remove heartbeat: %s", id) 164 } 165 166 func (t *Tracker) onVerified(event interface{}, ok bool) { 167 pv, isHeartbeat := event.(*pendingHeartbeat) 168 if !isHeartbeat { 169 t.log.Errorf("expected pending heartbeat: %T", event) 170 return 171 } 172 173 if err := t.removeHeartbeat(pv.GetID()); err != nil { 174 t.log.Error("could not remove pending heartbeat", logging.Error(err)) 175 } 176 177 if ok { 178 t.finalizedHeartbeats = append(t.finalizedHeartbeats, pv) 179 } 180 } 181 182 // UpdateContractBlock if an external engine has processed a chain-event for the contract at this address 183 // then the last-seen block for the contract is updated. 184 func (t *Tracker) UpdateContractBlock(address, chainID string, height uint64) { 185 bridge, ok := t.bridges[chainID] 186 if !ok { 187 return 188 } 189 190 lastSeen, ok := bridge.contractAddresses[address] 191 if !ok || lastSeen < height { 192 bridge.contractAddresses[address] = height 193 } 194 } 195 196 func (t *Tracker) OnTick(ctx context.Context, tt time.Time) { 197 for _, heartbeat := range t.finalizedHeartbeats { 198 bridge := t.bridges[heartbeat.chainID] 199 200 lastSeen, ok := bridge.contractAddresses[heartbeat.contractAddress] 201 if !ok || lastSeen < heartbeat.height { 202 bridge.contractAddresses[heartbeat.contractAddress] = heartbeat.height 203 } 204 } 205 t.finalizedHeartbeats = nil 206 } 207 208 func (t *Tracker) serialise() ([]byte, error) { 209 pending := make([]*snapshotpb.EVMFwdPendingHeartbeat, 0, len(t.pendingHeartbeats)) 210 for _, p := range t.pendingHeartbeats { 211 pending = append(pending, 212 &snapshotpb.EVMFwdPendingHeartbeat{ 213 BlockHeight: p.height, 214 BlockTime: p.blockTime, 215 ContractAddress: p.contractAddress, 216 ChainId: p.chainID, 217 }, 218 ) 219 } 220 221 lastSeen := make([]*snapshotpb.EVMFwdLastSeen, 0, len(t.bridges)) 222 223 chainIDs := maps.Keys(t.bridges) 224 sort.Strings(chainIDs) 225 226 for _, cid := range chainIDs { 227 seenBlocks := t.bridges[cid].contractAddresses 228 contracts := maps.Keys(t.bridges[cid].contractAddresses) 229 sort.Strings(contracts) 230 for _, addr := range contracts { 231 t.log.Info("serialising last-seen contract block", 232 logging.String("chain-id", cid), 233 logging.String("contract-address", addr), 234 logging.Uint64("height", seenBlocks[addr]), 235 ) 236 lastSeen = append(lastSeen, 237 &snapshotpb.EVMFwdLastSeen{ 238 ChainId: cid, 239 ContractAddress: addr, 240 BlockHeight: seenBlocks[addr], 241 }, 242 ) 243 } 244 } 245 246 pl := types.Payload{ 247 Data: &types.PayloadEVMFwdHeartbeats{ 248 EVMFwdHeartbeats: &snapshotpb.EVMFwdHeartbeats{ 249 PendingHeartbeats: pending, 250 LastSeen: lastSeen, 251 }, 252 }, 253 } 254 255 return proto.Marshal(pl.IntoProto()) 256 } 257 258 func (t *Tracker) restorePendingHeartbeats(_ context.Context, heartbeats []*snapshotpb.EVMFwdPendingHeartbeat) { 259 t.pendingHeartbeats = make([]*pendingHeartbeat, 0, len(heartbeats)) 260 261 for _, hb := range heartbeats { 262 bridge, ok := t.bridges[hb.ChainId] 263 if !ok { 264 t.log.Panic("cannot restore pending heartbeat, bridge not registered", logging.String("chain-id", hb.ChainId)) 265 } 266 267 pending := &pendingHeartbeat{ 268 height: hb.BlockHeight, 269 blockTime: hb.BlockTime, 270 chainID: hb.ChainId, 271 contractAddress: hb.ContractAddress, 272 check: func(ctx context.Context) error { 273 return bridge.engine.VerifyHeartbeat(ctx, hb.BlockHeight, hb.ChainId, hb.ContractAddress, hb.BlockTime) 274 }, 275 } 276 277 t.pendingHeartbeats = append(t.pendingHeartbeats, pending) 278 279 if err := t.witness.RestoreResource(pending, t.onVerified); err != nil { 280 t.log.Panic("unable to restore pending heartbeat resource", logging.String("ID", pending.GetID()), logging.Error(err)) 281 } 282 } 283 } 284 285 func (t *Tracker) restoreLastSeen( 286 _ context.Context, 287 lastSeen []*snapshotpb.EVMFwdLastSeen, 288 ) { 289 for _, ls := range lastSeen { 290 bridge, ok := t.bridges[ls.ChainId] 291 if !ok { 292 t.log.Panic("cannot restore last seen block, bridge not registered", logging.String("chain-id", ls.ChainId)) 293 } 294 bridge.contractAddresses[ls.ContractAddress] = ls.BlockHeight 295 t.log.Info("restored last seen block height", 296 logging.String("chain-id", ls.ChainId), 297 logging.String("contract-address", ls.ContractAddress), 298 logging.Uint64("last-seen", ls.BlockHeight), 299 ) 300 } 301 } 302 303 func (t *Tracker) Namespace() types.SnapshotNamespace { 304 return types.EVMHeartbeatSnapshot 305 } 306 307 func (t *Tracker) Keys() []string { 308 return []string{"all"} 309 } 310 311 func (t *Tracker) Stopped() bool { 312 return false 313 } 314 315 func (t *Tracker) GetState(_ string) ([]byte, []types.StateProvider, error) { 316 data, err := t.serialise() 317 return data, nil, err 318 } 319 320 func (t *Tracker) LoadState(ctx context.Context, payload *types.Payload) ([]types.StateProvider, error) { 321 if t.Namespace() != payload.Data.Namespace() { 322 return nil, types.ErrInvalidSnapshotNamespace 323 } 324 325 switch pl := payload.Data.(type) { 326 case *types.PayloadEVMFwdHeartbeats: 327 t.restorePendingHeartbeats(ctx, pl.EVMFwdHeartbeats.PendingHeartbeats) 328 t.restoreLastSeen(ctx, pl.EVMFwdHeartbeats.LastSeen) 329 return nil, nil 330 default: 331 return nil, types.ErrUnknownSnapshotType 332 } 333 } 334 335 func (t *Tracker) OnStateLoaded(_ context.Context) error { 336 for _, bridge := range t.bridges { 337 for address, lastSeen := range bridge.contractAddresses { 338 t.log.Info("updating starting block after restore", 339 logging.String("address", address), 340 logging.Uint64("last-seen", lastSeen), 341 ) 342 bridge.engine.UpdateStartingBlock(address, lastSeen) 343 } 344 } 345 return nil 346 }