Skip to content
Open
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ require (
github.com/hashicorp/go-version v1.8.0
github.com/ktr0731/go-fuzzyfinder v0.9.0
github.com/manifoldco/promptui v0.9.0
github.com/nxadm/tail v1.4.11
github.com/opencontainers/image-spec v1.1.1
github.com/pelletier/go-toml/v2 v2.2.4
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.40.0
github.com/zalando/go-keyring v0.2.6
go.etcd.io/bbolt v1.4.0-alpha.1
go.uber.org/multierr v1.11.0
golang.org/x/term v0.37.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11
Expand Down Expand Up @@ -185,7 +187,6 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
Expand Down Expand Up @@ -236,7 +237,6 @@ require (
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
Expand Down Expand Up @@ -115,6 +117,8 @@ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
Expand Down
181 changes: 108 additions & 73 deletions internal/application/devnet/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ type ProvisionUseCase struct {
logger ports.Logger
}

type provisionPipelineState struct {
metadata *ports.DevnetMetadata
rpcEndpoint string
genesis []byte
chainID string
bech32Prefix string
accountsDir string
nodes []*ports.NodeMetadata
validators []ports.ValidatorInfo
}

// NewProvisionUseCase creates a new ProvisionUseCase.
func NewProvisionUseCase(
devnetRepo ports.DevnetRepository,
Expand Down Expand Up @@ -58,20 +69,36 @@ func NewProvisionUseCase(
func (uc *ProvisionUseCase) Execute(ctx context.Context, input dto.ProvisionInput) (*dto.ProvisionOutput, error) {
uc.logger.Info("Provisioning devnet...")

// Check if devnet already exists
pipelineState, err := uc.prepareMetadata(input)
if err != nil {
return nil, err
}

if err := uc.fetchGenesis(ctx, input, pipelineState); err != nil {
return nil, err
}

if err := uc.initializeKeysAndNodes(ctx, input, pipelineState); err != nil {
return nil, err
}

if err := uc.patchGenesis(ctx, input, pipelineState); err != nil {
return nil, err
}

return uc.persistState(ctx, input, pipelineState)
}

func (uc *ProvisionUseCase) prepareMetadata(input dto.ProvisionInput) (*provisionPipelineState, error) {
if uc.devnetRepo.Exists(input.HomeDir) {
return nil, fmt.Errorf("devnet already exists at %s", input.HomeDir)
}

// Determine execution mode
var execMode types.ExecutionMode
execMode := types.ExecutionModeLocal
if input.Mode == string(types.ExecutionModeDocker) {
execMode = types.ExecutionModeDocker
} else {
execMode = types.ExecutionModeLocal
}

// Create metadata
metadata := &ports.DevnetMetadata{
HomeDir: input.HomeDir,
NetworkName: input.Network,
Expand All @@ -83,163 +110,171 @@ func (uc *ProvisionUseCase) Execute(ctx context.Context, input dto.ProvisionInpu
Status: ports.StateCreated,
DockerImage: input.DockerImage,
CustomBinaryPath: input.CustomBinaryPath,
InitialVersion: input.StableVersion, // Store the deployed version
CurrentVersion: input.StableVersion, // Initially same as deployed version
InitialVersion: input.StableVersion,
CurrentVersion: input.StableVersion,
CreatedAt: time.Now(),
}

// Get RPC endpoint for fetching genesis
rpcEndpoint := ""
if uc.networkModule != nil {
rpcEndpoint = uc.networkModule.RPCEndpoint(input.Network)
if uc.networkModule == nil {
return nil, fmt.Errorf("no RPC endpoint available for network: %s", input.Network)
}

// Fetch genesis from RPC (required for initial provisioning)
rpcEndpoint := uc.networkModule.RPCEndpoint(input.Network)
if rpcEndpoint == "" {
return nil, fmt.Errorf("no RPC endpoint available for network: %s", input.Network)
}

uc.logger.Info("Fetching genesis from RPC %s...", rpcEndpoint)
rpcGenesis, err := uc.genesisSvc.FetchFromRPC(ctx, rpcEndpoint)
return &provisionPipelineState{
metadata: metadata,
rpcEndpoint: rpcEndpoint,
bech32Prefix: uc.networkModule.Bech32Prefix(),
accountsDir: paths.DevnetAccountsPath(input.HomeDir),
}, nil
}

func (uc *ProvisionUseCase) fetchGenesis(ctx context.Context, input dto.ProvisionInput, state *provisionPipelineState) error {
uc.logger.Info("Fetching genesis from RPC %s...", state.rpcEndpoint)
rpcGenesis, err := uc.genesisSvc.FetchFromRPC(ctx, state.rpcEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to fetch genesis from RPC: %w", err)
return fmt.Errorf("failed to fetch genesis from RPC: %w", err)
}

// Use snapshot-based export if requested
var genesis []byte
genesis := rpcGenesis
if input.UseSnapshot && uc.stateExportSvc != nil {
uc.logger.Info("Exporting genesis from snapshot state...")
genesis, err = uc.exportGenesisFromSnapshot(ctx, input, rpcGenesis)
if err != nil {
return nil, fmt.Errorf("failed to export genesis from snapshot: %w", err)
return fmt.Errorf("failed to export genesis from snapshot: %w", err)
}
} else {
genesis = rpcGenesis
}

// Determine chain ID to use from genesis
chainID, _ := extractChainID(genesis)
metadata.ChainID = chainID
state.metadata.ChainID = chainID
state.genesis = genesis
state.chainID = chainID

return nil
}

// Step 1: Create account keys for validators (for transaction signing)
func (uc *ProvisionUseCase) initializeKeysAndNodes(ctx context.Context, input dto.ProvisionInput, state *provisionPipelineState) error {
uc.logger.Info("Creating validator account keys...")
accountsDir := paths.DevnetAccountsPath(input.HomeDir)
accountKeys, err := uc.createAccountKeys(ctx, accountsDir, input.NumValidators, input.UseTestMnemonic)
accountKeys, err := uc.createAccountKeys(ctx, state.accountsDir, input.NumValidators, input.UseTestMnemonic)
if err != nil {
return nil, fmt.Errorf("failed to create account keys: %w", err)
return fmt.Errorf("failed to create account keys: %w", err)
}

// Step 2: Initialize nodes to generate consensus keys (for block signing)
uc.logger.Info("Initializing validator nodes...")
nodes, err := uc.initializeNodes(ctx, input, chainID)
nodes, err := uc.initializeNodes(ctx, input, state.chainID)
if err != nil {
return nil, fmt.Errorf("failed to initialize nodes: %w", err)
return fmt.Errorf("failed to initialize nodes: %w", err)
}

// Step 2.1: Save validator key information to JSON files for export-keys command
uc.logger.Debug("Saving validator key information...")
if err := uc.saveValidatorKeys(input.HomeDir, accountKeys, uc.networkModule.Bech32Prefix()); err != nil {
return nil, fmt.Errorf("failed to save validator keys: %w", err)
if err := uc.saveValidatorKeys(input.HomeDir, accountKeys, state.bech32Prefix); err != nil {
return fmt.Errorf("failed to save validator keys: %w", err)
}

// Step 2.2: Create and save additional account keys (for testing/transactions)
if input.NumAccounts > 0 {
uc.logger.Info("Creating %d additional account keys...", input.NumAccounts)
additionalAccounts, err := uc.createAdditionalAccountKeys(ctx, accountsDir, input.NumAccounts, input.UseTestMnemonic, input.NumValidators)
additionalAccounts, err := uc.createAdditionalAccountKeys(ctx, state.accountsDir, input.NumAccounts, input.UseTestMnemonic, input.NumValidators)
if err != nil {
return nil, fmt.Errorf("failed to create additional account keys: %w", err)
return fmt.Errorf("failed to create additional account keys: %w", err)
}

uc.logger.Debug("Saving account key information...")
if err := uc.saveAccountKeys(input.HomeDir, additionalAccounts); err != nil {
return nil, fmt.Errorf("failed to save account keys: %w", err)
return fmt.Errorf("failed to save account keys: %w", err)
}
}

// Step 2.5: Configure nodes with network-specific settings (config.toml, app.toml)
uc.logger.Info("Configuring node settings...")
if err := uc.configureNodes(ctx, nodes, chainID, input.NumValidators); err != nil {
return nil, fmt.Errorf("failed to configure nodes: %w", err)
if err := uc.configureNodes(ctx, nodes, state.chainID, input.NumValidators); err != nil {
return fmt.Errorf("failed to configure nodes: %w", err)
}

// Step 3: Build validator info combining consensus and account keys
uc.logger.Info("Building validator info...")
validators, err := uc.buildValidatorInfo(nodes, accountKeys, uc.networkModule.Bech32Prefix())
validators, err := uc.buildValidatorInfo(nodes, accountKeys, state.bech32Prefix)
if err != nil {
return nil, fmt.Errorf("failed to build validator info: %w", err)
return fmt.Errorf("failed to build validator info: %w", err)
}

// Step 4: Modify genesis with validators
uc.logger.Info("Modifying genesis for devnet (chainID: %s)...", chainID)
state.nodes = nodes
state.validators = validators
return nil
}

func (uc *ProvisionUseCase) patchGenesis(ctx context.Context, input dto.ProvisionInput, state *provisionPipelineState) error {
genesis := state.genesis

uc.logger.Info("Modifying genesis for devnet (chainID: %s)...", state.chainID)
if uc.networkModule != nil {
opts := ports.GenesisModifyOptions{
ChainID: chainID,
ChainID: state.chainID,
NumValidators: input.NumValidators,
AddValidators: validators,
AddValidators: state.validators,
}

// Check genesis size - gRPC has 4MB default limit
const grpcSizeLimit = 4 * 1024 * 1024 // 4MB
const grpcSizeLimit = 4 * 1024 * 1024
if len(genesis) > grpcSizeLimit {
// Use file-based modification for large genesis (e.g., exported mainnet ~90MB)
uc.logger.Info("Using file-based genesis modification (size: %.1f MB)", float64(len(genesis))/(1024*1024))
modifiedGenesis, err := uc.modifyGenesisViaFile(ctx, genesis, opts, input.HomeDir)
if err != nil {
return nil, fmt.Errorf("failed to modify genesis via file: %w", err)
return fmt.Errorf("failed to modify genesis via file: %w", err)
}
genesis = modifiedGenesis
} else {
// Use standard in-memory modification for small genesis
modifiedGenesis, err := uc.networkModule.ModifyGenesis(genesis, opts)
if err != nil {
return nil, fmt.Errorf("failed to modify genesis: %w", err)
return fmt.Errorf("failed to modify genesis: %w", err)
}
genesis = modifiedGenesis
}
uc.logger.Debug("Genesis modified with %d validators", len(validators))
uc.logger.Debug("Genesis modified with %d validators", len(state.validators))
}

// Step 4: Write modified genesis to all nodes
for _, node := range nodes {
for _, node := range state.nodes {
genesisPath := filepath.Join(node.HomeDir, "config", "genesis.json")
if err := os.WriteFile(genesisPath, genesis, 0644); err != nil {
return nil, fmt.Errorf("failed to write genesis to node %d: %w", node.Index, err)
return fmt.Errorf("failed to write genesis to node %d: %w", node.Index, err)
}
}

// Set genesis path in metadata
if len(nodes) > 0 {
metadata.GenesisPath = filepath.Join(nodes[0].HomeDir, "config", "genesis.json")
if len(state.nodes) > 0 {
state.metadata.GenesisPath = filepath.Join(state.nodes[0].HomeDir, "config", "genesis.json")
}

// Update metadata
metadata.Status = ports.StateProvisioned
state.metadata.Status = ports.StateProvisioned
now := time.Now()
metadata.LastProvisioned = &now
state.metadata.LastProvisioned = &now
state.genesis = genesis

return nil
}

// Save metadata
if err := uc.devnetRepo.Save(ctx, metadata); err != nil {
func (uc *ProvisionUseCase) persistState(
ctx context.Context,
input dto.ProvisionInput,
state *provisionPipelineState,
) (*dto.ProvisionOutput, error) {
if err := uc.devnetRepo.Save(ctx, state.metadata); err != nil {
return nil, fmt.Errorf("failed to save metadata: %w", err)
}

// Save nodes
for _, node := range nodes {
for _, node := range state.nodes {
if err := uc.nodeRepo.Save(ctx, node); err != nil {
uc.logger.Warn("Failed to save node %d: %v", node.Index, err)
}
}

// Build output
output := &dto.ProvisionOutput{
HomeDir: input.HomeDir,
ChainID: metadata.ChainID,
GenesisPath: metadata.GenesisPath,
ChainID: state.metadata.ChainID,
GenesisPath: state.metadata.GenesisPath,
NumValidators: input.NumValidators,
NumAccounts: input.NumAccounts,
Nodes: make([]dto.NodeInfo, len(nodes)),
Nodes: make([]dto.NodeInfo, len(state.nodes)),
}

for i, node := range nodes {
for i, node := range state.nodes {
output.Nodes[i] = dto.NodeInfo{
Index: node.Index,
Name: node.Name,
Expand Down
Loading
Loading