-
Notifications
You must be signed in to change notification settings - Fork 807
Add simple CLI to fetch and archive C-Chain blocks #4305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
67224fd
f5a150f
807bba2
8215b5a
c1b3bdf
0d61b07
e0ef9e4
46aa5f7
3baa7f6
27e83cd
6d1453c
c3ca734
8b941c2
a3b4906
b26ecbf
985bef1
c424a4d
b3402fe
ead97a0
2a19163
c7ce1ef
efcbc8a
bd14efe
6a4bfee
b09d90d
ab764a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
package c | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
|
||
"connectrpc.com/connect" | ||
|
@@ -19,6 +20,46 @@ import ( | |
pb "github.com/ava-labs/avalanchego/connectproto/pb/proposervm/proposervmconnect" | ||
) | ||
|
||
func byAdvancingCChainHeight(tc *e2e.GinkgoTestContext, minBlocks int) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this code move in this PR? |
||
require := require.New(tc) | ||
|
||
tc.By(fmt.Sprintf("advance the C-Chain height minimum of %d blocks", minBlocks), func() { | ||
env := e2e.GetEnv(tc) | ||
nodeURI := env.GetRandomNodeURI() | ||
ethClient := e2e.NewEthClient(tc, nodeURI) | ||
senderKey := env.PreFundedKey | ||
senderEthAddress := senderKey.EthAddress() | ||
recipientKey := e2e.NewPrivateKey(tc) | ||
recipientEthAddress := recipientKey.EthAddress() | ||
|
||
for i := 0; i < minBlocks; i++ { | ||
// Create and send a simple transaction to trigger block production | ||
nonce, err := ethClient.AcceptedNonceAt(tc.DefaultContext(), senderEthAddress) | ||
require.NoError(err) | ||
gasPrice := e2e.SuggestGasPrice(tc, ethClient) | ||
tx := types.NewTransaction( | ||
nonce, | ||
recipientEthAddress, | ||
big.NewInt(1000000000000000), | ||
e2e.DefaultGasLimit, | ||
gasPrice, | ||
nil, | ||
) | ||
|
||
// Sign transaction | ||
cChainID, err := ethClient.ChainID(tc.DefaultContext()) | ||
require.NoError(err) | ||
signer := types.NewEIP155Signer(cChainID) | ||
signedTx, err := types.SignTx(tx, signer, senderKey.ToECDSA()) | ||
require.NoError(err) | ||
|
||
// Send the transaction and wait for receipt | ||
receipt := e2e.SendEthTransaction(tc, ethClient, signedTx) | ||
require.Equal(types.ReceiptStatusSuccessful, receipt.Status) | ||
} | ||
}) | ||
} | ||
|
||
var _ = e2e.DescribeCChain("[ProposerVM API]", ginkgo.Label("proposervm"), func() { | ||
tc := e2e.NewTestContext() | ||
require := require.New(tc) | ||
|
@@ -27,39 +68,7 @@ var _ = e2e.DescribeCChain("[ProposerVM API]", ginkgo.Label("proposervm"), func( | |
env := e2e.GetEnv(tc) | ||
nodeURI := env.GetRandomNodeURI() | ||
|
||
tc.By("advancing the C-chain height", func() { | ||
ethClient := e2e.NewEthClient(tc, nodeURI) | ||
senderKey := env.PreFundedKey | ||
senderEthAddress := senderKey.EthAddress() | ||
recipientKey := e2e.NewPrivateKey(tc) | ||
recipientEthAddress := recipientKey.EthAddress() | ||
|
||
for i := 0; i < 3; i++ { | ||
// Create and send a simple transaction to trigger block production | ||
nonce, err := ethClient.AcceptedNonceAt(tc.DefaultContext(), senderEthAddress) | ||
require.NoError(err) | ||
gasPrice := e2e.SuggestGasPrice(tc, ethClient) | ||
tx := types.NewTransaction( | ||
nonce, | ||
recipientEthAddress, | ||
big.NewInt(1000000000000000), | ||
e2e.DefaultGasLimit, | ||
gasPrice, | ||
nil, | ||
) | ||
|
||
// Sign transaction | ||
cChainID, err := ethClient.ChainID(tc.DefaultContext()) | ||
require.NoError(err) | ||
signer := types.NewEIP155Signer(cChainID) | ||
signedTx, err := types.SignTx(tx, signer, senderKey.ToECDSA()) | ||
require.NoError(err) | ||
|
||
// Send the transaction and wait for receipt | ||
receipt := e2e.SendEthTransaction(tc, ethClient, signedTx) | ||
require.Equal(types.ReceiptStatusSuccessful, receipt.Status) | ||
} | ||
}) | ||
byAdvancingCChainHeight(tc, 3) | ||
|
||
// Get the proper C-chain ID for routing | ||
keychain := env.NewKeychain() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package c | ||
|
||
import ( | ||
"os" | ||
|
||
"github.com/ava-labs/libevm/core/types" | ||
"github.com/ava-labs/libevm/rlp" | ||
"github.com/onsi/ginkgo/v2" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/ava-labs/avalanchego/database" | ||
"github.com/ava-labs/avalanchego/tests/fixture/e2e" | ||
"github.com/ava-labs/avalanchego/tests/reexecute/utils" | ||
) | ||
|
||
var _ = e2e.DescribeCChain("[Fetch Blocks]", func() { | ||
tc := e2e.NewTestContext() | ||
require := require.New(tc) | ||
|
||
ginkgo.It("fetches created blocks", func() { | ||
env := e2e.GetEnv(tc) | ||
nodeURI := env.GetRandomNodeURI() | ||
cChainNodeURI := nodeURI.URI + "/ext/bc/C/rpc" | ||
|
||
fetchBlocksDBDir, err := os.MkdirTemp("", "fetch-blocks-test") | ||
require.NoError(err) | ||
defer func() { | ||
require.NoError(os.RemoveAll(fetchBlocksDBDir)) | ||
}() | ||
const ( | ||
startBlock = 1 | ||
endBlock = 10 | ||
numBlocks = 10 | ||
) | ||
byAdvancingCChainHeight(tc, numBlocks) | ||
ginkgo.By("fetching blocks", func() { | ||
require.NoError(utils.FetchBlocksToBlockDB( | ||
tc.DefaultContext(), | ||
tc.Log(), | ||
fetchBlocksDBDir, | ||
startBlock, | ||
endBlock, | ||
cChainNodeURI, | ||
numBlocks, | ||
)) | ||
}) | ||
Comment on lines
+39
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels weird to me that:
I feel like we either need to run a e2e test on the cli binary as a subprocess or do a unit test the root cmd type |
||
ginkgo.By("checking blockDB contents", func() { | ||
blockDB, err := utils.NewBlockDB(fetchBlocksDBDir) | ||
require.NoError(err) | ||
|
||
defer func() { | ||
require.NoError(blockDB.Close()) | ||
}() | ||
|
||
blockIter := blockDB.NewIteratorFromHeight(startBlock) | ||
|
||
expectedBlock := uint64(startBlock) | ||
for blockIter.Next() { | ||
blockHeightKey := blockIter.Key() | ||
blockBytes := blockIter.Value() | ||
|
||
require.Len(blockHeightKey, database.Uint64Size) | ||
blockHeight, err := database.ParseUInt64(blockHeightKey) | ||
require.NoError(err) | ||
require.Equal(expectedBlock, blockHeight) | ||
expectedBlock++ | ||
|
||
block := new(types.Block) | ||
require.NoError(rlp.DecodeBytes(blockBytes, block)) | ||
require.Equal(blockHeight, block.NumberU64()) | ||
} | ||
require.Equal(expectedBlock, uint64(endBlock)+1) | ||
require.NoError(blockIter.Error()) | ||
Comment on lines
+58
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Iterator is not released after use; other code paths (e.g., prior test helpers) defer Release to free underlying resources. Call blockIter.Release() (e.g., via defer right after creation) before closing the DB. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||||||||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||||||||||||
// See the file LICENSE for licensing terms. | ||||||||||||
|
||||||||||||
package cmd | ||||||||||||
|
||||||||||||
import ( | ||||||||||||
"context" | ||||||||||||
"fmt" | ||||||||||||
|
||||||||||||
"github.com/spf13/cobra" | ||||||||||||
|
||||||||||||
"github.com/ava-labs/avalanchego/tests/reexecute/utils" | ||||||||||||
) | ||||||||||||
|
||||||||||||
const ( | ||||||||||||
dbDirKey = "db-dir" | ||||||||||||
startBlockKey = "start-block" | ||||||||||||
endBlockKey = "end-block" | ||||||||||||
rpcURLKey = "rpc-url" | ||||||||||||
concurrencyKey = "concurrency" | ||||||||||||
) | ||||||||||||
|
||||||||||||
// fetchBlocksCmd represents the fetchBlocks command | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: avoid self-documenting comments |
||||||||||||
var fetchBlocksCmd = &cobra.Command{ | ||||||||||||
Use: "fetchBlocks", | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cli tooling typically uses kebab casing for multi-word expressions - additionally this camelCasing is inconsistent with the kebab-cased parameters |
||||||||||||
Short: "Fetch blocks from the network and write to the specified database", | ||||||||||||
RunE: runFetchBlocks, | ||||||||||||
} | ||||||||||||
|
||||||||||||
func init() { | ||||||||||||
rootCmd.AddCommand(fetchBlocksCmd) | ||||||||||||
|
||||||||||||
fetchBlocksCmd.Flags().String(dbDirKey, "", "Database to store the fetched blocks") | ||||||||||||
fetchBlocksCmd.Flags().String(rpcURLKey, "http://localhost:9650/ext/bc/C/rpc", "Ethereum RPC URL to fetch blocks from") | ||||||||||||
fetchBlocksCmd.Flags().Uint64(startBlockKey, 0, "Block number to start fetching from (inclusive)") | ||||||||||||
fetchBlocksCmd.Flags().Uint64(endBlockKey, 0, "Block number to stop fetching at (inclusive)") | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is stop inclusive? Typically range semantics are inclusive start and exclusive end (like loops), deviating might lead to confusion when using this tooling. |
||||||||||||
fetchBlocksCmd.Flags().Int(concurrencyKey, 1000, "Number of concurrent fetches to make") | ||||||||||||
} | ||||||||||||
|
||||||||||||
func runFetchBlocks(cmd *cobra.Command, _ []string) error { | ||||||||||||
dbDir, err := cmd.Flags().GetString(dbDirKey) | ||||||||||||
if err != nil { | ||||||||||||
return fmt.Errorf("failed to get db-dir flag: %w", err) | ||||||||||||
} | ||||||||||||
startBlock, err := cmd.Flags().GetUint64(startBlockKey) | ||||||||||||
if err != nil { | ||||||||||||
return fmt.Errorf("failed to get start-block flag: %w", err) | ||||||||||||
} | ||||||||||||
endBlock, err := cmd.Flags().GetUint64(endBlockKey) | ||||||||||||
if err != nil { | ||||||||||||
return fmt.Errorf("failed to get end-block flag: %w", err) | ||||||||||||
} | ||||||||||||
rpcURL, err := cmd.Flags().GetString(rpcURLKey) | ||||||||||||
if err != nil { | ||||||||||||
return fmt.Errorf("failed to get rpc-url flag: %w", err) | ||||||||||||
} | ||||||||||||
concurrency, err := cmd.Flags().GetInt(concurrencyKey) | ||||||||||||
if err != nil { | ||||||||||||
return fmt.Errorf("failed to get concurrency flag: %w", err) | ||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Concurrency flag is not validated; a negative value will cause a panic when used as the channel buffer size (make(chan uint64, concurrency)) and zero will spawn no workers. Add a check ensuring concurrency > 0 and return a descriptive error otherwise.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||
return utils.FetchBlocksToBlockDB(context.Background(), cliLog, dbDir, startBlock, endBlock, rpcURL, concurrency) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we probably want to use |
||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
|
||
"github.com/ava-labs/avalanchego/utils/logging" | ||
) | ||
|
||
var ( | ||
cfgFile string | ||
logLevelKey = "log-level" | ||
cliLog logging.Logger | ||
) | ||
|
||
// rootCmd represents the base command when called without any subcommands | ||
var rootCmd = &cobra.Command{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this root cmd? We only have one sub command anyways, would it make sense to just have a |
||
Use: "argo", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
Short: "Simple CLI tool to interact with re-execution", | ||
} | ||
|
||
// Execute adds all child commands to the root command and sets flags appropriately. | ||
// This is called by main.main(). It only needs to happen once to the rootCmd. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this helper? It's a helper around a function that's only used once + it's exported even though it's only usage is for main. |
||
func Execute() { | ||
err := rootCmd.Execute() | ||
if err != nil { | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func init() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
cobra.OnInitialize(initConfig) | ||
|
||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cli.yaml)") | ||
rootCmd.PersistentFlags().String(logLevelKey, logging.Info.String(), "Log level") | ||
|
||
rootCmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error { | ||
logLevelStr, err := rootCmd.PersistentFlags().GetString(logLevelKey) | ||
if err != nil { | ||
return fmt.Errorf("failed to get %q flag: %w", logLevelKey, err) | ||
} | ||
logLevel, err := logging.ToLevel(logLevelStr) | ||
Comment on lines
+47
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This is in a lot of spots in this PR but typically a line break is done after these braces |
||
if err != nil { | ||
return fmt.Errorf("failed to parse log-level flag %q: %w", logLevelStr, err) | ||
} | ||
cliLog = logging.NewLogger( | ||
"argo", | ||
logging.NewWrappedCore( | ||
logLevel, | ||
os.Stdout, | ||
logging.Colors.ConsoleEncoder(), | ||
), | ||
) | ||
return nil | ||
} | ||
} | ||
|
||
// initConfig reads in config file and ENV variables if set. | ||
func initConfig() { | ||
if cfgFile != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// Use config file from the flag. | ||
viper.SetConfigFile(cfgFile) | ||
} else { | ||
// Find home directory. | ||
home, err := os.UserHomeDir() | ||
cobra.CheckErr(err) | ||
|
||
// Search config in home directory with name ".cli" (without extension). | ||
viper.AddConfigPath(home) | ||
viper.SetConfigType("yaml") | ||
viper.SetConfigName(".cli") | ||
} | ||
|
||
viper.AutomaticEnv() // read in environment variables that match | ||
|
||
// If a config file is found, read it in. | ||
if err := viper.ReadInConfig(); err == nil { | ||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package main | ||
|
||
import "github.com/ava-labs/avalanchego/tests/reexecute/c/cli/cmd" | ||
|
||
func main() { | ||
cmd.Execute() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we meant to merge this into the below require block?