diff --git a/lib/.changeset/v1.50.10.md b/lib/.changeset/v1.50.10.md index 9ee955d79..7c0e70e1e 100644 --- a/lib/.changeset/v1.50.10.md +++ b/lib/.changeset/v1.50.10.md @@ -1 +1 @@ -- Added Loki client to query logs data + tests, see usage here - [README](../../README.md) \ No newline at end of file +- Added Loki client to query logs data + tests, see usage here - [README](../../README.md) diff --git a/lib/README.md b/lib/README.md index 186d71dae..efb43e4ed 100644 --- a/lib/README.md +++ b/lib/README.md @@ -12,6 +12,7 @@ The purpose of this framework is to: + - Interact with different blockchains - Configure CL jobs - Deploy using `docker` diff --git a/lib/blockchain/blockchain.go b/lib/blockchain/blockchain.go index ceb8f0c5a..6015cb70c 100644 --- a/lib/blockchain/blockchain.go +++ b/lib/blockchain/blockchain.go @@ -165,7 +165,9 @@ func (h *SafeEVMHeader) UnmarshalJSON(bs []byte) error { h.Hash = jsonHead.Hash h.Number = (*big.Int)(jsonHead.Number) - h.Timestamp = time.Unix(int64(jsonHead.Timestamp), 0).UTC() + + jsonTimestamp := jsonHead.Timestamp + h.Timestamp = time.Unix(int64(jsonTimestamp), 0).UTC() // nolint gosec h.BaseFee = (*big.Int)(jsonHead.BaseFee) return nil } diff --git a/lib/blockchain/celo.go b/lib/blockchain/celo.go index ca74e4a5c..8c10fd65e 100644 --- a/lib/blockchain/celo.go +++ b/lib/blockchain/celo.go @@ -83,7 +83,7 @@ func (e *CeloClient) TransactionOpts(from *EthereumWallet) (*bind.TransactOpts, if err != nil { return nil, err } - opts.Nonce = big.NewInt(int64(nonce)) + opts.Nonce = new(big.Int).SetUint64(nonce) gasPrice, err := e.Client.SuggestGasPrice(context.Background()) if err != nil { diff --git a/lib/blockchain/ethereum.go b/lib/blockchain/ethereum.go index 4427084dd..7f2131423 100644 --- a/lib/blockchain/ethereum.go +++ b/lib/blockchain/ethereum.go @@ -11,6 +11,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "math/big" "regexp" "strconv" @@ -263,7 +264,7 @@ func (e *EthereumClient) HeaderTimestampByNumber(ctx context.Context, bn *big.In if err != nil { return 0, err } - return uint64(h.Timestamp.UTC().Unix()), nil + return uint64(h.Timestamp.UTC().Unix()), nil // nolint gosec } // BlockNumber gets latest block number @@ -539,7 +540,7 @@ func (e *EthereumClient) TransactionOpts(from *EthereumWallet) (*bind.TransactOp if err != nil { return nil, err } - opts.Nonce = big.NewInt(int64(nonce)) + opts.Nonce = new(big.Int).SetUint64(nonce) if e.NetworkConfig.MinimumConfirmations <= 0 { // Wait for your turn to send on an L2 chain <-e.NonceSettings.registerInstantTransaction(from.Address(), nonce) @@ -707,8 +708,7 @@ func (e *EthereumClient) WaitForFinalizedTx(txHash common.Hash) (*big.Int, time. // if the tx is finalized it returns true, the finalized header number by which the tx was considered finalized and the time at which it was finalized func (e *EthereumClient) IsTxHeadFinalized(txHdr, header *SafeEVMHeader) (bool, *big.Int, time.Time, error) { if e.NetworkConfig.FinalityDepth > 0 { - if header.Number.Cmp(new(big.Int).Add(txHdr.Number, - big.NewInt(int64(e.NetworkConfig.FinalityDepth)))) > 0 { + if header.Number.Cmp(new(big.Int).Add(txHdr.Number, new(big.Int).SetUint64(e.NetworkConfig.FinalityDepth))) > 0 { return true, header.Number, header.Timestamp, nil } return false, nil, time.Time{}, nil @@ -1100,7 +1100,7 @@ func (e *EthereumClient) GetLatestFinalizedBlockHeader(ctx context.Context) (*ty } latestBlockNumber := header.Number.Uint64() finalizedBlockNumber := latestBlockNumber - e.NetworkConfig.FinalityDepth - return e.Client.HeaderByNumber(ctx, big.NewInt(int64(finalizedBlockNumber))) + return e.Client.HeaderByNumber(ctx, new(big.Int).SetUint64(finalizedBlockNumber)) } // EstimatedFinalizationTime returns the estimated time it takes for a block to be finalized @@ -1118,10 +1118,14 @@ func (e *EthereumClient) EstimatedFinalizationTime(ctx context.Context) (time.Du if err != nil { return 0, err } - if e.NetworkConfig.FinalityDepth == 0 { + finDepth := e.NetworkConfig.FinalityDepth + if finDepth == 0 { return 0, fmt.Errorf("finality depth is 0 and finality tag is not enabled") } - timeBetween := time.Duration(e.NetworkConfig.FinalityDepth) * blckTime + if finDepth > math.MaxInt64 { + return 0, fmt.Errorf("finality depth %d is larger than the max value of int64", finDepth) + } + timeBetween := time.Duration(finDepth) * blckTime e.l.Info(). Str("Time", timeBetween.String()). Str("Network", e.GetNetworkName()). @@ -1159,7 +1163,7 @@ func (e *EthereumClient) TimeBetweenFinalizedBlocks(ctx context.Context, maxTime return 0, err } if nextFinalizedHeader.Number.Cmp(currentFinalizedHeader.Number) > 0 { - timeBetween := time.Unix(int64(nextFinalizedHeader.Time), 0).Sub(time.Unix(int64(currentFinalizedHeader.Time), 0)) + timeBetween := time.Unix(int64(nextFinalizedHeader.Time), 0).Sub(time.Unix(int64(currentFinalizedHeader.Time), 0)) // nolint gosec e.l.Info(). Str("Time", timeBetween.String()). Str("Network", e.GetNetworkName()). @@ -1190,23 +1194,26 @@ func (e *EthereumClient) AvgBlockTime(ctx context.Context) (time.Duration, error } totalTime := time.Duration(0) var previousHeader *types.Header - previousHeader, err = e.Client.HeaderByNumber(ctx, big.NewInt(int64(startBlockNumber-1))) + previousHeader, err = e.Client.HeaderByNumber(ctx, new(big.Int).SetUint64(startBlockNumber-1)) if err != nil { return totalTime, err } for i := startBlockNumber; i <= latestBlockNumber; i++ { - hdr, err := e.Client.HeaderByNumber(ctx, big.NewInt(int64(i))) + hdr, err := e.Client.HeaderByNumber(ctx, new(big.Int).SetUint64(i)) if err != nil { return totalTime, err } - blockTime := time.Unix(int64(hdr.Time), 0) - previousBlockTime := time.Unix(int64(previousHeader.Time), 0) + blockTime := time.Unix(int64(hdr.Time), 0) // nolint gosec + previousBlockTime := time.Unix(int64(previousHeader.Time), 0) // nolint gosec blockDuration := blockTime.Sub(previousBlockTime) totalTime += blockDuration previousHeader = hdr } + if numBlocks > math.MaxInt64 { + return 0, fmt.Errorf("numBlocks %d is larger than the max value of int64", numBlocks) + } averageBlockTime := totalTime / time.Duration(numBlocks) return averageBlockTime, nil diff --git a/lib/blockchain/transaction_confirmers.go b/lib/blockchain/transaction_confirmers.go index 2e912b1fc..bead36c5a 100644 --- a/lib/blockchain/transaction_confirmers.go +++ b/lib/blockchain/transaction_confirmers.go @@ -453,7 +453,7 @@ func (e *EthereumClient) backfillMissedBlocks(lastBlockSeen uint64, headerChanne Uint64("Latest Block", latestBlockNumber). Msg("Backfilling missed blocks since RPC connection issues") for i := lastBlockSeen + 1; i <= latestBlockNumber; i++ { - header, err := e.HeaderByNumber(context.Background(), big.NewInt(int64(i))) + header, err := e.HeaderByNumber(context.Background(), new(big.Int).SetUint64(i)) if err != nil { e.l.Err(err).Uint64("Number", i).Msg("Error getting header, unable to backfill and process it") return diff --git a/lib/client/rpc_suite_test.go b/lib/client/rpc_suite_test.go index e37d29ee5..198b1bb50 100644 --- a/lib/client/rpc_suite_test.go +++ b/lib/client/rpc_suite_test.go @@ -29,7 +29,7 @@ func TestRPCSuite(t *testing.T) { require.NoError(t, err) blockNumber, err := client.BlockNumber(context.Background()) require.NoError(t, err) - block, err := client.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber))) + block, err := client.BlockByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) // check the base fee of the block require.Equal(t, "2000000000", block.BaseFee().String(), "expected base fee to be 20 gwei") @@ -41,7 +41,7 @@ func TestRPCSuite(t *testing.T) { require.NoError(t, err) blockNumber, err = client.BlockNumber(context.Background()) require.NoError(t, err) - block, err = client.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber))) + block, err = client.BlockByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) // check the base fee of the block require.Equal(t, "3000000000", block.BaseFee().String(), "expected base fee to be 30 gwei") @@ -52,7 +52,7 @@ func TestRPCSuite(t *testing.T) { require.NoError(t, err) blockNumber, err = client.BlockNumber(context.Background()) require.NoError(t, err) - block, err = client.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber))) + block, err = client.BlockByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) // check the base fee of the block require.Equal(t, "2250000000", block.BaseFee().String(), "expected base fee to be 30 gwei") diff --git a/lib/client/rpc_test.go b/lib/client/rpc_test.go index f5adcc53e..633cc5b8f 100644 --- a/lib/client/rpc_test.go +++ b/lib/client/rpc_test.go @@ -211,8 +211,8 @@ func TestRPCAPI(t *testing.T) { pm.Stop() bn, err := client.BlockNumber(context.Background()) require.NoError(t, err) - require.GreaterOrEqual(t, uint64(iterations), bn-1) - require.LessOrEqual(t, uint64(iterations), bn+1) + require.GreaterOrEqual(t, uint64(iterations), bn-1) // nolint gosec + require.LessOrEqual(t, uint64(iterations), bn+1) // nolint gosec }) t.Run("(anvil) test we can mine blocks with strictly N+ transactions", func(t *testing.T) { @@ -234,7 +234,7 @@ func TestRPCAPI(t *testing.T) { } bn, err := client.BlockNumber(context.Background()) require.NoError(t, err) - for i := 1; i <= int(bn); i++ { + for i := 1; i <= int(bn); i++ { // nolint gosec block, err := client.BlockByNumber(context.Background(), big.NewInt(int64(i))) require.NoError(t, err) require.GreaterOrEqual(t, int64(block.Transactions().Len()), txnInBlock) diff --git a/lib/config/network.go b/lib/config/network.go index e1a646d23..2d9227298 100644 --- a/lib/config/network.go +++ b/lib/config/network.go @@ -90,21 +90,21 @@ func (n *NetworkConfig) OverrideURLsAndKeysFromEVMNetwork() { return } for name, evmNetwork := range n.EVMNetworks { - if evmNetwork.URLs != nil && len(evmNetwork.URLs) > 0 { + if len(evmNetwork.URLs) > 0 { logging.L.Warn().Str("network", name).Msg("found URLs in EVMNetwork. overriding RPC URLs in RpcWsUrls with EVMNetwork URLs") if n.RpcWsUrls == nil { n.RpcWsUrls = make(map[string][]string) } n.RpcWsUrls[name] = evmNetwork.URLs } - if evmNetwork.HTTPURLs != nil && len(evmNetwork.HTTPURLs) > 0 { + if len(evmNetwork.HTTPURLs) > 0 { logging.L.Warn().Str("network", name).Msg("found HTTPURLs in EVMNetwork. overriding RPC URLs in RpcHttpUrls with EVMNetwork HTTP URLs") if n.RpcHttpUrls == nil { n.RpcHttpUrls = make(map[string][]string) } n.RpcHttpUrls[name] = evmNetwork.HTTPURLs } - if evmNetwork.PrivateKeys != nil && len(evmNetwork.PrivateKeys) > 0 { + if len(evmNetwork.PrivateKeys) > 0 { logging.L.Warn().Str("network", name).Msg("found PrivateKeys in EVMNetwork. overriding wallet keys in WalletKeys with EVMNetwork private keys") if n.WalletKeys == nil { n.WalletKeys = make(map[string][]string) diff --git a/lib/gauntlet/gauntlet.go b/lib/gauntlet/gauntlet.go index b0dc56c92..a92306f10 100644 --- a/lib/gauntlet/gauntlet.go +++ b/lib/gauntlet/gauntlet.go @@ -65,7 +65,7 @@ func (g *Gauntlet) GenerateRandomNetwork() { type ExecCommandOptions struct { ErrHandling []string CheckErrorsInRead bool - RetryCount int + RetryCount int // TODO: set to uint RetryDelay time.Duration } @@ -154,7 +154,7 @@ func (g *Gauntlet) ExecCommandWithRetries(args []string, options ExecCommandOpti }, retry.Delay(options.RetryDelay), retry.MaxDelay(options.RetryDelay), - retry.Attempts(uint(options.RetryCount)), + retry.Attempts(uint(options.RetryCount)), // nolint gosec ) return output, err diff --git a/lib/logstream/logstream.go b/lib/logstream/logstream.go index 17f9531de..f7da4bf42 100644 --- a/lib/logstream/logstream.go +++ b/lib/logstream/logstream.go @@ -259,7 +259,7 @@ func (m *LogStream) ConnectContainer(ctx context.Context, container LogProducing startErr := retry.Do(func() error { return container.StartLogProducer(ctx, tc.WithLogProductionTimeout(timeout)) }, - retry.Attempts(uint(retryLimit)), + retry.Attempts(uint(retryLimit)), // nolint gosec retry.Delay(1*time.Second), retry.OnRetry(func(n uint, err error) { m.log.Info(). @@ -298,7 +298,7 @@ func (m *LogStream) ConnectContainer(ctx context.Context, container LogProducing return } } - }(cons.logListeningDone, m.loggingConfig.LogStream.LogProducerTimeout.Duration, int(*m.loggingConfig.LogStream.LogProducerRetryLimit)) + }(cons.logListeningDone, m.loggingConfig.LogStream.LogProducerTimeout.Duration, int(*m.loggingConfig.LogStream.LogProducerRetryLimit)) // nolint gosec return err } diff --git a/lib/utils/seth/seth.go b/lib/utils/seth/seth.go index 747f6c1f2..c78a05bbf 100644 --- a/lib/utils/seth/seth.go +++ b/lib/utils/seth/seth.go @@ -246,10 +246,10 @@ func ValidateSethNetworkConfig(cfg *pkg_seth.Network) error { if cfg == nil { return errors.New("network cannot be nil") } - if cfg.URLs == nil || len(cfg.URLs) == 0 { + if len(cfg.URLs) == 0 { return errors.New("URLs are required") } - if cfg.PrivateKeys == nil || len(cfg.PrivateKeys) == 0 { + if len(cfg.PrivateKeys) == 0 { return errors.New("PrivateKeys are required") } if cfg.TransferGasFee == 0 { diff --git a/seth/block_stats.go b/seth/block_stats.go index 33bfde4a0..587d61802 100644 --- a/seth/block_stats.go +++ b/seth/block_stats.go @@ -126,7 +126,14 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { totalSize := uint64(0) for i := 1; i < len(blocks); i++ { - duration := time.Unix(int64(blocks[i].Time()), 0).Sub(time.Unix(int64(blocks[i-1].Time()), 0)) + // Handle possible overflow + currentBlockTime := blocks[i].Time() + previousBlockTime := blocks[i-1].Time() + if currentBlockTime > math.MaxInt64 || previousBlockTime > math.MaxInt64 { + return fmt.Errorf("block time exceeds int64 range") + } + + duration := time.Unix(int64(currentBlockTime), 0).Sub(time.Unix(int64(previousBlockTime), 0)) durations = append(durations, duration) totalDuration += duration @@ -156,7 +163,7 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { L.Debug(). Uint64("BlockNumber", blocks[i].Number().Uint64()). - Time("BlockTime", time.Unix(int64(blocks[i].Time()), 0)). + Time("BlockTime", time.Unix(int64(currentBlockTime), 0)). Str("Duration", duration.String()). Float64("GasUsedPercentage", calculateRatioPercentage(blocks[i].GasUsed(), blocks[i].GasLimit())). Float64("TPS", tps). diff --git a/seth/client.go b/seth/client.go index 97e0237ac..040a5a4b2 100644 --- a/seth/client.go +++ b/seth/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "fmt" + "math" "math/big" "net/http" "path/filepath" @@ -58,7 +59,7 @@ type Client struct { Client *ethclient.Client Addresses []common.Address PrivateKeys []*ecdsa.PrivateKey - ChainID int64 + ChainID int64 // TODO: Use uint64 URL string Context context.Context CancelFunc context.CancelFunc @@ -253,13 +254,20 @@ func NewClientRaw( cfg.Network.ChainID = chainId.Uint64() } ctx, cancelFunc := context.WithCancel(context.Background()) + + // Protect from overflow + chainId := cfg.Network.ChainID + if chainId > math.MaxInt64 { + cancelFunc() + return nil, fmt.Errorf("chain ID is too big: %d", chainId) + } c := &Client{ Cfg: cfg, Client: client, Addresses: addrs, PrivateKeys: pkeys, URL: cfg.FirstNetworkURL(), - ChainID: int64(cfg.Network.ChainID), + ChainID: int64(chainId), Context: ctx, CancelFunc: cancelFunc, } @@ -433,13 +441,18 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri return errors.Wrap(err, "failed to get network ID") } - var gasLimit int64 + var gasLimit uint64 //nolint gasLimitRaw, err := m.EstimateGasLimitForFundTransfer(m.Addresses[fromKeyNum], common.HexToAddress(to), value) if err != nil { - gasLimit = m.Cfg.Network.TransferGasFee + // protect from overflow + gasFee := m.Cfg.Network.TransferGasFee + if gasFee < 0 { + return fmt.Errorf("transferGasFee is negative, and gas limit estimation failed: %w", err) + } + gasLimit = uint64(gasFee) } else { - gasLimit = int64(gasLimitRaw) + gasLimit = gasLimitRaw } if gasPrice == nil { @@ -565,7 +578,7 @@ func WithPending(pending bool) CallOpt { // WithBlockNumber sets blockNumber option for bind.CallOpts func WithBlockNumber(bn uint64) CallOpt { return func(o *bind.CallOpts) { - o.BlockNumber = big.NewInt(int64(bn)) + o.BlockNumber = new(big.Int).SetUint64(bn) } } @@ -916,7 +929,7 @@ func (m *Client) configureTransactionOpts( estimations GasEstimations, o ...TransactOpt, ) *bind.TransactOpts { - opts.Nonce = big.NewInt(int64(nonce)) + opts.Nonce = new(big.Int).SetUint64(nonce) opts.GasPrice = estimations.GasPrice opts.GasLimit = m.Cfg.Network.GasLimit @@ -945,7 +958,12 @@ func NewContractLoader[T any](client *Client) *ContractLoader[T] { // LoadContract loads contract by name, address, ABI loader and wrapper init function, it adds contract ABI to Seth Contract Store and address to Contract Map. Thanks to that we can easily // trace and debug interactions with the contract. Signatures of functions passed to this method were chosen to conform to Geth wrappers' GetAbi() and NewXXXContract() functions. -func (cl *ContractLoader[T]) LoadContract(name string, address common.Address, abiLoadFn func() (*abi.ABI, error), wrapperInitFn func(common.Address, bind.ContractBackend) (*T, error)) (*T, error) { +func (cl *ContractLoader[T]) LoadContract( + name string, + address common.Address, + abiLoadFn func() (*abi.ABI, error), + wrapperInitFn func(common.Address, bind.ContractBackend) (*T, error), +) (*T, error) { abiData, err := abiLoadFn() if err != nil { return new(T), err diff --git a/seth/client_api_test.go b/seth/client_api_test.go index 222b3ed13..a1fb6b95a 100644 --- a/seth/client_api_test.go +++ b/seth/client_api_test.go @@ -124,7 +124,7 @@ func TestAPINonces(t *testing.T) { { name: "with nonce override", transactionOpts: []seth.TransactOpt{ - seth.WithNonce(big.NewInt(int64(pnonce))), + seth.WithNonce(new(big.Int).SetUint64(pnonce)), }, }, } diff --git a/seth/gas.go b/seth/gas.go index b2979686f..702e01ac1 100644 --- a/seth/gas.go +++ b/seth/gas.go @@ -32,7 +32,7 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges fromNumber = 1 } } - hist, err := m.Client.Client.FeeHistory(context.Background(), fromNumber, big.NewInt(int64(bn)), []float64{priorityPerc}) + hist, err := m.Client.Client.FeeHistory(context.Background(), fromNumber, new(big.Int).SetUint64(bn), []float64{priorityPerc}) if err != nil { return GasSuggestions{}, err } diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 997972a59..c52b498d8 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -59,7 +59,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy timeout = 6 } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) // nolint gosec defer cancel() header, err := m.Client.HeaderByNumber(ctx, bn) if err != nil { @@ -79,7 +79,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy L.Trace().Msgf("Block range for gas calculation: %d - %d", lastBlockNumber-blocksNumber, lastBlockNumber) - lastBlock, err := getHeaderData(big.NewInt(int64(lastBlockNumber))) + lastBlock, err := getHeaderData(new(big.Int).SetUint64(lastBlockNumber)) if err != nil { return 0, err } @@ -114,7 +114,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy return } dataCh <- header - }(big.NewInt(int64(i))) + }(new(big.Int).SetUint64(i)) } wg.Wait() @@ -147,7 +147,7 @@ func calculateSimpleNetworkCongestionMetric(headers []*types.Header) float64 { func calculateNewestFirstNetworkCongestionMetric(headers []*types.Header) float64 { // sort blocks so that we are sure they are in ascending order slices.SortFunc(headers, func(i, j *types.Header) int { - return int(i.Number.Uint64() - j.Number.Uint64()) + return int(i.Number.Int64() - j.Number.Int64()) }) var weightedSum, totalWeight float64 diff --git a/seth/keyfile.go b/seth/keyfile.go index 6c29e3983..78c714c85 100644 --- a/seth/keyfile.go +++ b/seth/keyfile.go @@ -3,6 +3,8 @@ package seth import ( "context" "crypto/ecdsa" + "fmt" + "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -68,6 +70,9 @@ func ReturnFunds(c *Client, toAddr string) error { if err != nil { gasLimit = c.Cfg.Network.TransferGasFee } else { + if gasLimitRaw > math.MaxInt64 { + return fmt.Errorf("Gas limit exceeds maximum int64 value: %d", gasLimitRaw) + } gasLimit = int64(gasLimitRaw) } diff --git a/seth/nonce.go b/seth/nonce.go index 6c2e2131d..3b0c54a71 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -4,6 +4,8 @@ import ( "context" "crypto/ecdsa" "errors" + "fmt" + "math" "time" "math/big" @@ -31,7 +33,7 @@ type NonceManager struct { SyncedKeys chan *KeyNonce Addresses []common.Address PrivateKeys []*ecdsa.PrivateKey - Nonces map[common.Address]int64 + Nonces map[common.Address]int64 // TODO: Use big.Int or uint64 instead of int64 } type KeyNonce struct { @@ -66,14 +68,23 @@ func (m *NonceManager) UpdateNonces() error { if err != nil { return err } + if nonce > math.MaxInt64 { // Protect from overflow + return fmt.Errorf("nonce %d is larger than the max int64 value", nonce) + } m.Nonces[addr] = int64(nonce) } L.Debug().Interface("Nonces", m.Nonces).Msg("Updated nonces for addresses") m.SyncedKeys = make(chan *KeyNonce, len(m.Addresses)) for keyNum, addr := range m.Addresses[1:] { + // Protect from overflow + nonce := m.Nonces[addr] + if nonce < 0 { + return fmt.Errorf("negative nonce %d", nonce) + } + m.SyncedKeys <- &KeyNonce{ KeyNum: keyNum + 1, - Nonce: uint64(m.Nonces[addr]), + Nonce: uint64(nonce), } } return nil @@ -134,7 +145,7 @@ func (m *NonceManager) anySyncedKey() int { L.Trace(). Interface("KeyNum", keyData.KeyNum). Uint64("Nonce", nonce). - Int("Expected nonce", int(keyData.Nonce+1)). + Uint64("Expected nonce", keyData.Nonce+1). Interface("Address", m.Addresses[keyData.KeyNum]). Msg("Key NOT synced") diff --git a/seth/tracing.go b/seth/tracing.go index 8a7615082..2e2c6bc9b 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -399,7 +399,7 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e } if rawCall.Gas != "" && rawCall.Gas != "0x0" { - decimalValue, err := strconv.ParseInt(strings.TrimPrefix(rawCall.Gas, "0x"), 16, 64) + decimalValue, err := strconv.ParseUint(strings.TrimPrefix(rawCall.Gas, "0x"), 16, 64) if err != nil { L.Debug(). Err(err). @@ -411,14 +411,14 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e } if rawCall.GasUsed != "" && rawCall.GasUsed != "0x0" { - decimalValue, err := strconv.ParseInt(strings.TrimPrefix(rawCall.GasUsed, "0x"), 16, 64) + decimalValue, err := strconv.ParseUint(strings.TrimPrefix(rawCall.GasUsed, "0x"), 16, 64) if err != nil { L.Debug(). Err(err). Str("GasUsed", rawCall.GasUsed). Msg("Failed to parse value") } else { - defaultCall.GasUsed = uint64(decimalValue) + defaultCall.GasUsed = decimalValue } } diff --git a/seth/util.go b/seth/util.go index 0ef0a44f5..258a7efb4 100644 --- a/seth/util.go +++ b/seth/util.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "math" "math/big" "os" "path/filepath" @@ -61,6 +62,9 @@ func (m *Client) CalculateSubKeyFunding(addrs, gasPrice, rooKeyBuffer int64) (*F if err == nil { gasLimitRaw, err := m.EstimateGasLimitForFundTransfer(m.Addresses[0], common.HexToAddress(newAddress), big.NewInt(0).Quo(balance, big.NewInt(addrs))) if err == nil { + if gasLimitRaw > math.MaxInt64 { // Protect against overflow + return nil, fmt.Errorf("raw gas limit is larger than max int64: %d", gasLimitRaw) + } gasLimit = int64(gasLimitRaw) } } @@ -382,7 +386,7 @@ func DecodePragmaVersion(bytecode string) (Pragma, error) { } // each byte is represented by 2 characters in hex - metadataLengthInt := int(metadataByteLengthUint) * 2 + metadataLengthInt := int(metadataByteLengthUint) * 2 // nolint gosec // if we get nonsensical metadata length, it means that metadata section is not present and last 2 bytes do not represent metadata length if metadataLengthInt > len(bytecode) { diff --git a/seth/util_test.go b/seth/util_test.go index 5b48e6ae3..01e22d7b3 100644 --- a/seth/util_test.go +++ b/seth/util_test.go @@ -183,8 +183,8 @@ func TestUtilPendingNonce(t *testing.T) { seth.L.Debug().Msgf("Starting tx %d", index) opts := c.NewTXOpts() - nonceToUse := int64(getNonceAndIncrement()) - opts.Nonce = big.NewInt(nonceToUse) + nonceToUse := getNonceAndIncrement() + opts.Nonce = new(big.Int).SetUint64(nonceToUse) defer seth.L.Debug().Msgf("Finished tx %d with nonce %d", index, nonceToUse) _, _, _, err := network_sub_contract.DeployNetworkDebugSubContract(opts, c.Client) @@ -193,7 +193,7 @@ func TestUtilPendingNonce(t *testing.T) { if index == 50 { started <- struct{}{} } - nonceCh <- uint64(nonceToUse) + nonceCh <- nonceToUse }(i) } }() @@ -205,7 +205,7 @@ func TestUtilPendingNonce(t *testing.T) { pendingNonce, err := c.Client.PendingNonceAt(context.Background(), c.Addresses[testCase.keyNum]) require.NoError(t, err, "Error getting pending nonce") - require.Greater(t, int64(pendingNonce), int64(lastNonce), "Pending nonce should be greater than last nonce") + require.Greater(t, pendingNonce, lastNonce, "Pending nonce should be greater than last nonce") if testCase.keyNum == 0 { err = c.WaitUntilNoPendingTxForRootKey(testCase.timeout) diff --git a/tools/breakingchanges/cmd/main.go b/tools/breakingchanges/cmd/main.go index 60c2a5ece..096e7ea9b 100644 --- a/tools/breakingchanges/cmd/main.go +++ b/tools/breakingchanges/cmd/main.go @@ -48,8 +48,7 @@ func findGoModDirs(rootFolder, subDir string) ([]string, error) { } func getLastTag(pathPrefix string) (string, error) { - //nolint - cmd := exec.Command("sh", "-c", fmt.Sprintf("git tag | grep '%s' | tail -1", pathPrefix)) + cmd := exec.Command("sh", "-c", fmt.Sprintf("git tag | grep '%s' | tail -1", pathPrefix)) // #nosec G204 var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() diff --git a/tools/citool/README.md b/tools/citool/README.md index dfbf49d4a..42525b0d4 100644 --- a/tools/citool/README.md +++ b/tools/citool/README.md @@ -1,15 +1,5 @@ # A tool to manage E2E tests on Github CI -Available Commands: -check-tests Check if all tests in a directory are included in the test configurations YAML file -completion Generate the autocompletion script for the specified shell -csvexport Export tests to CSV format -filter Filter test configurations based on specified criteria -help Help about any command -test-config Manage test config - -## Usage - -```bash -go run main.go +```sh +go run . -h # See run commands ``` diff --git a/tools/citool/cmd/csv_export_cmd.go b/tools/citool/cmd/csv_export_cmd.go index a86a446a5..4ad6d7aa8 100644 --- a/tools/citool/cmd/csv_export_cmd.go +++ b/tools/citool/cmd/csv_export_cmd.go @@ -55,14 +55,14 @@ func exportConfigToCSV(configFile string) error { defer writer.Flush() // Write CSV headers - headers := []string{"ID", "Test Path", "Test Env Type", "Runs On", "Test Cmd", "Test Config Override Required", "Test Secrets Required", "Remote Runner Memory", "Pyroscope Env", "Triggers", "Test Inputs"} + headers := []string{"ID", "Test Path", "Test Env Type", "Runs On", "Chainlink Image Types", "Test Cmd", "Test Config Override Required", "Test Secrets Required", "Remote Runner Memory", "Pyroscope Env", "Triggers", "Test Inputs"} if err := writer.Write(headers); err != nil { return err } // Iterate over Tests and write data to CSV for _, test := range config.Tests { - triggers := strings.Join(test.Triggers, ", ") // Combine workflow triggers into a single CSV field + triggers := strings.Join(test.Triggers, ", ") // Combine triggers into a single CSV field // Serialize TestInputs testInputs := serializeMap(test.TestEnvVars) @@ -71,6 +71,7 @@ func exportConfigToCSV(configFile string) error { test.Path, test.TestEnvType, test.RunsOn, + strings.Join(test.ChainlinkImageTypes, ", "), test.TestCmd, fmt.Sprintf("%t", test.TestConfigOverrideRequired), fmt.Sprintf("%t", test.TestSecretsRequired), diff --git a/tools/citool/cmd/filter_cmd.go b/tools/citool/cmd/filter_cmd.go index 36f4558aa..85ed49c0a 100644 --- a/tools/citool/cmd/filter_cmd.go +++ b/tools/citool/cmd/filter_cmd.go @@ -15,20 +15,28 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/lib/utils" ) -// Filter tests based on workflow, test type, and test IDs. -func filterTests(allTests []CITestConf, workflow, testType, ids string, envresolve bool) []CITestConf { - workflowFilter := workflow - typeFilter := testType +// filterTests filters tests based on trigger, test type, test IDs, and chainlink image types. +func filterTests( + allTests []CITestConf, + trigger, testType, ids, chainlinkImageType string, + envresolve bool, +) []CITestConf { + triggerFilter := trigger + testTypeFilter := testType + imageTypeFilter := chainlinkImageType idFilter := strings.Split(ids, ",") var filteredTests []CITestConf for _, test := range allTests { - workflowMatch := workflow == "" || contains(test.Triggers, workflowFilter) - typeMatch := testType == "" || test.TestEnvType == typeFilter - idMatch := ids == "*" || ids == "" || contains(idFilter, test.ID) - - if workflowMatch && typeMatch && idMatch { + var ( + triggerMatch = trigger == "" || contains(test.Triggers, triggerFilter) + testTypeMatch = testType == "" || test.TestEnvType == testTypeFilter + imageTypeMatch = chainlinkImageType == "" || contains(test.ChainlinkImageTypes, imageTypeFilter) || (len(test.ChainlinkImageTypes) == 0 && imageTypeFilter == "amd64") // Default amd64 for all + idMatch = ids == "*" || ids == "" || contains(idFilter, test.ID) + ) + + if triggerMatch && testTypeMatch && idMatch && imageTypeMatch { test.IDSanitized = sanitizeTestID(test.ID) filteredTests = append(filteredTests, test) } @@ -42,7 +50,7 @@ func filterTests(allTests []CITestConf, workflow, testType, ids string, envresol return filteredTests } -func filterAndMergeTests(allTests []CITestConf, workflow, testType, base64Tests string, envresolve bool) ([]CITestConf, error) { +func filterAndMergeTests(allTests []CITestConf, trigger, testType, base64Tests string, envresolve bool) ([]CITestConf, error) { decodedBytes, err := base64.StdEncoding.DecodeString(base64Tests) if err != nil { return nil, err @@ -60,10 +68,10 @@ func filterAndMergeTests(allTests []CITestConf, workflow, testType, base64Tests var filteredTests []CITestConf for _, test := range allTests { - workflowMatch := workflow == "" || contains(test.Triggers, workflow) + triggerMatch := trigger == "" || contains(test.Triggers, trigger) typeMatch := testType == "" || test.TestEnvType == testType - if decodedTest, exists := idFilter[test.ID]; exists && workflowMatch && typeMatch { + if decodedTest, exists := idFilter[test.ID]; exists && triggerMatch && typeMatch { // Override test inputs from the base64 encoded tests for k, v := range decodedTest.TestEnvVars { if test.TestEnvVars == nil { @@ -105,14 +113,15 @@ func contains(slice []string, element string) bool { var filterCmd = &cobra.Command{ Use: "filter", Short: "Filter test configurations based on specified criteria", - Long: `Filters tests from a YAML configuration based on name, workflow, test type, and test IDs. + Long: `Filters tests from a YAML configuration based on name, trigger, test type, and test IDs. Example usage: -./e2e_tests_tool filter --file .github/e2e-tests.yml --workflow "Run Nightly E2E Tests" --test-env-type "docker" --test-ids "test1,test2"`, +./e2e_tests_tool filter --file .github/e2e-tests.yml --trigger "Run Nightly E2E Tests" --test-env-type "docker" --test-ids "test1,test2"`, Run: func(cmd *cobra.Command, _ []string) { yamlFile, _ := cmd.Flags().GetString("file") - workflow, _ := cmd.Flags().GetString("workflow") + trigger, _ := cmd.Flags().GetString("trigger") testType, _ := cmd.Flags().GetString("test-env-type") testIDs, _ := cmd.Flags().GetString("test-ids") + chainlinkImageType, _ := cmd.Flags().GetString("chainlink-image-type") testMap, _ := cmd.Flags().GetString("test-list") envresolve, _ := cmd.Flags().GetBool("envresolve") @@ -131,9 +140,9 @@ Example usage: var filteredTests []CITestConf if testMap == "" { - filteredTests = filterTests(config.Tests, workflow, testType, testIDs, envresolve) + filteredTests = filterTests(config.Tests, trigger, testType, testIDs, chainlinkImageType, envresolve) } else { - filteredTests, err = filterAndMergeTests(config.Tests, workflow, testType, testMap, envresolve) + filteredTests, err = filterAndMergeTests(config.Tests, trigger, testType, testMap, envresolve) if err != nil { log.Fatalf("Error filtering and merging tests: %v", err) } @@ -153,8 +162,9 @@ func init() { filterCmd.Flags().StringP("file", "f", "", "Path to the YAML file") filterCmd.Flags().String("test-list", "", "Base64 encoded list of tests (YML objects) to filter by. Can include test_inputs for each test.") filterCmd.Flags().StringP("test-ids", "i", "*", "Comma-separated list of test IDs to filter by") + filterCmd.Flags().StringP("chainlink-image-type", "c", "amd64", "Build type of Chainlink image to filter by (e.g. 'amd64', 'arm64')") filterCmd.Flags().StringP("test-env-type", "y", "", "Type of test to filter by") - filterCmd.Flags().StringP("workflow", "t", "", "Workflow filter") + filterCmd.Flags().StringP("trigger", "t", "", "trigger filter") filterCmd.Flags().Bool("envresolve", false, "Resolve environment variables in test inputs") err := filterCmd.MarkFlagRequired("file") diff --git a/tools/citool/cmd/filter_cmd_test.go b/tools/citool/cmd/filter_cmd_test.go index 81c9c6307..3a69d9966 100644 --- a/tools/citool/cmd/filter_cmd_test.go +++ b/tools/citool/cmd/filter_cmd_test.go @@ -24,7 +24,36 @@ func TestFilterTestsByID(t *testing.T) { for _, c := range cases { t.Run(c.description, func(t *testing.T) { - filtered := filterTests(tests, "", "", c.inputIDs, false) + filtered := filterTests(tests, "", "", c.inputIDs, "", false) + if len(filtered) != c.expectedLen { + t.Errorf("FilterTests(%s) returned %d tests, expected %d", c.description, len(filtered), c.expectedLen) + } + }) + } +} + +func TestFilterTestsByChainlinkImageType(t *testing.T) { + tests := []CITestConf{ + {ChainlinkImageTypes: []string{"arm64"}, TestEnvType: "docker"}, + {ChainlinkImageTypes: []string{"amd64"}, TestEnvType: "docker"}, + {ChainlinkImageTypes: []string{"plugins", "arm64"}, TestEnvType: "k8s_remote_runner"}, + {ChainlinkImageTypes: []string{}, TestEnvType: "docker"}, // Empty image type should default to amd64 + } + + cases := []struct { + description string + inputChainlinkImageTypes string + expectedLen int + }{ + {"Filter by single type", "arm64", 2}, + {"Filter by single type once", "amd64", 2}, + {"Filter by non-existent type", "nonsense", 0}, + {"Empty type string to include all", "", 4}, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + filtered := filterTests(tests, "", "", "", c.inputChainlinkImageTypes, false) if len(filtered) != c.expectedLen { t.Errorf("FilterTests(%s) returned %d tests, expected %d", c.description, len(filtered), c.expectedLen) } @@ -42,7 +71,7 @@ func TestFilterTestsIntegration(t *testing.T) { cases := []struct { description string inputNames string - inputWorkflow string + inputTrigger string inputTestType string inputIDs string expectedLen int @@ -55,7 +84,7 @@ func TestFilterTestsIntegration(t *testing.T) { for _, c := range cases { t.Run(c.description, func(t *testing.T) { - filtered := filterTests(tests, c.inputWorkflow, c.inputTestType, c.inputIDs, false) + filtered := filterTests(tests, c.inputTrigger, c.inputTestType, c.inputIDs, "", false) if len(filtered) != c.expectedLen { t.Errorf("FilterTests(%s) returned %d tests, expected %d", c.description, len(filtered), c.expectedLen) } diff --git a/tools/citool/cmd/types.go b/tools/citool/cmd/types.go index acdd18adc..89913b3df 100644 --- a/tools/citool/cmd/types.go +++ b/tools/citool/cmd/types.go @@ -7,11 +7,18 @@ type Test struct { // CITestConf defines the configuration for running a test in a CI environment, specifying details like test ID, path, type, runner settings, command, and associated triggers. type CITestConf struct { - ID string `yaml:"id" json:"id"` - IDSanitized string `json:"id_sanitized"` - Path string `yaml:"path" json:"path"` - TestEnvType string `yaml:"test_env_type" json:"test_env_type"` - RunsOn string `yaml:"runs_on" json:"runs_on"` + ID string `yaml:"id" json:"id"` + IDSanitized string `json:"id_sanitized"` + // Name is a human-readable name for the test + Name string `yaml:"name" json:"name"` + Path string `yaml:"path" json:"path"` + TestEnvType string `yaml:"test_env_type" json:"test_env_type"` + // RunsOn denotes the type of GitHub Actions runner to use for the test: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates + RunsOn string `yaml:"runs_on" json:"runs_on"` + // RunsOnARM64 denotes what type of GitHub Actions runner to use for the test when running on ARM64 architecture. Only used if ChainlinkImageTypes specifies ARM64. + RunsOnARM64 string `yaml:"runs_on_arm64" json:"runs_on_arm64"` + // ChainlinkImageTypes is a list of Chainlink image variants to test with + ChainlinkImageTypes []string `yaml:"chainlink_image_types" json:"chainlink_image_types"` TestCmd string `yaml:"test_cmd" json:"test_cmd"` TestConfigOverrideRequired bool `yaml:"test_config_override_required" json:"test_config_override_required"` TestConfigOverridePath string `yaml:"test_config_override_path" json:"test_config_override_path"`