Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion backup/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
return fmt.Errorf("invalid config, %w", cfgErr)
}

// Fetch the chain ID once at the start
chainID, err := s.client.GetChainID()
if err != nil {
return fmt.Errorf("unable to fetch chain ID, %w", err)
}

// Determine the right bound
toBlock, boundErr := determineRightBound(s.client, cfg.ToBlock)
if boundErr != nil {
Expand Down Expand Up @@ -158,7 +164,9 @@
txData := &gnoland.TxWithMetadata{
Tx: tx,
Metadata: &gnoland.GnoTxMetadata{
Timestamp: block.Timestamp,
Timestamp: block.Timestamp,
BlockHeight: int64(block.Height),

Check failure on line 168 in backup/backup.go

View workflow job for this annotation

GitHub Actions / Go Test / test

unknown field BlockHeight in struct literal of type gnoland.GnoTxMetadata

Check failure on line 168 in backup/backup.go

View workflow job for this annotation

GitHub Actions / Go Test / test-with-race

unknown field BlockHeight in struct literal of type gnoland.GnoTxMetadata
ChainID: chainID,

Check failure on line 169 in backup/backup.go

View workflow job for this annotation

GitHub Actions / Go Test / test

unknown field ChainID in struct literal of type gnoland.GnoTxMetadata

Check failure on line 169 in backup/backup.go

View workflow job for this annotation

GitHub Actions / Go Test / test-with-race

unknown field ChainID in struct literal of type gnoland.GnoTxMetadata
},
}

Expand Down
3 changes: 3 additions & 0 deletions backup/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ type Client interface {

// GetTxResults returns the block transaction results (if any)
GetTxResults(block uint64) ([]*abci.ResponseDeliverTx, error)

// GetChainID returns the chain ID from the node status
GetChainID() (string, error)
}

type Block struct {
Expand Down
9 changes: 9 additions & 0 deletions backup/client/rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ func (c *Client) GetBlocks(ctx context.Context, from, to uint64) ([]*client.Bloc
return blocks, nil
}

func (c *Client) GetChainID() (string, error) {
status, err := c.client.Status()
if err != nil {
return "", fmt.Errorf("unable to fetch chain ID, %w", err)
}

return status.NodeInfo.Network, nil
}

func (c *Client) GetTxResults(block uint64) ([]*abci.ResponseDeliverTx, error) {
block64 := int64(block)

Expand Down
10 changes: 10 additions & 0 deletions backup/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ type (
getLatestBlockNumberDelegate func() (uint64, error)
getBlocksDelegate func(context.Context, uint64, uint64) ([]*client.Block, error)
getTxResultsDelegate func(uint64) ([]*abci.ResponseDeliverTx, error)
getChainIDDelegate func() (string, error)
)

type mockClient struct {
getLatestBlockNumberFn getLatestBlockNumberDelegate
getBlocksFn getBlocksDelegate
getTxResultsFn getTxResultsDelegate
getChainIDFn getChainIDDelegate
}

func (m *mockClient) GetLatestBlockNumber() (uint64, error) {
Expand All @@ -42,3 +44,11 @@ func (m *mockClient) GetTxResults(block uint64) ([]*abci.ResponseDeliverTx, erro

return nil, nil
}

func (m *mockClient) GetChainID() (string, error) {
if m.getChainIDFn != nil {
return m.getChainIDFn()
}

return "test-chain", nil
}
237 changes: 237 additions & 0 deletions cmd/genesis_assemble.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package main

import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"os"
"time"

"github.com/gnolang/gno/gno.land/pkg/gnoland"
_ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/tm2/pkg/amino"
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/peterbourgon/ff/v3/ffcli"
)

var (
errMissingInput = errors.New("--input is required")
errMissingOutput = errors.New("--output is required")
errMissingChainID = errors.New("--chain-id is required")
)

// genesisAssembleCfg is the genesis-assemble command configuration
type genesisAssembleCfg struct {
input string
output string
chainID string
initialHeight int64
genesisTime string
genesisTemplate string
}

// newGenesisAssembleCmd creates the genesis-assemble command
func newGenesisAssembleCmd() *ffcli.Command {
cfg := &genesisAssembleCfg{}

fs := flag.NewFlagSet("genesis-assemble", flag.ExitOnError)
cfg.registerFlags(fs)

return &ffcli.Command{
Name: "genesis-assemble",
ShortUsage: "genesis-assemble [flags]",
LongHelp: "Assembles a genesis.json from a JSONL tx export",
FlagSet: fs,
Exec: cfg.exec,
}
}

// registerFlags registers the genesis-assemble command flags
func (c *genesisAssembleCfg) registerFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.input,
"input",
"",
"the input JSONL file path (required)",
)

fs.StringVar(
&c.output,
"output",
"",
"the output genesis.json file path (required)",
)

fs.StringVar(
&c.chainID,
"chain-id",
"",
"the chain ID for the new genesis (required)",
)

fs.Int64Var(
&c.initialHeight,
"initial-height",
0,
"the initial block height (0 = infer from max block_height + 1)",
)

fs.StringVar(
&c.genesisTime,
"genesis-time",
"",
"the genesis time in RFC3339 format (default: now)",
)

fs.StringVar(
&c.genesisTemplate,
"genesis-template",
"",
"optional base genesis.json to merge app_state.txs into",
)
}

// exec executes the genesis-assemble command
func (c *genesisAssembleCfg) exec(_ context.Context, _ []string) error {
// Validate required flags
if c.input == "" {
return errMissingInput
}

if c.output == "" {
return errMissingOutput
}

if c.chainID == "" {
return errMissingChainID
}

// Read the JSONL input file
txs, maxBlockHeight, err := readJSONLTxs(c.input)
if err != nil {
return fmt.Errorf("unable to read JSONL input, %w", err)
}

// Determine initial height
initialHeight := c.initialHeight
if initialHeight == 0 && maxBlockHeight > 0 {
initialHeight = maxBlockHeight + 1
}

// NOTE: GenesisDoc does not yet have an InitialHeight field.
// When it's added (Jae is working on this), set it here.
// For now, just log the computed value.
fmt.Fprintf(os.Stderr, "Computed initial_height: %d (will be used when GenesisDoc supports it)\n", initialHeight)

// Determine genesis time
genesisTime := time.Now()
if c.genesisTime != "" {
parsed, parseErr := time.Parse(time.RFC3339, c.genesisTime)
if parseErr != nil {
return fmt.Errorf("unable to parse genesis time %q, %w", c.genesisTime, parseErr)
}

genesisTime = parsed
}

// Build or load genesis doc
var genDoc *bft.GenesisDoc

if c.genesisTemplate != "" {
// Load template genesis
tmplDoc, loadErr := bft.GenesisDocFromFile(c.genesisTemplate)
if loadErr != nil {
return fmt.Errorf("unable to load genesis template, %w", loadErr)
}

genDoc = tmplDoc
} else {
genDoc = &bft.GenesisDoc{}
}

// Set chain ID and genesis time
genDoc.ChainID = c.chainID
genDoc.GenesisTime = genesisTime

// Build the app state with txs
appState := gnoland.GnoGenesisState{
Txs: txs,
}

// If template had an existing app state, try to preserve non-tx fields
if c.genesisTemplate != "" && genDoc.AppState != nil {
if existing, ok := genDoc.AppState.(gnoland.GnoGenesisState); ok {
appState.Balances = existing.Balances
appState.Auth = existing.Auth
appState.Bank = existing.Bank
appState.VM = existing.VM
appState.Txs = txs // override txs with our export
}
}

genDoc.AppState = appState

// Marshal and write the output
genDocBytes, marshalErr := amino.MarshalJSONIndent(genDoc, "", " ")
if marshalErr != nil {
return fmt.Errorf("unable to marshal genesis doc, %w", marshalErr)
}

if writeErr := os.WriteFile(c.output, genDocBytes, 0o644); writeErr != nil {
return fmt.Errorf("unable to write genesis file, %w", writeErr)
}

fmt.Fprintf(os.Stderr, "Genesis file written to %s (%d txs, chain_id=%s)\n", c.output, len(txs), c.chainID)

return nil
}

// readJSONLTxs reads TxWithMetadata entries from a JSONL file.
// Returns the collected txs and the maximum block height found.
func readJSONLTxs(path string) ([]gnoland.TxWithMetadata, int64, error) {
file, err := os.Open(path)
if err != nil {
return nil, 0, fmt.Errorf("unable to open input file, %w", err)
}
defer file.Close()

var (
txs []gnoland.TxWithMetadata
maxBlockHeight int64
)

scanner := bufio.NewScanner(file)

// Increase scanner buffer for large lines
scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)

lineNum := 0
for scanner.Scan() {
lineNum++

line := scanner.Bytes()
if len(line) == 0 {
continue
}

var tx gnoland.TxWithMetadata
if unmarshalErr := amino.UnmarshalJSON(line, &tx); unmarshalErr != nil {
return nil, 0, fmt.Errorf("unable to unmarshal tx at line %d, %w", lineNum, unmarshalErr)
}

// Track max block height from metadata
if tx.Metadata != nil && tx.Metadata.BlockHeight > maxBlockHeight {
maxBlockHeight = tx.Metadata.BlockHeight
}

txs = append(txs, tx)
}

if scanErr := scanner.Err(); scanErr != nil {
return nil, 0, fmt.Errorf("unable to read input file, %w", scanErr)
}

return txs, maxBlockHeight, nil
}
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func main() {
cmd.Subcommands = []*ffcli.Command{
newBackupCmd(),
newRestoreCmd(),
newGenesisAssembleCmd(),
}

if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
Expand Down
Loading