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] feat: implement Morse account state upload #1047

Open
wants to merge 4 commits into
base: issues/1034/scaffold/morse_account_state
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
645 changes: 645 additions & 0 deletions api/poktroll/migration/event.pulsar.go

Large diffs are not rendered by default.

101 changes: 4 additions & 97 deletions cmd/poktrolld/cmd/migrate/migrate_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
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"
"github.com/pokt-network/poktroll/testutil/testmigration"
migrationtypes "github.com/pokt-network/poktroll/x/migration/types"
)

Expand All @@ -26,7 +21,7 @@ func TestCollectMorseAccounts(t *testing.T) {
inputFile, err := os.CreateTemp(tmpDir, "morse-state-input.json")
require.NoError(t, err)

morseStateExportBz, morseAccountStateBz := newMorseStateExportAndAccountState(t, 10)
morseStateExportBz, morseAccountStateBz := testmigration.NewMorseStateExportAndAccountStateBytes(t, 10)
_, err = inputFile.Write(morseStateExportBz)
require.NoError(t, err)

Expand Down Expand Up @@ -55,7 +50,7 @@ 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)
stateExportBz, _ := testmigration.NewMorseStateExportAndAccountStateBytes(t, i)
err := cmtjson.Unmarshal(stateExportBz, morseStateExport)
require.NoError(t, err)

Expand All @@ -79,7 +74,7 @@ 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)
morseStateExportBz, _ := testmigration.NewMorseStateExportAndAccountStateBytes(b, numAccounts)
err := cmtjson.Unmarshal(morseStateExportBz, morseStateExport)
require.NoError(b, err)

Expand All @@ -94,91 +89,3 @@ func BenchmarkTransformMorseState(b *testing.B) {
})
}
}

// 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
}
18 changes: 18 additions & 0 deletions proto/poktroll/migration/event.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
syntax = "proto3";
package poktroll.migration;

option go_package = "github.com/pokt-network/poktroll/x/migration/types";
option (gogoproto.stable_marshaler_all) = true;

import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";

import "poktroll/shared/service.proto";
import "poktroll/migration/types.proto";

// EventUploadMorseState is emitted when a new state hash is uploaded.
Copy link
Member

Choose a reason for hiding this comment

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

  1. EventUploadMorseState != EventCreateMorseAccountState
  2. state hash or the whole thing?

message EventCreateMorseAccountState {
int64 height = 1 [(gogoproto.jsontag) = "height"];
Copy link
Member

Choose a reason for hiding this comment

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

Is this a snapshot height or the height of morse?

bytes state_hash = 3 [(gogoproto.jsontag) = "state_hash"];
}
122 changes: 122 additions & 0 deletions testutil/testmigration/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package testmigration

