Skip to content
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

[Morse->Shannon Migration] state export/import - collect accounts #1039

Open
wants to merge 12 commits into
base: scaffold/migration-module
Choose a base branch
from
Open
5,099 changes: 5,099 additions & 0 deletions api/poktroll/migration/legacy.pulsar.go

Large diffs are not rendered by default.

1,858 changes: 1,858 additions & 0 deletions api/poktroll/migration/types.pulsar.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions cmd/poktrolld/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/spf13/viper"

"github.com/pokt-network/poktroll/app"
"github.com/pokt-network/poktroll/cmd/poktrolld/cmd/migrate"
)

func initRootCmd(
Expand All @@ -52,6 +53,7 @@ func initRootCmd(
queryCommand(),
txCommand(),
keys.Commands(),
migrate.MigrateCmd(),
)
}

Expand Down
14 changes: 14 additions & 0 deletions cmd/poktrolld/cmd/migrate/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package migrate

import sdkerrors "cosmossdk.io/errors"

const codespace = "poktrolld/migrate"

var (
// ErrInvalidUsage usage is returned when the CLI arguments are invalid.
ErrInvalidUsage = sdkerrors.Register(codespace, 1100, "invalid usage")
// ErrMorseExportState is returned with the JSON generated from `pocket util export-genesis-for-reset` is invalid.
ErrMorseExportState = sdkerrors.Register(codespace, 1101, "morse export state")
// ErrMorseStateTransform is returned upon general failure when transforming the MorseExportState into the MorseAccountState.
ErrMorseStateTransform = sdkerrors.Register(codespace, 1102, "morse state transform")
)
275 changes: 275 additions & 0 deletions cmd/poktrolld/cmd/migrate/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package migrate
Copy link
Member

Choose a reason for hiding this comment

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

Given that this is all new code, let's use autocli instead.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Jan 30, 2025

Choose a reason for hiding this comment

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

AutoCLI does not apply here because there is no gRPC service, message, or query.

The purpose of this command is to facilitate the deterministic (i.e. reproducible) transformation from the Morse export data structure (MorseStateExport) into the Shannon import data structure (MorseAccountState). It does not interact with the network directly.


import (
"fmt"
"io"
"os"

cosmosmath "cosmossdk.io/math"
cmtjson "github.com/cometbft/cometbft/libs/json"
"github.com/spf13/cobra"

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/pkg/polylog"
"github.com/pokt-network/poktroll/pkg/polylog/polyzero"
migrationtypes "github.com/pokt-network/poktroll/x/migration/types"
)

var (
flagDebugAccountsPerLog int
flagLogLevel string
flagLogOutput string
logger polylog.Logger

collectMorseAccountsCmd = &cobra.Command{
Use: "collect-morse-accounts [morse-state-export-path] [morse-account-state-path]",
Args: cobra.ExactArgs(2),
Short: "Collect all account balances and corresponding stakes from the JSON file at [morse-state-export-path] and outputs them as JSON to [morse-account-state-path]",
Long: `Collects the account balances and corresponding stakes from the MorseStateExport JSON file at morse-state-path
and outputs them as a MorseAccountState JSON to morse-accounts-path for use with
Shannon's MsgUploadMorseState. The Morse state export is generated via the Morse CLI:
pocket util export-genesis-for-reset [height] [new-chain-id] > morse-state-export.json`,
PreRunE: func(cmd *cobra.Command, args []string) error {
var (
logOutput io.Writer
err error
)
logLevel := polyzero.ParseLevel(flagLogLevel)
if flagLogOutput == "-" {
logOutput = os.Stderr
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be os.Stdout as per the flag description?

migrateCmd.PersistentFlags().StringVar(&flagLogOutput, "log-output", "-", "The logging output (file path); defaults to stdout")

} else {
logOutput, err = os.Open(flagLogOutput)
if err != nil {
return err
}
}

logger = polyzero.NewLogger(
polyzero.WithLevel(logLevel),
polyzero.WithOutput(logOutput),
).With("cmd", "migrate")
return nil
},
RunE: runCollectMorseAccounts,
}
)

func MigrateCmd() *cobra.Command {
migrateCmd := &cobra.Command{
Use: "migrate",
Short: "Migration commands",
}
migrateCmd.AddCommand(collectMorseAccountsCmd)
migrateCmd.PersistentFlags().StringVar(&flagLogLevel, "log-level", "info", "The logging level (debug|info|warn|error)")
migrateCmd.PersistentFlags().StringVar(&flagLogOutput, "log-output", "-", "The logging output (file path); defaults to stdout")

collectMorseAccountsCmd.Flags().IntVar(&flagDebugAccountsPerLog, "debug-accounts-per-log", 0, "The number of accounts to log per debug message")

return migrateCmd
}

