diff --git a/server/migration_cmd.go b/server/migration_cmd.go index 0b00329..89be76c 100644 --- a/server/migration_cmd.go +++ b/server/migration_cmd.go @@ -20,6 +20,7 @@ import ( "github.com/cometbft/cometbft/state" "github.com/cometbft/cometbft/store" cometbfttypes "github.com/cometbft/cometbft/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" ds "github.com/ipfs/go-datastore" ktds "github.com/ipfs/go-datastore/keytransform" "github.com/spf13/cobra" @@ -29,6 +30,8 @@ import ( rollkitstore "github.com/rollkit/rollkit/pkg/store" rollkittypes "github.com/rollkit/rollkit/types" + "github.com/rollkit/go-execution-abci/modules/rollkitmngr" + rollkitmngrTypes "github.com/rollkit/go-execution-abci/modules/rollkitmngr/types" "github.com/rollkit/go-execution-abci/pkg/adapter" ) @@ -370,14 +373,27 @@ func createRollkitMigrationGenesis(rootDir string, cometBFTState state.State) er sequencerPubKey crypto.PubKey ) - // use the first validator as sequencer (assuming single validator setup for migration) - if len(cometBFTState.LastValidators.Validators) == 1 { - sequencerAddr = cometBFTState.LastValidators.Validators[0].Address.Bytes() - sequencerPubKey = cometBFTState.LastValidators.Validators[0].PubKey + // If the cometbft validators are > 0, fetch the rollkitmngr state to get the sequencer + var rollkitMngrErr error + if len(cometBFTState.LastValidators.Validators) > 0 { + if sequencer, err := getSequencerFromRollkitMngrState(rootDir, cometBFTState); err == nil { + sequencerAddr = sequencer.Address + sequencerPubKey = sequencer.PubKey + } else { + rollkitMngrErr = err + // If rollkitmngr state is not available, fall back to single validator logic + if len(cometBFTState.LastValidators.Validators) == 1 { + sequencerAddr = cometBFTState.LastValidators.Validators[0].Address.Bytes() + sequencerPubKey = cometBFTState.LastValidators.Validators[0].PubKey + } else { + return errors.Join( + fmt.Errorf("expected exactly one validator in the last validators, found %d", len(cometBFTState.LastValidators.Validators)), + fmt.Errorf("unable to determine sequencer from rollkitmngr state: %w", err), + ) + } + } } else { - // TODO(@julienrbrt): Allow to use rollkitmngr state to get the sequencer address - // ref: https://github.com/rollkit/go-execution-abci/issues/164 - return fmt.Errorf("expected exactly one validator in the last validators, found %d", len(cometBFTState.LastValidators.Validators)) + return fmt.Errorf("no validators found in the last validators") } migrationGenesis := rollkitMigrationGenesis{ @@ -391,17 +407,113 @@ func createRollkitMigrationGenesis(rootDir string, cometBFTState state.State) er // using cmtjson for marshalling to ensure compatibility with cometbft genesis format genesisBytes, err := cmtjson.MarshalIndent(migrationGenesis, "", " ") if err != nil { + if rollkitMngrErr != nil { + return errors.Join( + fmt.Errorf("failed to marshal rollkit migration genesis: %w", err), + fmt.Errorf("rollkitmngr state query failed: %w", rollkitMngrErr), + ) + } return fmt.Errorf("failed to marshal rollkit migration genesis: %w", err) } + const rollkitGenesisFilename = "rollkit_genesis.json" genesisPath := filepath.Join(rootDir, rollkitGenesisFilename) if err := os.WriteFile(genesisPath, genesisBytes, 0o644); err != nil { + if rollkitMngrErr != nil { + return errors.Join( + fmt.Errorf("failed to write rollkit migration genesis to %s: %w", genesisPath, err), + fmt.Errorf("rollkitmngr state query failed: %w", rollkitMngrErr), + ) + } return fmt.Errorf("failed to write rollkit migration genesis to %s: %w", genesisPath, err) } return nil } +// sequencerInfo holds the sequencer information extracted from rollkitmngr state +type sequencerInfo struct { + Address []byte + PubKey crypto.PubKey +} + +// getSequencerFromRollkitMngrState attempts to load the sequencer information from the rollkitmngr module state +func getSequencerFromRollkitMngrState(rootDir string, cometBFTState state.State) (*sequencerInfo, error) { + // Try to load the application database + config := cfg.DefaultConfig() + config.SetRoot(rootDir) + + dbType := dbm.BackendType(config.DBBackend) + + // Check if application database exists + appDBPath := filepath.Join(config.DBDir(), "application.db") + if ok, err := fileExists(appDBPath); err != nil { + return nil, fmt.Errorf("error checking application database in %v: %w", config.DBDir(), err) + } else if !ok { + return nil, fmt.Errorf("no application database found in %v", config.DBDir()) + } + + // Open application database (read-only) + appDB, err := dbm.NewDB("application", dbType, config.DBDir()) + if err != nil { + return nil, fmt.Errorf("failed to open application database: %w", err) + } + defer appDB.Close() + + // Set up encoding config + encCfg := moduletestutil.MakeTestEncodingConfig(rollkitmngr.AppModuleBasic{}) + + // Read sequencer data from the database using the collections key format + // Collections store data with a prefix byte (0x00) followed by the key + sequencerKey := append([]byte{0}, rollkitmngrTypes.SequencerKey...) + sequencerBytes, err := appDB.Get(sequencerKey) + if err != nil { + return nil, fmt.Errorf("failed to read sequencer from database: %w", err) + } + if sequencerBytes == nil { + return nil, fmt.Errorf("sequencer not found in rollkitmngr state") + } + + // Decode the sequencer data + var sequencer rollkitmngrTypes.Sequencer + if err := encCfg.Codec.Unmarshal(sequencerBytes, &sequencer); err != nil { + return nil, fmt.Errorf("failed to decode sequencer from rollkitmngr state: %w", err) + } + + // Extract the public key from the sequencer + pubKeyAny := sequencer.ConsensusPubkey + if pubKeyAny == nil { + return nil, fmt.Errorf("sequencer consensus public key is nil") + } + + // Get the cached value which should be a crypto.PubKey + pubKey, ok := pubKeyAny.GetCachedValue().(crypto.PubKey) + if !ok { + return nil, fmt.Errorf("failed to extract public key from sequencer") + } + + // Get the address from the public key + addr := pubKey.Address() + + // Validate that this sequencer is actually one of the validators + validatorFound := false + for _, validator := range cometBFTState.LastValidators.Validators { + if bytes.Equal(validator.Address.Bytes(), addr) { + validatorFound = true + break + } + } + + if !validatorFound { + return nil, fmt.Errorf("sequencer from rollkitmngr state (address: %x) is not found in the validator set", addr) + } + + return &sequencerInfo{ + Address: addr, + PubKey: pubKey, + }, nil +} + // fileExists checks if a file/directory exists. func fileExists(filename string) (bool, error) { _, err := os.Stat(filename) diff --git a/server/migration_cmd_test.go b/server/migration_cmd_test.go new file mode 100644 index 0000000..f8e29eb --- /dev/null +++ b/server/migration_cmd_test.go @@ -0,0 +1,137 @@ +package server + +import ( + "testing" + + "github.com/cometbft/cometbft/state" + cometbfttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" +) + +func TestCreateRollkitMigrationGenesis_SingleValidator(t *testing.T) { + // Test case: single validator should work with existing logic + validator := &cometbfttypes.Validator{ + Address: []byte("test_validator_addr"), + PubKey: cometbfttypes.NewMockPV().PrivKey.PubKey(), + VotingPower: 100, // Set voting power to avoid the validation error + } + + cometBFTState := state.State{ + ChainID: "test-chain", + InitialHeight: 1, + LastValidators: cometbfttypes.NewValidatorSet([]*cometbfttypes.Validator{validator}), + } + + // Use a temporary directory for testing + tmpDir := t.TempDir() + + // This should succeed with the single validator fallback logic + err := createRollkitMigrationGenesis(tmpDir, cometBFTState) + require.NoError(t, err) +} + +func TestCreateRollkitMigrationGenesis_MultipleValidators_NoRollkitState(t *testing.T) { + // Test case: multiple validators without rollkitmngr state should fail gracefully + validator1 := &cometbfttypes.Validator{ + Address: []byte("test_validator_addr1"), + PubKey: cometbfttypes.NewMockPV().PrivKey.PubKey(), + VotingPower: 100, + } + validator2 := &cometbfttypes.Validator{ + Address: []byte("test_validator_addr2"), + PubKey: cometbfttypes.NewMockPV().PrivKey.PubKey(), + VotingPower: 100, + } + + cometBFTState := state.State{ + ChainID: "test-chain", + InitialHeight: 1, + LastValidators: cometbfttypes.NewValidatorSet([]*cometbfttypes.Validator{validator1, validator2}), + } + + // Use a temporary directory for testing (no rollkitmngr state present) + tmpDir := t.TempDir() + + // This should fail with our new logic when rollkitmngr state is not available + err := createRollkitMigrationGenesis(tmpDir, cometBFTState) + require.Error(t, err) + require.Contains(t, err.Error(), "expected exactly one validator") +} + +func TestCreateRollkitMigrationGenesis_NoValidators(t *testing.T) { + // Test case: no validators should return an error + cometBFTState := state.State{ + ChainID: "test-chain", + InitialHeight: 1, + LastValidators: cometbfttypes.NewValidatorSet([]*cometbfttypes.Validator{}), + } + + // Use a temporary directory for testing + tmpDir := t.TempDir() + + // This should fail + err := createRollkitMigrationGenesis(tmpDir, cometBFTState) + require.Error(t, err) + require.Contains(t, err.Error(), "no validators found") +} + +func TestGetSequencerFromRollkitMngrState_NoDatabase(t *testing.T) { + // Test case: should fail gracefully when no application database exists + tmpDir := t.TempDir() + + cometBFTState := state.State{ + ChainID: "test-chain", + } + + _, err := getSequencerFromRollkitMngrState(tmpDir, cometBFTState) + require.Error(t, err) + require.Contains(t, err.Error(), "no application database found") +} + +func TestSequencerInfo_Validation(t *testing.T) { + // Test the sequencer validation logic by creating a mock scenario + // where we have multiple validators and verify the error messages + + validator1 := &cometbfttypes.Validator{ + Address: []byte("test_validator_addr1"), + PubKey: cometbfttypes.NewMockPV().PrivKey.PubKey(), + VotingPower: 100, + } + validator2 := &cometbfttypes.Validator{ + Address: []byte("test_validator_addr2"), + PubKey: cometbfttypes.NewMockPV().PrivKey.PubKey(), + VotingPower: 100, + } + + cometBFTState := state.State{ + ChainID: "test-chain", + InitialHeight: 1, + LastValidators: cometbfttypes.NewValidatorSet([]*cometbfttypes.Validator{validator1, validator2}), + } + + tmpDir := t.TempDir() + + // This should fail since there's no rollkitmngr state, and provide a helpful error message + err := createRollkitMigrationGenesis(tmpDir, cometBFTState) + require.Error(t, err) + + // The error should mention both the validator count issue and the rollkitmngr state issue + require.Contains(t, err.Error(), "expected exactly one validator") + require.Contains(t, err.Error(), "found 2") + require.Contains(t, err.Error(), "unable to determine sequencer from rollkitmngr state") +} + +func TestMigrateToRollkitCmd_CommandSetup(t *testing.T) { + // Test that the command is properly configured + cmd := MigrateToRollkitCmd() + + require.NotNil(t, cmd) + require.Equal(t, "rollkit-migrate", cmd.Use) + require.Equal(t, "Migrate the data from the CometBFT chain to Rollkit", cmd.Short) + require.NotNil(t, cmd.RunE) + + // Check that the da-height flag is available + flag := cmd.Flags().Lookup("da-height") + require.NotNil(t, flag) + require.Equal(t, "1", flag.DefValue) +} \ No newline at end of file