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

Merged
merged 43 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1231e6f
scaffold: module migration
bryanchriswhite Jan 17, 2025
5235822
fix: linter error
bryanchriswhite Jan 20, 2025
6d9c3d9
fix: stable marshaler options
bryanchriswhite Jan 20, 2025
b225150
fix: linter errors
bryanchriswhite Jan 21, 2025
03b76e1
Merge branch 'main' into scaffold/migration-module
bryanchriswhite Jan 27, 2025
85992e2
feat: add migrate collect-morse-account subcommand
bryanchriswhite Jan 23, 2025
890d5b5
refactor: test & add benchmark
bryanchriswhite Jan 27, 2025
95e1edf
fix: linter errors
bryanchriswhite Jan 27, 2025
77bc6a5
fix: add missing files
bryanchriswhite Jan 27, 2025
ef4c985
Merge branch 'main' into scaffold/migration-module
Olshansk Jan 29, 2025
08a1dd2
refactor: migration keeper in cosmos app
bryanchriswhite Jan 30, 2025
a63ea9b
chore: review feedback improvements
bryanchriswhite Jan 30, 2025
0d8505d
Merge branch 'scaffold/migration-module' into chore/migration/state-prep
bryanchriswhite Jan 30, 2025
46c87be
refactor: morse types
bryanchriswhite Jan 30, 2025
d4f4a80
Merge branch 'main' into scaffold/migration-module
bryanchriswhite Jan 31, 2025
178ded8
chore: review feedback improvements
bryanchriswhite Jan 31, 2025
c2cca35
fix: linter error
bryanchriswhite Jan 31, 2025
54fabd9
chore: review feedback improvements
bryanchriswhite Jan 31, 2025
9bf52b2
Merge branch 'main' into scaffold/migration-module
bryanchriswhite Feb 3, 2025
aa2859b
Merge branch 'scaffold/migration-module' into chore/migration/state-prep
bryanchriswhite Feb 3, 2025
c7d725a
chore: review improvements
bryanchriswhite Feb 4, 2025
794b768
Merge branch 'scaffold/migration-module' into chore/migration/state-prep
bryanchriswhite Feb 4, 2025
5d473f0
Add a couple TODO_UPNEXT
Olshansk Feb 4, 2025
165cf40
Merge branch 'main' into scaffold/migration-module
bryanchriswhite Feb 5, 2025
6ad5099
Merge branch 'scaffold/migration-module' into chore/migration/state-prep
bryanchriswhite Feb 5, 2025
7b25a9a
chore: review feedback improvements
bryanchriswhite Feb 6, 2025
9ddea8a
Merge branch 'main' into chore/migration/state-prep
bryanchriswhite Feb 6, 2025
6f4e992
chore: review improvements
bryanchriswhite Feb 6, 2025
63c0790
fix: linter error
bryanchriswhite Feb 6, 2025
efb6d49
chore: review feedback improvements
bryanchriswhite Feb 7, 2025
945c915
chore: review feedback
bryanchriswhite Feb 10, 2025
6792827
Merge remote-tracking branch 'pokt/main' into chore/migration/state-prep
bryanchriswhite Feb 10, 2025
e6b0a6e
fix: typo
bryanchriswhite Feb 10, 2025
1bf7a3b
fix: linter error
bryanchriswhite Feb 10, 2025
4a8f011
chore: review feedback improvements
bryanchriswhite Feb 10, 2025
76f1775
fix: off-by-one
bryanchriswhite Feb 10, 2025
d3c8b76
Add TODO
Olshansk Feb 10, 2025
5d5fdb1
Review + TODO_IN_THIS_PR
Olshansk Feb 10, 2025
fdbf8bd
Live convo with bryan
Olshansk Feb 11, 2025
6347a04
Merge branch 'main' into chore/migration/state-prep
Olshansk Feb 11, 2025
194fbc8
Cleanup TODOs
Olshansk Feb 11, 2025
ac97ff4
Empty commit
Olshansk Feb 11, 2025
7f9e980
chore: review feedback improvements
bryanchriswhite Feb 12, 2025
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
5,129 changes: 5,129 additions & 0 deletions api/poktroll/migration/morse_offchain.pulsar.go

Large diffs are not rendered by default.

2,178 changes: 2,178 additions & 0 deletions api/poktroll/migration/morse_onchain.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
16 changes: 16 additions & 0 deletions cmd/poktrolld/cmd/migrate/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 CLI 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 failed")

