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  }