// runCollectedMorseAccounts is run by the `poktrolld migrate collect-morse-accounts` command.
func runCollectMorseAccounts(_ *cobra.Command, args []string) error {
// DEV_NOTE: No need to check args length due to cobra.ExactArgs(2).
morseStateExportPath := args[0]
morseAccountStatePath := args[1]

logger.Info().
Str("morse_state_export_path", morseStateExportPath).
Str("morse_account_state_path", morseAccountStatePath).
Msg("collecting Morse accounts...")

morseWorkspace, err := collectMorseAccounts(morseStateExportPath, morseAccountStatePath)
if err != nil {
return err
}

return morseWorkspace.infoLogComplete()
}

// collectMorseAccounts reads and transforms the JSON serialized MorseStateExport
// at morseStateExportPath into a JSON serialized MorseAccountState, and then writes
// it to morseAccountStatePath.
func collectMorseAccounts(morseStateExportPath, morseAccountStatePath string) (*morseImportWorkspace, error) {
if err := validatePathIsFile(morseStateExportPath); err != nil {
return nil, err
}

inputStateJSON, err := os.ReadFile(morseStateExportPath)
if err != nil {
return nil, err
}

inputState := new(migrationtypes.MorseStateExport)
if err = cmtjson.Unmarshal(inputStateJSON, inputState); err != nil {
return nil, err
}

morseWorkspace := newMorseImportWorkspace()
if err = transformMorseState(inputState, morseWorkspace); err != nil {
return nil, err
}

outputStateJSONBz, err := cmtjson.Marshal(morseWorkspace.accountState)
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 👍

if err != nil {
return nil, err
}

if err = os.WriteFile(morseAccountStatePath, outputStateJSONBz, 0644); err != nil {
return nil, err
}

return morseWorkspace, nil
}

// validatePathIsFile returns an error if the given path does not exist or is not a file.
func validatePathIsFile(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}

if info.IsDir() {
return ErrInvalidUsage.Wrapf("[morse-JSON-input-path] cannot be a directory: %s", path)
}
return nil
}

// transformMorseState consolidates the Morse account balance, application stake,
// and supplier stake for each account as an entry in the resulting MorseAccountState.
Copy link
Member

Choose a reason for hiding this comment

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

How about validator stake?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that "supplier stake" is identical to "validator stake" in Morse terms. I intentionally chose Shannon terms for all the comments for consistency.

func transformMorseState(
inputState *migrationtypes.MorseStateExport,
morseWorkspace *morseImportWorkspace,
) error {
// Iterate over accounts and copy the balances.
logger.Info().Msg("collecting account balances...")
if err := collectInputAccountBalances(inputState, morseWorkspace); err != nil {
return err
}

// Iterate over applications and add the stakes to the corresponding account balances.
logger.Info().Msg("collecting application stakes...")
if err := collectInputApplicationStakes(inputState, morseWorkspace); err != nil {
return err
}

// Iterate over suppliers and add the stakes to the corresponding account balances.
logger.Info().Msg("collecting supplier stakes...")
err := collectInputSupplierStakes(inputState, morseWorkspace)
if err != nil {
return err
}

morseWorkspace.accountState = &migrationtypes.MorseAccountState{Accounts: morseWorkspace.accounts}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you #PUC why this is needed?

return nil
}

