diff --git a/README.md b/README.md index 18c483c4..ec51d2f2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ graph TD - **State Management**: Manages blockchain state including validators and consensus parameters. - **P2P Communication**: Implements transaction gossip across the network. - **RPC Endpoints**: Provides compatible API endpoints for clients to interact with. +- **Genesis DA Height Auto-Configuration**: Automatically configures the Data Availability (DA) start height for new chains at genesis by querying the DA node, reducing manual configuration steps for non-aggregator nodes. ## ABCI Compatibility @@ -58,6 +59,61 @@ This adapter achieves compatibility with ABCI by calling the appropriate methods Note, that because of the nature of ev-node (single proposer), **Vote Extensions are not supported**. The adapter will not call the `VoteExtensions` methods on the ABCI application, and any logic related to vote extensions should be handled separately or not used at all. +## Genesis DA Height Auto-Configuration + +When starting a new chain at genesis, `ev-abci` can automatically configure the Data Availability (DA) start height to optimize the startup process. This feature works by: + +1. **Detecting Genesis Conditions**: The system checks if the chain is starting at genesis (initial height = 1) and the DA start height is not manually configured (set to 0). + +2. **Node Type Verification**: The optimization only applies to non-aggregator nodes, as aggregator nodes have different DA requirements. + +3. **Automatic DA Height Retrieval**: When conditions are met, the system queries the DA node using the `GetGenesisDaHeight` RPC method to obtain the appropriate starting DA height. + +4. **Seamless Configuration**: The retrieved DA height is automatically applied to the node configuration, eliminating the need for manual DA height configuration in genesis scenarios. + +### Benefits + +- **Reduced Configuration Complexity**: Operators don't need to manually determine and set the DA start height for new chains. +- **Optimized Startup**: Automatically uses the correct DA height, preventing unnecessary synchronization of old DA blocks. +- **Error Prevention**: Eliminates misconfiguration issues related to DA start height settings. + +### Conditions for Activation + +The genesis DA height auto-configuration activates only when **all** of the following conditions are met: + +- The chain is at genesis (initial height = 1) +- The DA start height is unset (configured as 0) +- The node is **not** an aggregator +- The DA node supports the `GetGenesisDaHeight` RPC method + +If any condition is not met, the system logs the reason and continues with the existing configuration without modification. + +### Example Usage + +When starting a new chain with `ev-abci`, the system will automatically attempt to optimize the DA configuration: + +```bash +# Starting a new chain at genesis (no manual DA configuration needed) +./app start --rollkit.node.aggregator=false + +# System logs will show: +# INFO attempting to prefill genesis start DA height from node +# INFO successfully retrieved genesis DA height from node genesis_da_height=12345 +# INFO genesis start DA height has been automatically set start_height=12345 +``` + +For cases where auto-configuration is skipped: + +```bash +# Aggregator nodes skip the optimization +./app start --rollkit.node.aggregator=true +# LOG: skipping genesis DA height prefill: node is an aggregator + +# Chains with pre-configured DA height skip the optimization +./app start --rollkit.da.start_height=100 +# LOG: skipping genesis DA height prefill: DA start height already set start_height=100 +``` + ## Installation ```bash diff --git a/server/genesis_da_height_test.go b/server/genesis_da_height_test.go new file mode 100644 index 00000000..bd53dd3c --- /dev/null +++ b/server/genesis_da_height_test.go @@ -0,0 +1,141 @@ +package server + +import ( + "context" + "testing" + "time" + + "cosmossdk.io/log" + "github.com/evstack/ev-node/pkg/config" + "github.com/evstack/ev-node/pkg/genesis" + "github.com/stretchr/testify/assert" +) + +func TestPrefillGenesisDaHeight(t *testing.T) { + ctx := context.Background() + logger := log.NewNopLogger() + + // Create test rollkit genesis at height 1 + rollkitGenesis := genesis.NewGenesis( + "test-chain", + 1, // Initial height 1 = at genesis + time.Now(), + []byte("test-sequencer-addr"), + ) + + tests := []struct { + name string + cfg *config.Config + migrationGenesis *evolveMigrationGenesis + rollkitGenesis *genesis.Genesis + expectSkip bool + expectReason string + }{ + { + name: "should skip when node is aggregator", + cfg: &config.Config{ + Node: config.NodeConfig{Aggregator: true}, + DA: config.DAConfig{StartHeight: 0}, + }, + rollkitGenesis: &rollkitGenesis, + expectSkip: true, + expectReason: "node is aggregator", + }, + { + name: "should skip when DA start height already set", + cfg: &config.Config{ + Node: config.NodeConfig{Aggregator: false}, + DA: config.DAConfig{StartHeight: 100}, + }, + rollkitGenesis: &rollkitGenesis, + expectSkip: true, + expectReason: "DA start height already set", + }, + { + name: "should skip when migration scenario", + cfg: &config.Config{ + Node: config.NodeConfig{Aggregator: false}, + DA: config.DAConfig{StartHeight: 0}, + }, + migrationGenesis: &evolveMigrationGenesis{ + ChainID: "test-chain", + InitialHeight: 1, + }, + rollkitGenesis: &rollkitGenesis, + expectSkip: true, + expectReason: "migration scenario", + }, + { + name: "should skip when not at genesis", + cfg: &config.Config{ + Node: config.NodeConfig{Aggregator: false}, + DA: config.DAConfig{StartHeight: 0}, + }, + rollkitGenesis: &genesis.Genesis{ + ChainID: "test-chain", + InitialHeight: 100, // Not at genesis + StartTime: time.Now(), + ProposerAddress: []byte("test-sequencer-addr"), + }, + expectSkip: true, + expectReason: "not at genesis", + }, + { + name: "should attempt prefill when conditions are met", + cfg: &config.Config{ + Node: config.NodeConfig{Aggregator: false}, + DA: config.DAConfig{StartHeight: 0}, + }, + rollkitGenesis: &rollkitGenesis, + expectSkip: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset config for each test + originalStartHeight := tt.cfg.DA.StartHeight + + // Call the prefill function + err := prefillGenesisDaHeight( + ctx, + tt.cfg, + nil, // DA client is nil, but we expect early returns for most tests + tt.rollkitGenesis, + tt.migrationGenesis, + logger, + ) + + if tt.expectSkip { + // For skip scenarios, the config should not be modified + assert.Equal(t, originalStartHeight, tt.cfg.DA.StartHeight, + "DA start height should not be modified when skipping") + + // Error should be nil for skip scenarios + assert.NoError(t, err, "should not return error for valid skip scenarios") + } else { + // For non-skip scenarios with nil DA client, we expect an error + // since getGenesisDaHeight will fail with "not yet implemented" + assert.Error(t, err, "should return error when GetGenesisDaHeight is not available") + assert.Contains(t, err.Error(), "not yet implemented", + "error should indicate GetGenesisDaHeight is not yet implemented") + + // Config should not be modified when RPC fails + assert.Equal(t, originalStartHeight, tt.cfg.DA.StartHeight, + "DA start height should not be modified when RPC fails") + } + }) + } +} + +func TestGetGenesisDaHeight(t *testing.T) { + ctx := context.Background() + + // Test that the function returns the expected error until the RPC is implemented + height, err := getGenesisDaHeight(ctx, nil) + + assert.Error(t, err, "should return error when GetGenesisDaHeight RPC is not available") + assert.Contains(t, err.Error(), "not yet implemented", + "error should indicate the method is not yet implemented") + assert.Equal(t, uint64(0), height, "height should be 0 when error occurs") +} \ No newline at end of file diff --git a/server/start.go b/server/start.go index e84beb01..f22950b3 100644 --- a/server/start.go +++ b/server/start.go @@ -453,6 +453,16 @@ func setupNodeAndExecutor( return nil, nil, cleanupFn, fmt.Errorf("failed to create DA client: %w", err) } + // Pre-fill genesis start DA height when possible + // This optimization fetches the first DA height from the node when: + // 1. The chain is at genesis (not a migration scenario and initial height is 1) + // 2. The genesis start DA height is unset (0) + // 3. The node is not an aggregator + if err := prefillGenesisDaHeight(ctx, &rollkitcfg, daClient, rollkitGenesis, migrationGenesis, sdkLogger); err != nil { + sdkLogger.Error("failed to prefill genesis DA height", "error", err) + // Don't fail startup for this optimization, just log the error + } + singleMetrics, err := single.NopMetrics() if err != nil { return nil, nil, cleanupFn, err @@ -715,3 +725,95 @@ func createEvolveGenesisFromCometBFT(cmtGenDoc *cmttypes.GenesisDoc) *genesis.Ge return &rollkitGenesis } + +// prefillGenesisDaHeight automatically sets the genesis start DA height when possible. +// This optimization fetches the first DA height from the node and sets it in the configuration +// when the chain is at genesis, the DA start height is unset, and the node is not an aggregator. +// +// Parameters: +// - ctx: Context for the operation +// - cfg: Rollkit configuration (will be modified in-place if conditions are met) +// - daClient: DA client to query for genesis DA height +// - rollkitGenesis: The rollkit genesis information +// - migrationGenesis: Migration genesis (nil if not a migration scenario) +// - logger: Logger for information and error messages +// +// This function will not cause startup to fail if it encounters errors; errors are logged +// and the startup continues with the original configuration. +func prefillGenesisDaHeight( + ctx context.Context, + cfg *config.Config, + daClient *jsonrpc.Client, + rollkitGenesis *genesis.Genesis, + migrationGenesis *evolveMigrationGenesis, + logger log.Logger, +) error { + // Only proceed if node is NOT an aggregator + if cfg.Node.Aggregator { + logger.Debug("skipping genesis DA height prefill: node is an aggregator") + return nil + } + + // Only proceed if DA start height is unset (0) + if cfg.DA.StartHeight != 0 { + logger.Debug("skipping genesis DA height prefill: DA start height already set", + "start_height", cfg.DA.StartHeight) + return nil + } + + // Only proceed if this is not a migration scenario and we're at genesis + if migrationGenesis != nil { + logger.Debug("skipping genesis DA height prefill: migration scenario detected") + return nil + } + + // Check if we're at genesis (initial height is 1) + if rollkitGenesis.InitialHeight != 1 { + logger.Debug("skipping genesis DA height prefill: not at genesis", + "initial_height", rollkitGenesis.InitialHeight) + return nil + } + + logger.Info("attempting to prefill genesis start DA height from node") + + // Call GetGenesisDaHeight RPC method + // Note: This method is expected to be available in the ev-node RPC interface + // as mentioned in the issue description + genesisDaHeight, err := getGenesisDaHeight(ctx, daClient) + if err != nil { + return fmt.Errorf("failed to get genesis DA height from node: %w", err) + } + + if genesisDaHeight == 0 { + logger.Warn("genesis DA height returned from node is 0, not updating configuration") + return nil + } + + logger.Info("successfully retrieved genesis DA height from node", + "genesis_da_height", genesisDaHeight) + + // Update the configuration + cfg.DA.StartHeight = genesisDaHeight + + logger.Info("genesis start DA height has been automatically set", + "start_height", genesisDaHeight) + + return nil +} + +// getGenesisDaHeight calls the GetGenesisDaHeight RPC method on the DA node. +// This is a wrapper function that handles the RPC call to retrieve the first +// DA height from the node, as mentioned in issue ev-node/pull/2614. +func getGenesisDaHeight(ctx context.Context, daClient *jsonrpc.Client) (uint64, error) { + // TODO: Once the GetGenesisDaHeight RPC method is available in the ev-node + // jsonrpc API (as mentioned in ev-node/pull/2614), this implementation should + // be updated to make the actual RPC call. + // + // The expected implementation would be: + // if methodExists(&daClient.DA, "GetGenesisDaHeight") { + // return daClient.DA.GetGenesisDaHeight(ctx) + // } + // + // For now, we return an informative error that doesn't break startup + return 0, fmt.Errorf("GetGenesisDaHeight RPC method is not yet implemented in ev-node jsonrpc API - see ev-node/pull/2614") +}