-
Notifications
You must be signed in to change notification settings - Fork 13
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
base: scaffold/migration-module
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,191 @@ | ||||||
package migrate | ||||||
|
||||||
import ( | ||||||
"fmt" | ||||||
"os" | ||||||
|
||||||
cosmosmath "cosmossdk.io/math" | ||||||
cmtjson "github.com/cometbft/cometbft/libs/json" | ||||||
"github.com/spf13/cobra" | ||||||
|
||||||
"github.com/pokt-network/poktroll/app/volatile" | ||||||
migrationtypes "github.com/pokt-network/poktroll/x/migration/types" | ||||||
) | ||||||
|
||||||
var collectMorseAccountsCmd = &cobra.Command{ | ||||||
Use: "collect-morse-accounts [morse-state-path] [morse-accounts-path]", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
See: #1032 (comment) |
||||||
Args: cobra.ExactArgs(2), | ||||||
Short: "Collect all account balances and corresponding stakes from the JSON file at [morse-state-path] and outputs them as JSON to [morse-accounts-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`, | ||||||
RunE: runCollectMorseAccounts, | ||||||
} | ||||||
|
||||||
func MigrateCmd() *cobra.Command { | ||||||
cmd := &cobra.Command{ | ||||||
Use: "migrate", | ||||||
Short: "Migration commands", | ||||||
} | ||||||
cmd.AddCommand(collectMorseAccountsCmd) | ||||||
|
||||||
return cmd | ||||||
} | ||||||
|
||||||
// runCollectedMorseAccounts is run by the `poktrolld migrate collect-morse-accounts` command. | ||||||
func runCollectMorseAccounts(cmd *cobra.Command, args []string) error { | ||||||
inputPath := args[0] | ||||||
outputPath := args[1] | ||||||
|
||||||
return collectMorseAccounts(inputPath, outputPath) | ||||||
} | ||||||
|
||||||
// collectMorseAccounts transforms the JSON serialized MorseStateExport at | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any chance you can add an "integration" test (using that word lightly) of sorts so we even have examples (input & output) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been avoiding E2E tests for the time being. If we wanted to integrate any more than this, we would require a running and synced Morse node (local or remote). I didn't see any value in pursuing this given the coverage that the current approach provides. Additionally, there are integration-app level tests in #1047 which exercise the |
||||||
// inputStatePath into a JSON serialized MorseAccountState at outputStatePath. | ||||||
func collectMorseAccounts(inputStatePath, outputStatePath string) error { | ||||||
if err := validatePathIsFile(inputStatePath); err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
inputStateJSON, err := os.ReadFile(inputStatePath) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #PUC w/ a small one comment one-liner above each section. |
||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
inputState := new(migrationtypes.MorseStateExport) | ||||||
if err = cmtjson.Unmarshal(inputStateJSON, inputState); err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
outputStateJSON, err := transformMorseState(inputState) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
if err = os.WriteFile(outputStatePath, outputStateJSON, 0644); err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
return 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 fmt.Errorf("[morse-JSON-input-path] cannot be a directory") | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
// transformMorseState consolidates the Morse account balance, application stake, | ||||||
// and supplier stake for each account as an entry in the resulting MorseAccountState. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about validator stake? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) ([]byte, error) { | ||||||
morseWorkspace := &morseImportWorkspace{ | ||||||
addressToIdx: make(map[string]uint64), | ||||||
accounts: make([]*migrationtypes.MorseAccount, 0), | ||||||
} | ||||||
|
||||||
// Iterate over accounts and copy the balances. | ||||||
if err := collectInputAccountBalances(inputState, morseWorkspace); err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
// Iterate over applications and add the stakes to the corresponding account balances. | ||||||
if err := collectInputApplicationStakes(inputState, morseWorkspace); err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
// Iterate over suppliers and add the stakes to the corresponding account balances. | ||||||
err := collectInputSupplierStakes(inputState, morseWorkspace) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
morseAccountState := &migrationtypes.MorseAccountState{Accounts: morseWorkspace.accounts} | ||||||
return cmtjson.Marshal(morseAccountState) | ||||||
} | ||||||
|
||||||
// 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 _, exportAccount := range inputState.AppState.Auth.Accounts { | ||||||
// DEV_NOTE: Ignore module accounts. | ||||||
if exportAccount.Type != "posmint/Account" { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a module account? I did a grep and found my way here: https://github.com/pokt-network/pocket-core/blob/35c05eb89ec30b50239b24730d0aeab0002cfde2/x/auth/types/codec.go#L16 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The question isn't quite clear to me. The Morse data structure 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? |
||||||
continue | ||||||
} | ||||||
|
||||||
addr := exportAccount.Value.Address.String() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/addr/morseAddr |
||||||
morseWorkspace.ensureAccount(addr, exportAccount) | ||||||
|
||||||
coins := exportAccount.Value.Coins | ||||||
if len(coins) == 0 { | ||||||
return nil | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log on this error. Should it ever actually happen? |
||||||
} | ||||||
|
||||||
// DEV_NOTE: SHOULD ONLY be one denom (upokt). | ||||||
coin := coins[0] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you assert that it's exactly one? |
||||||
if coin.Denom != volatile.DenomuPOKT { | ||||||
return fmt.Errorf("unsupported denom %q", coin.Denom) | ||||||
} | ||||||
|
||||||
if err := morseWorkspace.addUpokt(addr, coin.Amount); err != nil { | ||||||
return err | ||||||
} | ||||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
// 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 { | ||||||
addr := exportApplication.Address.String() | ||||||
|
||||||
// DEV_NOTE: An account SHOULD exist for each actor. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log if not |
||||||
if !morseWorkspace.hasAccount(addr) { | ||||||
// TODO_IN_THIS_COMMIT: consolidate error types... | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's do it |
||||||
return fmt.Errorf("account %q not found", addr) | ||||||
} | ||||||
|
||||||
appStakeAmtUpokt, ok := cosmosmath.NewIntFromString(exportApplication.StakedTokens) | ||||||
if !ok { | ||||||
return fmt.Errorf("failed to parse application stake amount %q", exportApplication.StakedTokens) | ||||||
} | ||||||
|
||||||
if err := morseWorkspace.addUpokt(addr, appStakeAmtUpokt); err != nil { | ||||||
return 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ShannonSuppliers == MorseNode (aka Morse Servicer) Only top 1000 of staked validators are ACTUAL validators in Morse There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
addr := exportSupplier.Address.String() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
// DEV_NOTE: An account SHOULD exist for each actor. | ||||||
if !morseWorkspace.hasAccount(addr) { | ||||||
// TODO_IN_THIS_COMMIT: consolidate error types... | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
return fmt.Errorf("account %q not found", addr) | ||||||
} | ||||||
|
||||||
supplierStakeAmtUpokt, ok := cosmosmath.NewIntFromString(exportSupplier.StakedTokens) | ||||||
if !ok { | ||||||
return fmt.Errorf("failed to parse supplier stake amount %q", exportSupplier.StakedTokens) | ||||||
} | ||||||
|
||||||
if err := morseWorkspace.addUpokt(addr, supplierStakeAmtUpokt); err != nil { | ||||||
return err | ||||||
} | ||||||
} | ||||||
return nil | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package migrate | ||
|
||
import ( | ||
"encoding/binary" | ||
"fmt" | ||
"math" | ||
"math/rand" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
cometcrypto "github.com/cometbft/cometbft/crypto/ed25519" | ||
cmtjson "github.com/cometbft/cometbft/libs/json" | ||
cosmostypes "github.com/cosmos/cosmos-sdk/types" | ||
"github.com/regen-network/gocuke" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/pokt-network/poktroll/app/volatile" | ||
migrationtypes "github.com/pokt-network/poktroll/x/migration/types" | ||
) | ||
|
||
func TestCollectMorseAccounts(t *testing.T) { | ||
tmpDir := t.TempDir() | ||
outputPath := filepath.Join(tmpDir, "morse-state-output.json") | ||
inputFile, err := os.CreateTemp(tmpDir, "morse-state-input.json") | ||
require.NoError(t, err) | ||
|
||
morseStateExportBz, morseAccountStateBz := newMorseStateExportAndAccountState(t, 10) | ||
_, err = inputFile.Write(morseStateExportBz) | ||
require.NoError(t, err) | ||
|
||
err = inputFile.Close() | ||
require.NoError(t, err) | ||
|
||
// Call the function under test. | ||
err = collectMorseAccounts(inputFile.Name(), outputPath) | ||
require.NoError(t, err) | ||
|
||
outputJSON, err := os.ReadFile(outputPath) | ||
require.NoError(t, err) | ||
|
||
expectedJSON := string(morseAccountStateBz) | ||
require.NoError(t, err) | ||
|
||
// Strip all whitespace from the expected JSON. | ||
expectedJSON = strings.ReplaceAll(expectedJSON, "\n", "") | ||
expectedJSON = strings.ReplaceAll(expectedJSON, " ", "") | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, expectedJSON, string(outputJSON)) | ||
} | ||
|
||
func TestNewTestMorseStateExport(t *testing.T) { | ||
for i := 1; i < 10; i++ { | ||
t.Run(fmt.Sprintf("num_accounts=%d", i), func(t *testing.T) { | ||
morseStateExport := new(migrationtypes.MorseStateExport) | ||
stateExportBz, _ := newMorseStateExportAndAccountState(t, i) | ||
err := cmtjson.Unmarshal(stateExportBz, morseStateExport) | ||
require.NoError(t, err) | ||
|
||
exportAccounts := morseStateExport.AppState.Auth.Accounts | ||
require.Equal(t, i, len(exportAccounts)) | ||
|
||
expectedShannonBalance := fmt.Sprintf("%d%d%d0%d%d%d", i, i, i, i, i, i) | ||
morseAccountState := new(migrationtypes.MorseAccountState) | ||
morseAccountStateBz, err := transformMorseState(morseStateExport) | ||
require.NoError(t, err) | ||
|
||
err = cmtjson.Unmarshal(morseAccountStateBz, morseAccountState) | ||
require.NoError(t, err) | ||
|
||
require.Equal(t, expectedShannonBalance, morseAccountState.Accounts[i-1].Coins[0].Amount.String()) | ||
}) | ||
} | ||
} | ||
|
||
func BenchmarkTransformMorseState(b *testing.B) { | ||
for i := 0; i < 5; i++ { | ||
numAccounts := int(math.Pow10(i + 1)) | ||
morseStateExport := new(migrationtypes.MorseStateExport) | ||
morseStateExportBz, _ := newMorseStateExportAndAccountState(b, numAccounts) | ||
err := cmtjson.Unmarshal(morseStateExportBz, morseStateExport) | ||
require.NoError(b, err) | ||
|
||
b.Run(fmt.Sprintf("num_accounts=%d", numAccounts), func(b *testing.B) { | ||
|
||
// Call the function under test. | ||
b.ResetTimer() | ||
for i := 0; i < b.N; i++ { | ||
_, err = transformMorseState(morseStateExport) | ||
require.NoError(b, err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// TODO_CONSIDERATION: Test/benchmark execution speed can be optimized by refactoring this to a pre-generate fixture. | ||
func newMorseStateExportAndAccountState( | ||
t gocuke.TestingT, | ||
numAccounts int, | ||
) (morseStateExportBz []byte, morseAccountStateBz []byte) { | ||
morseStateExport := &migrationtypes.MorseStateExport{ | ||
AppHash: "", | ||
AppState: &migrationtypes.MorseAppState{ | ||
Application: &migrationtypes.MorseApplications{}, | ||
Auth: &migrationtypes.MorseAuth{}, | ||
Pos: &migrationtypes.MorsePos{}, | ||
}, | ||
} | ||
|
||
morseAccountState := &migrationtypes.MorseAccountState{ | ||
Accounts: make([]*migrationtypes.MorseAccount, numAccounts), | ||
} | ||
|
||
for i := 1; i < numAccounts+1; i++ { | ||
seedUint := rand.Uint64() | ||
seedBz := make([]byte, 8) | ||
binary.LittleEndian.PutUint64(seedBz, seedUint) | ||
privKey := cometcrypto.GenPrivKeyFromSecret(seedBz) | ||
pubKey := privKey.PubKey() | ||
balanceAmount := int64(1e6*i + i) // i_000_00i | ||
appStakeAmount := int64(1e5*i + (i * 10)) // i00_0i0 | ||
supplierStakeAmount := int64(1e4*i + (i * 100)) // i0_i00 | ||
sumAmount := balanceAmount + appStakeAmount + supplierStakeAmount // i_ii0_iii | ||
|
||
// Add an account. | ||
morseStateExport.AppState.Auth.Accounts = append( | ||
morseStateExport.AppState.Auth.Accounts, | ||
&migrationtypes.MorseAuthAccount{ | ||
Type: "posmint/Account", | ||
Value: &migrationtypes.MorseAccount{ | ||
Address: pubKey.Address(), | ||
Coins: cosmostypes.NewCoins(cosmostypes.NewInt64Coin(volatile.DenomuPOKT, balanceAmount)), | ||
PubKey: &migrationtypes.MorsePublicKey{ | ||
Value: pubKey.Bytes(), | ||
}, | ||
}, | ||
}, | ||
) | ||
|
||
// Add an application. | ||
morseStateExport.AppState.Application.Applications = append( | ||
morseStateExport.AppState.Application.Applications, | ||
&migrationtypes.MorseApplication{ | ||
Address: pubKey.Address(), | ||
PublicKey: pubKey.Bytes(), | ||
Jailed: false, | ||
Status: 2, | ||
StakedTokens: fmt.Sprintf("%d", appStakeAmount), | ||
}, | ||
) | ||
|
||
// Add a supplier. | ||
morseStateExport.AppState.Pos.Validators = append( | ||
morseStateExport.AppState.Pos.Validators, | ||
&migrationtypes.MorseValidator{ | ||
Address: pubKey.Address(), | ||
PublicKey: pubKey.Bytes(), | ||
Jailed: false, | ||
Status: 2, | ||
StakedTokens: fmt.Sprintf("%d", supplierStakeAmount), | ||
}, | ||
) | ||
|
||
// Add the account to the morseAccountState. | ||
morseAccountState.Accounts[i-1] = &migrationtypes.MorseAccount{ | ||
Address: pubKey.Address(), | ||
Coins: cosmostypes.NewCoins(cosmostypes.NewInt64Coin(volatile.DenomuPOKT, sumAmount)), | ||
PubKey: &migrationtypes.MorsePublicKey{ | ||
Value: pubKey.Bytes(), | ||
}, | ||
} | ||
} | ||
|
||
var err error | ||
morseStateExportBz, err = cmtjson.Marshal(morseStateExport) | ||
require.NoError(t, err) | ||
|
||
morseAccountStateBz, err = cmtjson.Marshal(morseAccountState) | ||
require.NoError(t, err) | ||
|
||
return morseStateExportBz, morseAccountStateBz | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.