Skip to content

feat: Allow fetching rollkitmngr state in rollkit-migrate command for attester sets #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
126 changes: 119 additions & 7 deletions server/migration_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

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

can we error joins those errors? as it will be lost otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Now using errors.Join to properly combine the validator count error with the rollkitmngr state error, preserving the error chain for better debugging. Commit 229d533

Copy link
Member

Choose a reason for hiding this comment

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

this isn't what i mean. Error from L378 is discarded, we should still return it if we end up with errors later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Now preserving the rollkitmngr state error in a variable and including it in the error chain only when subsequent operations fail, rather than discarding it entirely. Commit 89be76c

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{
Expand All @@ -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)
Expand Down
137 changes: 137 additions & 0 deletions server/migration_cmd_test.go
Original file line number Diff line number Diff line change
@@ -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)
}