// collectInputAccountBalances iterates over the accounts in the inputState and
// adds the balances to the corresponding account balances in the morseWorkspace.
func collectInputAccountBalances(inputState *migrationtypes.MorseStateExport, morseWorkspace *morseImportWorkspace) error {
for exportAccountIdx, exportAccount := range inputState.AppState.Auth.Accounts {
if shouldDebugLogProgress(exportAccountIdx) {
morseWorkspace.debugLogProgress(exportAccountIdx)
}

// DEV_NOTE: Ignore module accounts.
if exportAccount.Type != "posmint/Account" {
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Jan 30, 2025

Choose a reason for hiding this comment

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

Why is this a module account?

The question isn't quite clear to me. The Morse data structure BaseAccount is used in Morse, but is seralized as a pb.Any type. This is the reason for the MorseAuthAccount type, which includes the type field (to avoid having to deal with this additional and unnecessary complexity). Also note that the module account data structure is different, hence the use of pb.Any.

With respect to the morse state export / account state import, my understanding is that we're only interested in externally owned accounts. Do you see a reason to migrate module accounts as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you're asking how I determined that a type value of posmint/Account is indicative of an EOA (or the inverse), was by looking at my local ~/.pocket/config/genesis.json after doing ./pocket accounts create and ./pocket accounts set-validator <acct. addr>. You'll see that the posmint/Account type is associated with the created account.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you see a reason to migrate module accounts as well?

Not sure if they should be migrated but they might (@Olshansk ?) contain funds.

This makes me think we should validate the total supply returned by Morse vs. the supply we get by summing all accounts balances in the resulting morseImportWorkspace.

Or, are we OK not having the same total supply after the migration?

logger.Warn().
Str("type", exportAccount.Type).
Str("address", exportAccount.Value.Address.String()).
Str("coins", fmt.Sprintf("%s", exportAccount.Value.Coins)).
Msg("ignoring non-EOA account")
continue
}

accountAddr := exportAccount.Value.Address.String()
if _, _, err := morseWorkspace.ensureAccount(accountAddr, exportAccount); err != nil {
return err
}

coins := exportAccount.Value.Coins
if len(coins) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

This case is already handled by the if len(coins) != 1 below.

We shouldn't return nil anyway.

return nil
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
}

// DEV_NOTE: SHOULD ONLY be one denom (upokt).
if len(coins) != 1 {
return ErrMorseExportState.Wrapf(
"account %q has %d token denominations, expected upokt only: %s",
accountAddr, len(coins), coins,
)
}

coin := coins[0]
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
if coin.Denom != volatile.DenomuPOKT {
return ErrMorseExportState.Wrapf("unsupported denom %q", coin.Denom)
}

if err := morseWorkspace.addUpokt(accountAddr, coin.Amount); err != nil {
return fmt.Errorf(
"adding morse account balance (%s) to account balance of address %q: %w",
coin, accountAddr, err,
)
}
}
return nil
}

// shouldDebugLogProgress returns true if the given exportAccountIdx should be logged
// via debugLogProgress.
func shouldDebugLogProgress(exportAccountIdx int) bool {
return flagDebugAccountsPerLog > 0 &&
exportAccountIdx%flagDebugAccountsPerLog == 0
}

// collectInputApplicationStakes iterates over the applications in the inputState and
// adds the stake to the corresponding account balances in the morseWorkspace.
func collectInputApplicationStakes(inputState *migrationtypes.MorseStateExport, morseWorkspace *morseImportWorkspace) error {
for _, exportApplication := range inputState.AppState.Application.Applications {
appAddr := exportApplication.Address.String()

// DEV_NOTE: An account SHOULD exist for each actor.
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
if !morseWorkspace.hasAccount(appAddr) {
return ErrMorseExportState.Wrapf("account not found corresponding to application with address %q", appAddr)
}

appStakeAmtUpokt, ok := cosmosmath.NewIntFromString(exportApplication.StakedTokens)
if !ok {
return ErrMorseExportState.Wrapf("failed to parse application stake amount %q", exportApplication.StakedTokens)
}

if err := morseWorkspace.addUpokt(appAddr, appStakeAmtUpokt); err != nil {
return fmt.Errorf(
"adding application stake amount to account balance of address %q: %w",
appAddr, err,
)
}
}
return nil
}

// collectInputSupplierStakes iterates over the suppliers in the inputState and
// adds the stake to the corresponding account balances in the morseWorkspace.
func collectInputSupplierStakes(inputState *migrationtypes.MorseStateExport, morseWorkspace *morseImportWorkspace) error {
for _, exportSupplier := range inputState.AppState.Pos.Validators {
Copy link
Member

Choose a reason for hiding this comment

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

ShannonSuppliers == MorseNode (aka Morse Servicer)
ShannonValidator == MorseValidator (aka Morse Full Node)

Only top 1000 of staked validators are ACTUAL validators in Morse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding was that Morse supplier/node/servicer actors are all tendermint validators, and that tendermint uses sortition over the all validators to determine the active/voting set. I would have to look deeper into how Morse handles supplier staking to confirm/deny. According to tendermint v0.34 docs, the only ways for validators to be added are via genesis or an EndBlock message.

supplierAddr := exportSupplier.Address.String()

// DEV_NOTE: An account SHOULD exist for each actor.
if !morseWorkspace.hasAccount(supplierAddr) {
return ErrMorseExportState.Wrapf("account not found corresponding to supplier with address %q", supplierAddr)
}

supplierStakeAmtUpokt, ok := cosmosmath.NewIntFromString(exportSupplier.StakedTokens)
if !ok {
return ErrMorseExportState.Wrapf("failed to parse supplier stake amount %q", exportSupplier.StakedTokens)
}

if err := morseWorkspace.addUpokt(supplierAddr, supplierStakeAmtUpokt); err != nil {
return fmt.Errorf(
"adding supplier stake amount to account balance of address %q: %w",
supplierAddr, err,
)
}
}
return nil
}
Loading