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 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
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,103 changes: 5,103 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 @@
"github.com/spf13/viper"

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

Check failure on line 29 in cmd/poktrolld/cmd/commands.go

View workflow job for this annotation

GitHub Actions / go-test

could not import github.com/pokt-network/poktroll/cmd/poktrolld/cmd/migrate (-: # github.com/pokt-network/poktroll/cmd/poktrolld/cmd/migrate
)

func initRootCmd(
Expand All @@ -52,6 +53,7 @@
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")
)
285 changes: 285 additions & 0 deletions cmd/poktrolld/cmd/migrate/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
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

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-path
* Contains account balances and associated stakes
* Outputs MorseAccountState JSON to morse-accounts-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 {

Check failure on line 39 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

syntax error: unexpected :, expected type

Check failure on line 39 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

syntax error: unexpected :, expected type

Check failure on line 39 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

expected type, found ':' (typecheck)
var (

Check failure on line 40 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

expected 'IDENT', found 'var' (typecheck)
logOutput io.Writer

Check failure on line 41 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

expected ')', found io (typecheck)
err error
)
logLevel := polyzero.ParseLevel(flagLogLevel)

Check failure on line 44 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

syntax error: non-declaration statement outside function body) (typecheck)

Check failure on line 44 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

syntax error: non-declaration statement outside function body (typecheck)
if flagLogOutput == defaultLogOutput {

Check failure on line 45 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

expected 'IDENT', found 'if' (typecheck)
logOutput = os.Stdout
} else {
logOutput, err = os.Open(flagLogOutput)
if err != nil {

Check failure on line 49 in cmd/poktrolld/cmd/migrate/migrate.go

View workflow job for this annotation

GitHub Actions / go-test

expected 'IDENT', found 'if' (typecheck)
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 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 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
}
Copy link
Member

Choose a reason for hiding this comment

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

Very easy to follow, ty


// 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...")
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 {
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?

Copy link
Member

Choose a reason for hiding this comment

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

@bryanchriswhite - ACK. No followups.


@red-0ne - they shouldn't. In a followup PR, when we claim as a staked address, it'll be added in both places (i.e. like assets/liabilities on a balance sheet).


Can you add a TODO_MAINNET somewhere in this PR w/ the description of considering a validation against poket query supply.

poket query supply --remoteCLIURL https://pocket-rpc.liquify.com
2025/02/06 11:30:06 Initializing Pocket Datadir
2025/02/06 11:30:06 datadir = /Users/olshansky/.pocket
https://pocket-rpc.liquify.com/v1/query/supply
{
    "app_staked": "5057233331383",
    "dao": "362593211187825",
    "node_staked": "673419820181557",
    "total": "2236016326285505",
    "total_staked": "1041070264700765",
    "total_unstaked": "1194946061584740"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I neglected to comment on this thread, mostly because I wanted to see what @Olshansk thought first.

My perspective, at this point, is that we're likely going to encounter (and have to resolve) several edge cases if we want a high-precision supply-based validation. My intuition is that this will not be as simple as it may initially seem; at the very least, I feel that there's a decent amount of uncertainty involved. My concern is that it becomes increasingly difficult to get a high-precision match as we track down ever smaller, and more illusive, and unexpected contributing sources to supply.

  1. Is validation of supply useful if it is not high-precision? I.e., would we be willing to consider some error tolerance, or does that defeat the purpose?

    I don't see a technical reason why this would be the case, but if seems to me that the intention behind this kind of validation is more about inspiring confidence in network participants. I feel like this confidence could easily become undermined if it comes with an error tolerance baked in. It seems to me like we end up shining a light on something that could be perceived as an issue but should not be, in an attempt to ease users.

  2. What is the actual goal and are we able to (or already) accomplish(ing) it via other, simpler means?

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, 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,
)
}
}
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,
)
}

morseWorkspace.lastAccTotalAppStake = morseWorkspace.lastAccTotalAppStake.Add(appStakeAmtUpokt)
}
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.

Copy link
Member

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

Correct.

. According to tendermint v0.34 docs, the only ways for validators to be added are via genesis or an EndBlock message.

See pocket nodes stake...

Top 1000 nodes by stake are validators.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Feb 7, 2025

Choose a reason for hiding this comment

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

It's not clear to me whether we're just trying to align on terminology or if there's a specific ask or clarification intended here. 🤔

I looked at pocket nodes stakeNew and I did not see the connection between the pokt node/validator and the tendermint validator.

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.lastAccTotalSupplierStake = morseWorkspace.lastAccTotalSupplierStake.Add(supplierStakeAmtUpokt)
}
return nil
}
Loading
Loading