code.vegaprotocol.io/vega@v0.79.0/core/client/eth/secondary_client.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 eth
    17  
    18  import (
    19  	"context"
    20  	"encoding/hex"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"code.vegaprotocol.io/vega/core/types"
    28  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    29  
    30  	ethcommon "github.com/ethereum/go-ethereum/common"
    31  	"github.com/ethereum/go-ethereum/ethclient"
    32  )
    33  
    34  type SecondaryClient struct {
    35  	ETHClient
    36  	ethConfig *types.EVMChainConfig
    37  
    38  	// this is all just to prevent spamming the infura just
    39  	// to get the last height of the blockchain
    40  	mu                      sync.Mutex
    41  	currentHeightLastUpdate time.Time
    42  	currentHeight           uint64
    43  
    44  	retryDelay time.Duration
    45  }
    46  
    47  func SecondaryDial(ctx context.Context, cfg Config) (*SecondaryClient, error) {
    48  	if len(cfg.EVMBridgeConfigs) != 1 {
    49  		return nil, errors.New("require exactly one EVM bridge configuration")
    50  	}
    51  
    52  	evmCfg := cfg.EVMBridgeConfigs[0]
    53  	if len(evmCfg.RPCEndpoint) <= 0 {
    54  		return nil, fmt.Errorf("no rpc endpoint configured for chain-id: %s", evmCfg.ChainID)
    55  	}
    56  
    57  	ethClient, err := ethclient.DialContext(ctx, evmCfg.RPCEndpoint)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("couldn't instantiate secondary Ethereum client: %w", err)
    60  	}
    61  
    62  	return &SecondaryClient{
    63  		ETHClient:  newEthClientWrapper(ethClient),
    64  		retryDelay: cfg.RetryDelay.Get(),
    65  	}, nil
    66  }
    67  
    68  func (c *SecondaryClient) UpdateEthereumConfig(ctx context.Context, ethConfig *types.EVMChainConfig) error {
    69  	if c == nil {
    70  		return nil
    71  	}
    72  
    73  	netID, err := c.NetworkID(ctx)
    74  	if err != nil {
    75  		return fmt.Errorf("couldn't retrieve the network ID from the ethereum client: %w", err)
    76  	}
    77  
    78  	chainID, err := c.ChainID(ctx)
    79  	if err != nil {
    80  		return fmt.Errorf("couldn't retrieve the chain ID form the ethereum client: %w", err)
    81  	}
    82  
    83  	if netID.String() != ethConfig.NetworkID() {
    84  		return fmt.Errorf("updated network ID does not match the one set during start up, expected %s got %v", ethConfig.NetworkID(), netID)
    85  	}
    86  
    87  	if chainID.String() != ethConfig.ChainID() {
    88  		return fmt.Errorf("updated chain ID does not match the one set during start up, expected %v got %v", ethConfig.ChainID(), chainID)
    89  	}
    90  
    91  	c.ethConfig = ethConfig
    92  
    93  	return nil
    94  }
    95  
    96  func (c *SecondaryClient) CollateralBridgeAddress() ethcommon.Address {
    97  	return c.ethConfig.CollateralBridge().Address()
    98  }
    99  
   100  func (c *SecondaryClient) CollateralBridgeAddressHex() string {
   101  	return c.ethConfig.CollateralBridge().HexAddress()
   102  }
   103  
   104  // IsEthereum returns whether or not this client is the "primary" one and pointing to Ethereum.
   105  func (c *SecondaryClient) IsEthereum() bool {
   106  	return false
   107  }
   108  
   109  func (c *SecondaryClient) CurrentHeight(ctx context.Context) (uint64, error) {
   110  	c.mu.Lock()
   111  	defer c.mu.Unlock()
   112  
   113  	if now := time.Now(); c.currentHeightLastUpdate.Add(c.retryDelay).Before(now) {
   114  		lastBlockHeader, err := c.HeaderByNumber(ctx, nil)
   115  		if err != nil {
   116  			return c.currentHeight, err
   117  		}
   118  		c.currentHeightLastUpdate = now
   119  		c.currentHeight = lastBlockHeader.Number.Uint64()
   120  	}
   121  
   122  	return c.currentHeight, nil
   123  }
   124  
   125  func (c *SecondaryClient) ConfirmationsRequired() uint64 {
   126  	return c.ethConfig.Confirmations()
   127  }
   128  
   129  // VerifyContract takes the address of a contract in hex and checks the hash of the byte-code is as expected.
   130  func (c *SecondaryClient) VerifyContract(ctx context.Context, address ethcommon.Address, expectedHash string) error {
   131  	// nil block number means latest block
   132  	b, err := c.CodeAt(ctx, address, nil)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	// the bytecode of the contract is appended which is deployment specific. We only care about
   138  	// the contract code itself and so we need to strip this meta-data before hashing it. For the version
   139  	// of Solidity we use, the format is [contract-bytecode]a264[CBOR-encoded meta-data]
   140  	asHex := strings.Split(hex.EncodeToString(b), "a264")
   141  	if len(asHex) != 2 {
   142  		return fmt.Errorf("%w: address: %s", ErrUnexpectedSolidityFormat, address)
   143  	}
   144  
   145  	// Back to bytes for hashing
   146  	b, err = hex.DecodeString(asHex[0])
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	h := hex.EncodeToString(vgcrypto.Hash(b))
   152  	if h != expectedHash {
   153  		return fmt.Errorf("%w: address: %s, hash: %s, expected: %s", ErrUnexpectedContractHash, address, h, expectedHash)
   154  	}
   155  
   156  	return nil
   157  }