import (
"encoding/binary"
"fmt"
"math/rand"

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

// NewMorseStateExportAndAccountStateBytes returns a serialized `MorseStateExport`
// and its corresponding `MorseAccountState`, populated dynamically with randomized
// account addresses, and monotonically increasing balances/stakes. For each account,
// one application and supplier are also added to the states.
// TODO_CONSIDERATION: Test/benchmark execution speed can be optimized by refactoring this to a pre-generate fixture.
Comment on lines +18 to +22
Copy link
Member

Choose a reason for hiding this comment

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

If you use the "Code Cleaner" claude project I created, this is how it refactored the comments. Much easier to read.

Suggested change
// NewMorseStateExportAndAccountStateBytes returns a serialized `MorseStateExport`
// and its corresponding `MorseAccountState`, populated dynamically with randomized
// account addresses, and monotonically increasing balances/stakes. For each account,
// one application and supplier are also added to the states.
// TODO_CONSIDERATION: Test/benchmark execution speed can be optimized by refactoring this to a pre-generate fixture.
// NewMorseStateExportAndAccountStateBytes returns:
// - A serialized MorseStateExport
// - Its corresponding MorseAccountState
//
// The states are populated with:
// - Random account addresses
// - Monotonically increasing balances/stakes
// - One application per account
// - One supplier per account
//
// TODO_CONSIDERATION: Test/benchmark execution speed can be optimized by refactoring this to a pre-generate fixture.

func NewMorseStateExportAndAccountStateBytes(
t gocuke.TestingT,
numAccounts int,
) (morseStateExportBz []byte, morseAccountStateBz []byte) {
morseStateExport, morseAccountState := NewMorseStateExportAndAccountState(t, numAccounts)

var err error
morseStateExportBz, err = cmtjson.Marshal(morseStateExport)
require.NoError(t, err)

morseAccountStateBz, err = cmtjson.Marshal(morseAccountState)
require.NoError(t, err)

return morseStateExportBz, morseAccountStateBz
}

// NewMorseStateExportAndAccountState returns a `MorseStateExport` and its
// corresponding `MorseAccountState`, populated dynamically with randomized
// account addresses, and monotonically increasing balances/stakes. For each account,
// one application and supplier are also added to the states.
Comment on lines +39 to +42
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// NewMorseStateExportAndAccountState returns a `MorseStateExport` and its
// corresponding `MorseAccountState`, populated dynamically with randomized
// account addresses, and monotonically increasing balances/stakes. For each account,
// one application and supplier are also added to the states.
// NewMorseStateExportAndAccountState returns MorseStateExport and MorseAccountState
// structs populated with:
// - Random account addresses
// - Monotonically increasing balances/stakes
// - One application per account
// - One supplier per account
func NewMorseStateExportAndAccountState() (*types.MorseStateExport, *types.MorseAccountState)

func NewMorseStateExportAndAccountState(
t gocuke.TestingT, numAccounts int,
) (export *migrationtypes.MorseStateExport, state *migrationtypes.MorseAccountState) {
t.Helper()

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

return morseStateExport, morseAccountState
}
40 changes: 33 additions & 7 deletions x/migration/keeper/msg_server_morse_account_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,51 @@ package keeper
import (
"context"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pokt-network/poktroll/x/migration/types"
)

func (k msgServer) CreateMorseAccountState(goCtx context.Context, msg *types.MsgCreateMorseAccountState) (*types.MsgCreateMorseAccountStateResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
func (k msgServer) CreateMorseAccountState(ctx context.Context, msg *types.MsgCreateMorseAccountState) (*types.MsgCreateMorseAccountStateResponse, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Consider multilining heaer for readability

sdkCtx := sdk.UnwrapSDKContext(ctx)
Copy link
Member

Choose a reason for hiding this comment

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

Please add some logging


if err := msg.ValidateBasic(); err != nil {
return nil, err
}

// Check if the value already exists
_, isFound := k.GetMorseAccountState(ctx)
_, isFound := k.GetMorseAccountState(sdkCtx)
if isFound {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "already set")
return nil, status.Error(
codes.FailedPrecondition,
sdkerrors.ErrInvalidRequest.Wrap("already set").Error(),
Copy link
Member

Choose a reason for hiding this comment

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

Not a TODO, but just an FYI that we may need to handle updates.

No one knows what the requirements of that will look like yet.

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 think we SHOULD NOT. This is a very slippery slope. My expectation is that we do one or more "dress rehearsals" on Shannon testnet, using real Morse migration state, prior to doing it on Shannon mainnet.

Supporting updates to the imported account state necessarily adds very noteworthy complexity that I don't think really gives us any value in return.

See: #1045 (comment)

)
}

k.SetMorseAccountState(
ctx,
sdkCtx,
msg.MorseAccountState,
)
return &types.MsgCreateMorseAccountStateResponse{}, nil

stateHash, err := msg.MorseAccountState.GetHash()
Copy link
Member

Choose a reason for hiding this comment

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

Confirming that this is the actual "Morse Blockchain State Hash" and not just a "hash of the snapshot we make"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will #PUC, it is a hash of the unsigned MsgCreateMorseAccountState.

if err != nil {
return nil, err
}

if err = sdkCtx.EventManager().EmitTypedEvent(
&types.EventCreateMorseAccountState{
Height: sdkCtx.BlockHeight(),
Copy link
Member

Choose a reason for hiding this comment

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

Could you add some more details in this event given it'll only ever fired once (or a handful depending on how things evolve)?

E.g. Total amount of upokt transferred, total amount of accounts, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be fairly trivial to add the number of accounts.

I was already reluctant to do the hash computation on-chain due to concern about the CPU required to do so as the serialized state may be very large (tens of MB). Iterating over ~50K+ accounts to sum their amounts may take some time. I chose to avoid going down that rabbit hole. Happy to get out my shovel and hardhat if you think it's necessary.

StateHash: stateHash,
},
); err != nil {
return nil, err
}

return &types.MsgCreateMorseAccountStateResponse{
StateHash: stateHash,
NumAccounts: uint64(len(msg.MorseAccountState.Accounts)),
}, nil
}
Loading