// ErrMorseStateTransform is returned upon general failure when transforming the MorseExportState into the MorseAccountState.
ErrMorseStateTransform = sdkerrors.Register(codespace, 1102, "morse export to state transformation invalid")
)
327 changes: 327 additions & 0 deletions cmd/poktrolld/cmd/migrate/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
package migrate
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved

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"
)

const defaultLogOutput = "-"

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

// DEV_NOTE: 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 Morse's export data structure (MorseStateExport) into Shannon's import data structure (MorseAccountState).
//
// It does not interact with the network directly.
collectMorseAccountsCmd = &cobra.Command{
Use: "collect-morse-accounts [morse-state-export-path] [morse-account-state-path]",
Args: cobra.ExactArgs(2),
Short: "Collect account balances and stakes from [morse-state-export-path] JSON file and output to [morse-account-state-path] as JSON",
Long: `Processes Morse state for Shannon migration:
* Reads MorseStateExport JSON from [morse-state-export-path]
* Contains account balances and associated stakes
* Outputs MorseAccountState JSON to [morse-account-state-path]
* Integrates with Shannon's MsgUploadMorseState

Generate required input via 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 == defaultLogOutput {
logOutput = os.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", defaultLogOutput, "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 via the following command:
// $ poktrolld migrate collect-morse-accounts
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 a MorseStateExport JSON file from morseStateExportPath
// - Transforms it into a MorseAccountState
// - Writes the resulting JSON 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)
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

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

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

// 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.
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
// NOTE: In Shannon terms, a "supplier" is equivalent to all of the following in Morse terms:
// - "validator"
// - "node"
// - "servicer"
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...")
return collectInputSupplierStakes(inputState, morseWorkspace)
}

// 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 {
// DEV_NOTE: Ignore module accounts.
// TODO_IN_THIS_PR: Create a GitHub issue based on this thread: https://github.com/pokt-network/poktroll/pull/1039/files#r1934711993
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
if exportAccount.Type != "posmint/Account" {
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
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.addAccount(accountAddr, exportAccount); err != nil {
return err
}

coins := exportAccount.Value.Coins

// If, for whatever reason, the account has no coins, skip it.
// DEV_NOTE: This is NEVER expected to happen, but is technically possible.
if len(coins) == 0 {
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
logger.Warn().Str("address", accountAddr).Msg("account has no coins; skipping")
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,
)
}

morseWorkspace.accumulatedTotalBalance = morseWorkspace.accumulatedTotalBalance.Add(coin.Amount)

if shouldDebugLogProgress(exportAccountIdx) {
logger.Debug().
Int("account_idx", exportAccountIdx).
Uint64("num_accounts", morseWorkspace.getNumAccounts()).
Str("total_balance", morseWorkspace.accumulatedTotalBalance.String()).
Str("grand_total", morseWorkspace.accumulatedTotalsSum().String()).
Msg("processing account balances...")
}
}
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 exportApplicationIdx, 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,
)
}

morseWorkspace.accumulatedTotalAppStake = morseWorkspace.accumulatedTotalAppStake.Add(appStakeAmtUpokt)
morseWorkspace.numApplications++

if shouldDebugLogProgress(exportApplicationIdx) {
logger.Debug().
Int("application_idx", exportApplicationIdx).
Uint64("num_accounts", morseWorkspace.getNumAccounts()).
Uint64("num_applications", morseWorkspace.numApplications).
Str("total_app_stake", morseWorkspace.accumulatedTotalAppStake.String()).
Str("grand_total", morseWorkspace.accumulatedTotalsSum().String()).
Msg("processing application stakes...")
}
}
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 exportSupplierIdx, exportSupplier := range inputState.AppState.Pos.Validators {
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,
)
}

morseWorkspace.accumulatedTotalSupplierStake = morseWorkspace.accumulatedTotalSupplierStake.Add(supplierStakeAmtUpokt)
morseWorkspace.numSuppliers++

if shouldDebugLogProgress(exportSupplierIdx) {
logger.Debug().
Int("supplier_idx", exportSupplierIdx).
Uint64("num_accounts", morseWorkspace.getNumAccounts()).
Uint64("num_suppliers", morseWorkspace.numSuppliers).
Str("total_supplier_stake", morseWorkspace.accumulatedTotalSupplierStake.String()).
Str("grand_total", morseWorkspace.accumulatedTotalsSum().String()).
Msg("processing accounts...")
}
}
return nil
}
Loading