Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
67224fd
Add simple CLI to fetch and archive C-Chain blocks
aaronbuchwald Sep 19, 2025
f5a150f
Add ctx to errgroup
aaronbuchwald Sep 23, 2025
807bba2
fix linting
aaronbuchwald Sep 23, 2025
8215b5a
Merge branch 'master' into aaronbuchwald/fetch-blocks-cli
aaronbuchwald Sep 23, 2025
c1b3bdf
fix unterminated block
aaronbuchwald Sep 23, 2025
0d61b07
Merge branch 'master' into aaronbuchwald/fetch-blocks-cli
aaronbuchwald Oct 1, 2025
e0ef9e4
Merge branch 'master' into aaronbuchwald/fetch-blocks-cli
aaronbuchwald Oct 1, 2025
46aa5f7
revert spf13 mod updates
aaronbuchwald Oct 6, 2025
3baa7f6
mod tidy
aaronbuchwald Oct 6, 2025
27e83cd
Merge branch 'master' into aaronbuchwald/fetch-blocks-cli
aaronbuchwald Oct 6, 2025
6d1453c
fix go mod
aaronbuchwald Oct 6, 2025
c3ca734
Add ctx cancellation to fetchBlocks and separate from cli cmd
aaronbuchwald Oct 6, 2025
8b941c2
add simple e2e test for fetch blocks and check contents
aaronbuchwald Oct 6, 2025
a3b4906
remove tooling ci file in favor of e2e test
aaronbuchwald Oct 6, 2025
b26ecbf
fix lint import errors
aaronbuchwald Oct 6, 2025
985bef1
fix c-chain rpc uri for test
aaronbuchwald Oct 6, 2025
c424a4d
fix linting error on creating new uri
aaronbuchwald Oct 6, 2025
b3402fe
close blockdb in fetch blocks to block db
aaronbuchwald Oct 6, 2025
ead97a0
fix equals condition
aaronbuchwald Oct 6, 2025
2a19163
handle libevm type registration
aaronbuchwald Oct 6, 2025
c7ce1ef
fix import
aaronbuchwald Oct 6, 2025
efcbc8a
satisfy gci custom config
aaronbuchwald Oct 6, 2025
bd14efe
Align block parsing with vm ex
aaronbuchwald Oct 6, 2025
6a4bfee
Merge branch 'master' into aaronbuchwald/fetch-blocks-cli
aaronbuchwald Oct 7, 2025
b09d90d
Remove unnecessary custom import
aaronbuchwald Oct 7, 2025
ab764a3
Update require equal type to uint64
aaronbuchwald Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ require (
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
)

require (
Copy link
Contributor

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?

github.com/sourcegraph/conc v0.3.0 // indirect
github.com/zondax/ledger-go v1.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

require (
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
Expand Down Expand Up @@ -177,8 +185,7 @@ require (
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sanity-io/litter v1.5.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/status-im/keycard-go v0.2.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
Expand All @@ -189,7 +196,6 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zondax/golem v0.27.0 // indirect
github.com/zondax/hid v0.9.2 // indirect
github.com/zondax/ledger-go v1.0.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
Expand All @@ -202,9 +208,6 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
Expand Down
75 changes: 42 additions & 33 deletions tests/e2e/c/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package c

import (
"fmt"
"math/big"

"connectrpc.com/connect"
Expand All @@ -19,6 +20,46 @@ import (
pb "github.com/ava-labs/avalanchego/connectproto/pb/proposervm/proposervmconnect"
)

func byAdvancingCChainHeight(tc *e2e.GinkgoTestContext, minBlocks int) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
Expand All @@ -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()
Expand Down
79 changes: 79 additions & 0 deletions tests/e2e/c/fetch_blocks.go
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
Copy link
Contributor

@joshua-kim joshua-kim Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels weird to me that:

  1. utils.FetchBlocksToBlockDB is exported - this is an implementation detail of the cli
  2. We have an e2e test, but aren't actually testing the cli

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
Copy link
Preview

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The 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.

})
})
})
62 changes: 62 additions & 0 deletions tests/reexecute/c/cli/cmd/fetch_blocks.go
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid self-documenting comments

var fetchBlocksCmd = &cobra.Command{
Use: "fetchBlocks",
Copy link
Contributor

Choose a reason for hiding this comment

The 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)")
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}
Copy link
Preview

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The 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
}
}
if concurrency <= 0 {
return fmt.Errorf("invalid concurrency value: %d; must be greater than zero", concurrency)
}

Copilot uses AI. Check for mistakes.

return utils.FetchBlocksToBlockDB(context.Background(), cliLog, dbDir, startBlock, endBlock, rpcURL, concurrency)
Copy link
Contributor

@joshua-kim joshua-kim Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we probably want to use cmd.Context here - this might cause unexpected behavior if the root context is canceled since this is not a child of the parent.

}
86 changes: 86 additions & 0 deletions tests/reexecute/c/cli/cmd/root.go
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{
Copy link
Contributor

Choose a reason for hiding this comment

The 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 fetch-blocks command instead of having to use argo fetch-blocks?

Use: "argo",
Copy link
Contributor

@joshua-kim joshua-kim Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What is argo?
  2. The naming here isn't self-describing so it feels like it could be improved. We also already use argocd which is what I first though of when reading this.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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() {
Copy link
Contributor

@joshua-kim joshua-kim Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why do we define two init's? This introduces complexity since we now have to consciously be aware of how the initialization is ordered - which is not obvious since it is ordered according to build order. It might make sense for us to have a single init and merge these two.
  2. Go packages are generally fine with big files since there are no limits on numbers of structs/types per file like in other languages. Since the abstraction here is a cli, it would probably be fine to just stuff this all under main, or have one file for the cli and one for main.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. nit: early return for readability
  2. Is this config stuff overkill for what we're trying to do here? It seems like for a cli tool that just needs to download blocks in a range using the start/end range parameters would be sufficient - why do we care about supporting config files + env vars?

// 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())
}
}
10 changes: 10 additions & 0 deletions tests/reexecute/c/cli/main.go
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()
}
Loading