diff --git a/app/app.go b/app/app.go index 7a92f9827..68f834d0b 100644 --- a/app/app.go +++ b/app/app.go @@ -7,6 +7,8 @@ import ( "os" "path/filepath" + // this line is used by starport scaffolding # stargate/app/moduleImport + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" dbm "github.com/cometbft/cometbft-db" @@ -108,8 +110,6 @@ import ( ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" solomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" - "github.com/spf13/cast" - appparams "github.com/pokt-network/poktroll/app/params" "github.com/pokt-network/poktroll/docs" applicationmodule "github.com/pokt-network/poktroll/x/application" @@ -133,6 +133,7 @@ import ( tokenomicsmodule "github.com/pokt-network/poktroll/x/tokenomics" tokenomicsmodulekeeper "github.com/pokt-network/poktroll/x/tokenomics/keeper" tokenomicsmoduletypes "github.com/pokt-network/poktroll/x/tokenomics/types" + "github.com/spf13/cast" ) const ( @@ -660,6 +661,8 @@ func New( keys[tokenomicsmoduletypes.MemStoreKey], app.GetSubspace(tokenomicsmoduletypes.ModuleName), app.BankKeeper, + app.ApplicationKeeper, + app.SupplierKeeper, authority, ) tokenomicsModule := tokenomicsmodule.NewAppModule(appCodec, app.TokenomicsKeeper, app.AccountKeeper, app.BankKeeper) diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index c97017c2c..5afc0409f 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -16,14 +16,15 @@ import ( tmcli "github.com/cometbft/cometbft/libs/cli" "github.com/cosmos/cosmos-sdk/codec" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + "github.com/pokt-network/poktroll/app" "github.com/pokt-network/poktroll/testutil/testclient" apptypes "github.com/pokt-network/poktroll/x/application/types" sessiontypes "github.com/pokt-network/poktroll/x/session/types" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" - "github.com/regen-network/gocuke" - "github.com/stretchr/testify/require" ) var ( diff --git a/e2e/tests/session.feature b/e2e/tests/session.feature index 1623b0acf..0a1f9381c 100644 --- a/e2e/tests/session.feature +++ b/e2e/tests/session.feature @@ -5,7 +5,7 @@ Feature: Session Namespace Scenario: Supplier completes claim/proof lifecycle for a valid session Given the user has the pocketd binary installed When the supplier "supplier1" has serviced a session with "5" relays for service "svc1" for application "app1" - And after the supplier creates a claim for the session for service "svc1" for application "app1" + And the user should wait for "5" seconds Then the claim created by supplier "supplier1" for service "svc1" for application "app1" should be persisted on-chain # TODO_IMPROVE: ... # And an event should be emitted... diff --git a/e2e/tests/tokenomics.feature b/e2e/tests/tokenomics.feature new file mode 100644 index 000000000..5362550b0 --- /dev/null +++ b/e2e/tests/tokenomics.feature @@ -0,0 +1,13 @@ +Feature: Tokenomics Namespaces + + # This test + Scenario: Basic tokenomics validation that Supplier mint equals Application burn + Given the user has the pocketd binary installed + And an account exists for "supplier1" + And an account exists for "app1" + When the supplier "supplier1" has serviced a session with "20" relays for service "svc1" for application "app1" + And the user should wait for "5" seconds + # TODO_UPNEXT(@Olshansk, #359): Expand on the two expectations below after integrating the tokenomics module + # into the supplier module. + # Then the account balance of "supplier1" should be "1000" uPOKT "more" than before + # And the account balance of "app1" should be "1000" uPOKT "less" than before diff --git a/go.mod b/go.mod index 3bba35590..d8360ef37 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( github.com/athanorlabs/go-dleq v0.1.0 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.8.0 - github.com/cosmos/cosmos-proto v1.0.0-beta.2 github.com/cosmos/cosmos-sdk v0.47.5 github.com/cosmos/gogoproto v1.4.11 github.com/cosmos/ibc-go/v7 v7.3.1 @@ -51,7 +50,6 @@ require ( golang.org/x/crypto v0.15.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/sync v0.5.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb google.golang.org/grpc v1.59.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -98,6 +96,7 @@ require ( github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.0 // indirect @@ -294,6 +293,7 @@ require ( google.golang.org/api v0.143.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/pkg/sdk/errors.go b/pkg/sdk/errors.go index 064032be4..68840183c 100644 --- a/pkg/sdk/errors.go +++ b/pkg/sdk/errors.go @@ -4,6 +4,8 @@ import ( sdkerrors "cosmossdk.io/errors" ) +// TODO_TECHDEBT: Do a source code wise find-replace using regex pattern match +// of `sdkerrors\.Wrapf\(([a-zA-Z]+), ` with `$1.Wrapf(` var ( codespace = "poktrollsdk" ErrSDKHandleRelay = sdkerrors.Register(codespace, 1, "internal error handling relay request") diff --git a/testutil/keeper/tokenomics.go b/testutil/keeper/tokenomics.go index 4eb1befcb..a00ab49ab 100644 --- a/testutil/keeper/tokenomics.go +++ b/testutil/keeper/tokenomics.go @@ -16,27 +16,74 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "github.com/pokt-network/poktroll/testutil/sample" mocks "github.com/pokt-network/poktroll/testutil/tokenomics/mocks" + apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" "github.com/pokt-network/poktroll/x/tokenomics/keeper" "github.com/pokt-network/poktroll/x/tokenomics/types" ) -func TokenomicsKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { +// TODO_TECHDEBT: Replace `AnyTimes` w/ `Times/MinTimes/MaxTimes` as the tests +// mature to be explicit about the number of expected tests. + +func TokenomicsKeeper(t testing.TB) ( + k *keeper.Keeper, s sdk.Context, + appAddr, supplierAddr string, +) { storeKey := sdk.NewKVStoreKey(types.StoreKey) memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) + // Initialize the in-memory tendermint database db := tmdb.NewMemDB() stateStore := store.NewCommitMultiStore(db) stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) stateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) require.NoError(t, stateStore.LoadLatestVersion()) + // Initialize the codec and other necessary components registry := codectypes.NewInterfaceRegistry() cdc := codec.NewProtoCodec(registry) + ctrl := gomock.NewController(t) + // The on-chain governance address authority := authtypes.NewModuleAddress("gov").String() - ctrl := gomock.NewController(t) + // Prepare the test application + application := apptypes.Application{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100000)}, + } + + // Prepare the test supplier + supplier := sharedtypes.Supplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100000)}, + } + + // Mock the application keeper + mockApplicationKeeper := mocks.NewMockApplicationKeeper(ctrl) + mockApplicationKeeper.EXPECT(). + GetApplication(gomock.Any(), gomock.Eq(application.Address)). + Return(application, true). + AnyTimes() + mockApplicationKeeper.EXPECT(). + GetApplication(gomock.Any(), gomock.Not(application.Address)). + Return(apptypes.Application{}, false). + AnyTimes() + mockApplicationKeeper.EXPECT(). + SetApplication(gomock.Any(), gomock.Any()). + AnyTimes() + + // Mock the supplier keeper + mockSupplierKeeper := mocks.NewMockSupplierKeeper(ctrl) + mockSupplierKeeper.EXPECT(). + GetSupplier(gomock.Any(), supplier.Address). + Return(supplier, true). + AnyTimes() + + // Mock the bank keeper mockBankKeeper := mocks.NewMockBankKeeper(ctrl) mockBankKeeper.EXPECT(). MintCoins(gomock.Any(), gomock.Any(), gomock.Any()). @@ -45,28 +92,31 @@ func TokenomicsKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { BurnCoins(gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes() mockBankKeeper.EXPECT(). - SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()). + SendCoinsFromModuleToAccount(gomock.Any(), suppliertypes.ModuleName, gomock.Any(), gomock.Any()). AnyTimes() mockBankKeeper.EXPECT(). - SendCoinsFromModuleToModule(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()). + SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), apptypes.ModuleName, gomock.Any()). AnyTimes() mockBankKeeper.EXPECT(). - SendCoinsFromAccountToModule(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()). + UndelegateCoinsFromModuleToAccount(gomock.Any(), apptypes.ModuleName, gomock.Any(), gomock.Any()). AnyTimes() + // Initialize the tokenomics keeper paramsSubspace := typesparams.NewSubspace(cdc, types.Amino, storeKey, memStoreKey, "TokenomicsParams", ) - k := keeper.NewKeeper( + tokenomicsKeeper := keeper.NewKeeper( cdc, storeKey, memStoreKey, paramsSubspace, mockBankKeeper, + mockApplicationKeeper, + mockSupplierKeeper, authority, ) @@ -74,7 +124,7 @@ func TokenomicsKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) // Initialize params - k.SetParams(ctx, types.DefaultParams()) + tokenomicsKeeper.SetParams(ctx, types.DefaultParams()) - return k, ctx + return tokenomicsKeeper, ctx, application.Address, supplier.Address } diff --git a/x/session/types/errors.go b/x/session/types/errors.go index dde73c1e6..900c3c72e 100644 --- a/x/session/types/errors.go +++ b/x/session/types/errors.go @@ -15,4 +15,5 @@ var ( ErrSessionInvalidAppAddress = sdkerrors.Register(ModuleName, 5, "invalid application address for session") ErrSessionInvalidService = sdkerrors.Register(ModuleName, 6, "invalid service in session") ErrSessionInvalidBlockHeight = sdkerrors.Register(ModuleName, 7, "invalid block height for session") + ErrSessionInvalidSessionId = sdkerrors.Register(ModuleName, 8, "invalid sessionId") ) diff --git a/x/session/types/query_get_session_request.go b/x/session/types/query_get_session_request.go index 31b705f8e..3c7b754d3 100644 --- a/x/session/types/query_get_session_request.go +++ b/x/session/types/query_get_session_request.go @@ -1,7 +1,6 @@ package types import ( - sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sharedhelpers "github.com/pokt-network/poktroll/x/shared/helpers" @@ -24,17 +23,17 @@ func NewQueryGetSessionRequest(appAddress, serviceId string, blockHeight int64) func (query *QueryGetSessionRequest) ValidateBasic() error { // Validate the application address if _, err := sdk.AccAddressFromBech32(query.ApplicationAddress); err != nil { - return sdkerrors.Wrapf(ErrSessionInvalidAppAddress, "invalid app address for session being retrieved %s; (%v)", query.ApplicationAddress, err) + return ErrSessionInvalidAppAddress.Wrapf("invalid app address for session being retrieved %s; (%v)", query.ApplicationAddress, err) } // Validate the Service ID if !sharedhelpers.IsValidService(query.Service) { - return sdkerrors.Wrapf(ErrSessionInvalidService, "invalid service for session being retrieved %s;", query.Service) + return ErrSessionInvalidService.Wrapf("invalid service for session being retrieved %s;", query.Service) } // Validate the height for which a session is being retrieved if query.BlockHeight < 0 { // Note that `0` defaults to the latest height rather than genesis - return sdkerrors.Wrapf(ErrSessionInvalidBlockHeight, "invalid block height for session being retrieved %d;", query.BlockHeight) + return ErrSessionInvalidBlockHeight.Wrapf("invalid block height for session being retrieved %d;", query.BlockHeight) } return nil } diff --git a/x/session/types/session_header.go b/x/session/types/session_header.go new file mode 100644 index 000000000..adcb3b040 --- /dev/null +++ b/x/session/types/session_header.go @@ -0,0 +1,34 @@ +package types + +import ( + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// TODO_TECHDEBT: Make sure this is used everywhere we validate components +// of the session header. +func (sh *SessionHeader) ValidateBasic() error { + // Validate the application address + if _, err := sdk.AccAddressFromBech32(sh.ApplicationAddress); err != nil { + return sdkerrors.Wrapf(ErrSessionInvalidAppAddress, "invalid application address: %s; (%v)", sh.ApplicationAddress, err) + } + + // Validate the session ID + // TODO_TECHDEBT: Introduce a `SessionId#ValidateBasic` method. + if sh.SessionId == "" { + return sdkerrors.Wrapf(ErrSessionInvalidSessionId, "invalid session ID: %s", sh.SessionId) + } + + // Validate the service + // TODO_TECHDEBT: Introduce a `Service#ValidateBasic` method. + if sh.Service == nil { + return sdkerrors.Wrapf(ErrSessionInvalidService, "invalid service: %s", sh.Service) + } + + // Check if session end height is greater than session start height + if sh.SessionEndBlockHeight <= sh.SessionStartBlockHeight { + return sdkerrors.Wrapf(ErrSessionInvalidBlockHeight, "session end block height cannot be less than or equal to session start block height") + } + + return nil +} diff --git a/x/session/types/session_header_test.go b/x/session/types/session_header_test.go new file mode 100644 index 000000000..e0e60ce93 --- /dev/null +++ b/x/session/types/session_header_test.go @@ -0,0 +1,86 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +func TestSessionHeader_ValidateBasic(t *testing.T) { + tests := []struct { + desc string + sh types.SessionHeader + err error + }{ + { + desc: "invalid - invalid application address", + sh: types.SessionHeader{ + ApplicationAddress: "invalid_address", + SessionId: "valid_session_id", + Service: &sharedtypes.Service{}, + SessionStartBlockHeight: 100, + SessionEndBlockHeight: 101, + }, + err: types.ErrSessionInvalidAppAddress, + }, + { + desc: "invalid - empty session id", + sh: types.SessionHeader{ + ApplicationAddress: sample.AccAddress(), + SessionId: "", + Service: &sharedtypes.Service{}, + SessionStartBlockHeight: 100, + SessionEndBlockHeight: 101, + }, + err: types.ErrSessionInvalidSessionId, + }, + { + desc: "invalid - nil service", + sh: types.SessionHeader{ + ApplicationAddress: sample.AccAddress(), + SessionId: "valid_session_id", + Service: nil, + SessionStartBlockHeight: 100, + SessionEndBlockHeight: 101, + }, + err: types.ErrSessionInvalidService, + }, + { + desc: "invalid - start block height greater than end block height", + sh: types.SessionHeader{ + ApplicationAddress: sample.AccAddress(), + SessionId: "valid_session_id", + Service: &sharedtypes.Service{}, + SessionStartBlockHeight: 100, + SessionEndBlockHeight: 99, + }, + err: types.ErrSessionInvalidBlockHeight, + }, + { + desc: "valid", + sh: types.SessionHeader{ + ApplicationAddress: sample.AccAddress(), + SessionId: "valid_session_id", + Service: &sharedtypes.Service{}, + SessionStartBlockHeight: 100, + SessionEndBlockHeight: 101, + }, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := tt.sh.ValidateBasic() + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/supplier/keeper/msg_server_submit_proof.go b/x/supplier/keeper/msg_server_submit_proof.go index cae07a675..5455b190a 100644 --- a/x/supplier/keeper/msg_server_submit_proof.go +++ b/x/supplier/keeper/msg_server_submit_proof.go @@ -72,7 +72,7 @@ func (k msgServer) SubmitProof(goCtx context.Context, msg *suppliertypes.MsgSubm // in any case where the supplier should no longer be able to update the given proof. k.Keeper.UpsertProof(ctx, proof) - // TODO_BLOCKER(@bryanchriswhite, @Olshansk): Call `tokenomics.SettleSessionAccounting()` here + // TODO_UPNEXT(@Olshansk, #359): Call `tokenomics.SettleSessionAccounting()` here logger. With( diff --git a/x/tokenomics/genesis_test.go b/x/tokenomics/genesis_test.go index 24faddd65..65fbee07f 100644 --- a/x/tokenomics/genesis_test.go +++ b/x/tokenomics/genesis_test.go @@ -18,7 +18,7 @@ func TestGenesis(t *testing.T) { // this line is used by starport scaffolding # genesis/test/state } - k, ctx := keepertest.TokenomicsKeeper(t) + k, ctx, _, _ := keepertest.TokenomicsKeeper(t) tokenomics.InitGenesis(ctx, *k, genesisState) got := tokenomics.ExportGenesis(ctx, *k) require.NotNil(t, got) diff --git a/x/tokenomics/keeper/keeper.go b/x/tokenomics/keeper/keeper.go index 635b330a4..c07cbe767 100644 --- a/x/tokenomics/keeper/keeper.go +++ b/x/tokenomics/keeper/keeper.go @@ -24,7 +24,9 @@ type Keeper struct { paramstore paramtypes.Subspace // keeper dependencies - bankKeeper types.BankKeeper + appKeeper types.ApplicationKeeper + supplierKeeper types.SupplierKeeper + bankKeeper types.BankKeeper // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. @@ -39,6 +41,8 @@ func NewKeeper( // keeper dependencies bankKeeper types.BankKeeper, + appKeeper types.ApplicationKeeper, + supplierKeeper types.SupplierKeeper, authority string, ) *Keeper { @@ -53,6 +57,10 @@ func NewKeeper( memKey: memKey, paramstore: ps, + bankKeeper: bankKeeper, + appKeeper: appKeeper, + supplierKeeper: supplierKeeper, + authority: authority, } } diff --git a/x/tokenomics/keeper/msg_server_test.go b/x/tokenomics/keeper/msg_server_test.go index 545dff143..189647006 100644 --- a/x/tokenomics/keeper/msg_server_test.go +++ b/x/tokenomics/keeper/msg_server_test.go @@ -13,7 +13,7 @@ import ( ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { - k, ctx := testkeeper.TokenomicsKeeper(t) + k, ctx, _, _ := testkeeper.TokenomicsKeeper(t) return keeper.NewMsgServerImpl(*k), sdk.WrapSDKContext(ctx) } diff --git a/x/tokenomics/keeper/msg_server_update_params.go b/x/tokenomics/keeper/msg_server_update_params.go index 837904c2e..c37b380c0 100644 --- a/x/tokenomics/keeper/msg_server_update_params.go +++ b/x/tokenomics/keeper/msg_server_update_params.go @@ -47,7 +47,7 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { } // ComputeUnitsToTokensMultiplier returns the ComputeUnitsToTokensMultiplier param -func (k Keeper) ComputeUnitsToTokensMultiplier(ctx sdk.Context) (res uint64) { - k.paramstore.Get(ctx, types.KeyComputeUnitsToTokensMultiplier, &res) +func (k Keeper) ComputeUnitsToTokensMultiplier(ctx sdk.Context) (param uint64) { + k.paramstore.Get(ctx, types.KeyComputeUnitsToTokensMultiplier, ¶m) return } diff --git a/x/tokenomics/keeper/msg_server_update_params_test.go b/x/tokenomics/keeper/msg_server_update_params_test.go index 831244f72..3e601a49e 100644 --- a/x/tokenomics/keeper/msg_server_update_params_test.go +++ b/x/tokenomics/keeper/msg_server_update_params_test.go @@ -3,20 +3,19 @@ package keeper_test import ( "testing" - "github.com/stretchr/testify/require" - testkeeper "github.com/pokt-network/poktroll/testutil/keeper" "github.com/pokt-network/poktroll/testutil/sample" "github.com/pokt-network/poktroll/x/tokenomics/keeper" "github.com/pokt-network/poktroll/x/tokenomics/types" + "github.com/stretchr/testify/require" ) func TestUpdateParams_Validity(t *testing.T) { - Keeper, ctx := testkeeper.TokenomicsKeeper(t) - srv := keeper.NewMsgServerImpl(*Keeper) + tokenomicsKeeper, ctx, _, _ := testkeeper.TokenomicsKeeper(t) + srv := keeper.NewMsgServerImpl(*tokenomicsKeeper) params := types.DefaultParams() - Keeper.SetParams(ctx, params) + tokenomicsKeeper.SetParams(ctx, params) tests := []struct { desc string @@ -59,7 +58,7 @@ func TestUpdateParams_Validity(t *testing.T) { desc: "invalid ComputeUnitsToTokensMultiplier", req: &types.MsgUpdateParams{ - Authority: Keeper.GetAuthority(), + Authority: tokenomicsKeeper.GetAuthority(), Params: types.Params{ ComputeUnitsToTokensMultiplier: 0, @@ -74,7 +73,7 @@ func TestUpdateParams_Validity(t *testing.T) { desc: "successful param update", req: &types.MsgUpdateParams{ - Authority: Keeper.GetAuthority(), + Authority: tokenomicsKeeper.GetAuthority(), Params: types.Params{ ComputeUnitsToTokensMultiplier: 1000000, @@ -109,21 +108,21 @@ func TestUpdateParams_Validity(t *testing.T) { } func TestUpdateParams_ComputeUnitsToTokensMultiplier(t *testing.T) { - Keeper, ctx := testkeeper.TokenomicsKeeper(t) - srv := keeper.NewMsgServerImpl(*Keeper) + tokenomicsKeeper, ctx, _, _ := testkeeper.TokenomicsKeeper(t) + srv := keeper.NewMsgServerImpl(*tokenomicsKeeper) // Set the default params - Keeper.SetParams(ctx, types.DefaultParams()) + tokenomicsKeeper.SetParams(ctx, types.DefaultParams()) // Verify the default value for ComputeUnitsToTokensMultiplier getParamsReq := &types.QueryParamsRequest{} - getParamsRes, err := Keeper.Params(ctx, getParamsReq) + getParamsRes, err := tokenomicsKeeper.Params(ctx, getParamsReq) require.Nil(t, err) require.Equal(t, uint64(42), getParamsRes.Params.GetComputeUnitsToTokensMultiplier()) // Update the value for ComputeUnitsToTokensMultiplier updateParamsReq := &types.MsgUpdateParams{ - Authority: Keeper.GetAuthority(), + Authority: tokenomicsKeeper.GetAuthority(), Params: types.Params{ ComputeUnitsToTokensMultiplier: 69, }, @@ -132,7 +131,7 @@ func TestUpdateParams_ComputeUnitsToTokensMultiplier(t *testing.T) { require.Nil(t, err) // Verify that ComputeUnitsToTokensMultiplier was updated - getParamsRes, err = Keeper.Params(ctx, getParamsReq) + getParamsRes, err = tokenomicsKeeper.Params(ctx, getParamsReq) require.Nil(t, err) require.Equal(t, uint64(69), getParamsRes.Params.GetComputeUnitsToTokensMultiplier()) } diff --git a/x/tokenomics/keeper/query_params_test.go b/x/tokenomics/keeper/query_params_test.go index 19a5e9edc..456647091 100644 --- a/x/tokenomics/keeper/query_params_test.go +++ b/x/tokenomics/keeper/query_params_test.go @@ -4,14 +4,13 @@ import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - testkeeper "github.com/pokt-network/poktroll/testutil/keeper" "github.com/pokt-network/poktroll/x/tokenomics/types" + "github.com/stretchr/testify/require" ) func TestGetParams(t *testing.T) { - k, ctx := testkeeper.TokenomicsKeeper(t) + k, ctx, _, _ := testkeeper.TokenomicsKeeper(t) params := types.DefaultParams() k.SetParams(ctx, params) @@ -21,7 +20,7 @@ func TestGetParams(t *testing.T) { } func TestParamsQuery(t *testing.T) { - keeper, ctx := testkeeper.TokenomicsKeeper(t) + keeper, ctx, _, _ := testkeeper.TokenomicsKeeper(t) wctx := sdk.WrapSDKContext(ctx) params := types.DefaultParams() keeper.SetParams(ctx, params) diff --git a/x/tokenomics/keeper/settle_session_accounting.go b/x/tokenomics/keeper/settle_session_accounting.go index 274a2fe3a..b5c6c7130 100644 --- a/x/tokenomics/keeper/settle_session_accounting.go +++ b/x/tokenomics/keeper/settle_session_accounting.go @@ -2,10 +2,25 @@ package keeper import ( "context" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" + + apptypes "github.com/pokt-network/poktroll/x/application/types" suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" + "github.com/pokt-network/poktroll/x/tokenomics/types" +) + +const ( + // TODO_TECHDEBT: Retrieve this from the SMT package + // The number of bytes expected to be contained in the root hash being + // claimed in order to represent both the digest and the sum. + smstRootSize = 40 ) +// atomic.if this function is not atomic. + // SettleSessionAccounting is responsible for all of the post-session accounting // necessary to burn, mint or transfer tokens depending on the amount of work // done. The amount of "work done" complete is dictated by `sum` of `root`. @@ -14,10 +29,130 @@ import ( // against a proof BEFORE calling this function. // // TODO_BLOCKER(@Olshansk): Is there a way to limit who can call this function? -// TODO_UPNEXT(#323, @Olshansk): Implement this function func (k Keeper) SettleSessionAccounting( goCtx context.Context, claim *suppliertypes.Claim, ) error { + // Parse the context + ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "SettleSessionAccounting") + + if claim == nil { + logger.Error("received a nil claim") + return types.ErrTokenomicsClaimNil + } + + // Make sure the session header is not nil + sessionHeader := claim.SessionHeader + if sessionHeader == nil { + logger.Error("received a nil session header") + return types.ErrTokenomicsSessionHeaderNil + } + + // Validate the session header + if err := sessionHeader.ValidateBasic(); err != nil { + logger.Error("received an invalid session header", "error", err) + return types.ErrTokenomicsSessionHeaderInvalid + } + + // Decompose the claim into its constituent parts for readability + supplierAddress, err := sdk.AccAddressFromBech32(claim.SupplierAddress) + if err != nil { + return types.ErrTokenomicsSupplierAddressInvalid + } + applicationAddress, err := sdk.AccAddressFromBech32(claim.SessionHeader.ApplicationAddress) + if err != nil { + return types.ErrTokenomicsApplicationAddressInvalid + } + root := (smt.MerkleRoot)(claim.RootHash) + + if len(root) != smstRootSize { + logger.Error(fmt.Sprintf("received an invalid root hash of size: %d", len(root))) + return types.ErrTokenomicsRootHashInvalid + } + + // Retrieve the application + application, found := k.appKeeper.GetApplication(ctx, applicationAddress.String()) + if !found { + logger.Error(fmt.Sprintf("application for claim with address %s not found", applicationAddress)) + return types.ErrTokenomicsApplicationNotFound + } + + // Retrieve the sum of the root as a proxy into the amount of work done + claimComputeUnits := root.Sum() + + logger.Info(fmt.Sprintf("About to start settling claim for %d compute units", claimComputeUnits)) + + // Retrieve the existing tokenomics params + params := k.GetParams(ctx) + + // Calculate the amount of tokens to mint & burn + upokt := sdk.NewInt(int64(claimComputeUnits * params.ComputeUnitsToTokensMultiplier)) + upoktCoin := sdk.NewCoin("upokt", upokt) + upoktCoins := sdk.NewCoins(upoktCoin) + + logger.Info(fmt.Sprintf("%d compute units equate to %d uPOKT for session %s", claimComputeUnits, upokt, sessionHeader.SessionId)) + + // NB: We are doing a mint & burn + transfer, instead of a simple transfer + // of funds from the supplier to the application in order to enable second + // order economic effects with more optionality. This could include funds + // going to pnf, delegators, enabling bonuses/rebates, etc... + + // Mint uPOKT to the supplier module account + if err := k.bankKeeper.MintCoins(ctx, suppliertypes.ModuleName, upoktCoins); err != nil { + return types.ErrTokenomicsApplicationModuleFeeFailed + } + + logger.Info(fmt.Sprintf("minted %d uPOKT in the supplier module", upokt)) + + // Sent the minted coins to the supplier + if err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, suppliertypes.ModuleName, supplierAddress, upoktCoins, + ); err != nil { + return types.ErrTokenomicsApplicationModuleFeeFailed + } + + logger.Info(fmt.Sprintf("sent %d uPOKT to supplier with address %s", upokt, supplierAddress)) + + // Verify that the application has enough uPOKT to pay for the services it consumed + if application.Stake.IsLT(upoktCoin) { + logger.Error(fmt.Sprintf("THIS SHOULD NOT HAPPEN. Application with address %s needs to be charged more than it has staked: %v > %v", applicationAddress, upoktCoins, application.Stake)) + // TODO_BLOCKER(@Olshansk, @RawthiL): The application was over-serviced in the last session so it basically + // goes "into debt". Need to design a way to handle this when we implement + // probabilistic proofs and add all the parameter logic. Do we touch the application balance? + // Do we just let it go into debt? Do we penalize the application? Do we unstake it? Etc... + upoktCoins = sdk.NewCoins(*application.Stake) + } + + // Undelegate the amount of coins that need to be burnt from the application stake. + // Since the application commits a certain amount of stake to the network to be able + // to pay for relay mining, this stake is taken from the funds "in escrow" rather + // than its balance. + if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(ctx, apptypes.ModuleName, applicationAddress, upoktCoins); err != nil { + logger.Error(fmt.Sprintf("THIS SHOULD NOT HAPPEN. Application with address %s needs to be charged more than it has staked: %v > %v", applicationAddress, upoktCoins, application.Stake)) + + } + + // Send coins from the application to the application module account + if err := k.bankKeeper.SendCoinsFromAccountToModule( + ctx, applicationAddress, apptypes.ModuleName, upoktCoins, + ); err != nil { + return types.ErrTokenomicsApplicationModuleFeeFailed + } + + logger.Info(fmt.Sprintf("took %d uPOKT from application with address %s", upokt, applicationAddress)) + + // Burn uPOKT from the application module account + if err := k.bankKeeper.BurnCoins(ctx, apptypes.ModuleName, upoktCoins); err != nil { + return types.ErrTokenomicsApplicationModuleBurn + } + + logger.Info(fmt.Sprintf("burned %d uPOKT in the application module", upokt)) + + // Update the application's on-chain stake + newAppStake := (*application.Stake).Sub(upoktCoin) + application.Stake = &newAppStake + k.appKeeper.SetApplication(ctx, application) + return nil } diff --git a/x/tokenomics/keeper/settle_session_accounting_test.go b/x/tokenomics/keeper/settle_session_accounting_test.go new file mode 100644 index 000000000..355e91559 --- /dev/null +++ b/x/tokenomics/keeper/settle_session_accounting_test.go @@ -0,0 +1,272 @@ +package keeper_test + +import ( + "encoding/binary" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" + "github.com/stretchr/testify/require" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" + "github.com/pokt-network/poktroll/x/tokenomics/types" +) + +// TODO_TEST(@bryanchriswhite, @Olshansk): Improve tokenomics tests (i.e. checking balances) +// once in-memory network integration tests are supported. + +func TestSettleSessionAccounting_ValidAccounting(t *testing.T) { + t.Skip("TODO_BLOCKER(@Olshansk): Add E2E and integration tests so we validate the actual state changes of the bank & account keepers.") + // Assert that `suppliertypes.ModuleName` account module balance is *unchanged* + // Assert that `supplierAddress` account balance has *increased* by the appropriate amount + // Assert that `supplierAddress` staked balance is *unchanged* + // Assert that `apptypes.ModuleName` account module balance is *unchanged* + // Assert that `applicationAddress` account balance is *unchanged* + // Assert that `applicationAddress` staked balance has decreased by the appropriate amount +} + +func TestSettleSessionAccounting_AppStakeTooLow(t *testing.T) { + t.Skip("TODO_BLOCKER(@Olshansk): Add E2E and integration tests so we validate the actual state changes of the bank & account keepers.") + // Assert that `suppliertypes.Address` account balance has *increased* by the appropriate amount + // Assert that `applicationAddress` account staked balance has gone to zero + // Assert on whatever logic we have for slashing the application or other +} + +func TestSettleSessionAccounting_AppNotFound(t *testing.T) { + keeper, ctx, _, supplierAddr := testkeeper.TokenomicsKeeper(t) + wctx := sdk.WrapSDKContext(ctx) + + // The base claim whose root will be customized for testing purposes + claim := suppliertypes.Claim{ + SupplierAddress: supplierAddr, + SessionHeader: &sessiontypes.SessionHeader{ + ApplicationAddress: sample.AccAddress(), // Random address + Service: &sharedtypes.Service{ + Id: "svc1", + Name: "svcName1", + }, + SessionStartBlockHeight: 1, + SessionId: "1", + SessionEndBlockHeight: 5, + }, + RootHash: smstRootWithSum(42), + } + + err := keeper.SettleSessionAccounting(wctx, &claim) + require.Error(t, err) + require.ErrorIs(t, err, types.ErrTokenomicsApplicationNotFound) +} + +func TestSettleSessionAccounting_InvalidRoot(t *testing.T) { + keeper, ctx, appAddr, supplierAddr := testkeeper.TokenomicsKeeper(t) + wctx := sdk.WrapSDKContext(ctx) + + // Define test cases + testCases := []struct { + desc string + root []byte // smst.MerkleRoot + errExpected bool + }{ + { + desc: "Nil Root", + root: nil, + errExpected: true, + }, + { + desc: "Less than 40 bytes", + root: make([]byte, 39), // Less than 40 bytes + errExpected: true, + }, + { + desc: "More than 40 bytes", + root: make([]byte, 41), // More than 40 bytes + errExpected: true, + }, + { + desc: "40 bytes but empty", + root: func() []byte { + root := make([]byte, 40) // 40-byte slice of all 0s + return root[:] + }(), + errExpected: false, + }, + { + desc: "40 bytes but has an invalid value", + root: func() []byte { + var root [40]byte + copy(root[:], []byte("This text is exactly 40 characters!!!!!!")) + return root[:] + }(), + errExpected: true, + }, + { + desc: "40 bytes and has a valid value", + root: func() []byte { + root := smstRootWithSum(42) + return root[:] + }(), + errExpected: false, + }, + } + + // Iterate over each test case + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Use defer-recover to catch any panic + defer func() { + if r := recover(); r != nil { + t.Errorf("Test panicked: %s", r) + } + }() + + // Setup claim by copying the baseClaim and updating the root + claim := baseClaim(appAddr, supplierAddr, 0) + claim.RootHash = smt.MerkleRoot(tc.root[:]) + + // Execute test function + err := func() (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic occurred: %v", r) + } + }() + return keeper.SettleSessionAccounting(wctx, &claim) + }() + + // Assert the error + if tc.errExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSettleSessionAccounting_InvalidClaim(t *testing.T) { + keeper, ctx, appAddr, supplierAddr := testkeeper.TokenomicsKeeper(t) + wctx := sdk.WrapSDKContext(ctx) + + // Define test cases + testCases := []struct { + desc string + claim *suppliertypes.Claim + errExpected bool + expectErr error + }{ + + { + desc: "Valid Claim", + claim: func() *suppliertypes.Claim { + claim := baseClaim(appAddr, supplierAddr, 42) + return &claim + }(), + errExpected: false, + }, + { + desc: "Nil Claim", + claim: nil, + errExpected: true, + expectErr: types.ErrTokenomicsClaimNil, + }, + { + desc: "Claim with nil session header", + claim: func() *suppliertypes.Claim { + claim := baseClaim(appAddr, supplierAddr, 42) + claim.SessionHeader = nil + return &claim + }(), + errExpected: true, + expectErr: types.ErrTokenomicsSessionHeaderNil, + }, + { + desc: "Claim with invalid session id", + claim: func() *suppliertypes.Claim { + claim := baseClaim(appAddr, supplierAddr, 42) + claim.SessionHeader.SessionId = "" + return &claim + }(), + errExpected: true, + expectErr: types.ErrTokenomicsSessionHeaderInvalid, + }, + { + desc: "Claim with invalid application address", + claim: func() *suppliertypes.Claim { + claim := baseClaim(appAddr, supplierAddr, 42) + claim.SessionHeader.ApplicationAddress = "invalid address" + return &claim + }(), + errExpected: true, + expectErr: types.ErrTokenomicsSessionHeaderInvalid, + }, + { + desc: "Claim with invalid supplier address", + claim: func() *suppliertypes.Claim { + claim := baseClaim(appAddr, supplierAddr, 42) + claim.SupplierAddress = "invalid address" + return &claim + }(), + errExpected: true, + expectErr: types.ErrTokenomicsSupplierAddressInvalid, + }, + } + + // Iterate over each test case + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Use defer-recover to catch any panic + defer func() { + if r := recover(); r != nil { + t.Errorf("Test panicked: %s", r) + } + }() + + // Execute test function + err := func() (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic occurred: %v", r) + } + }() + return keeper.SettleSessionAccounting(wctx, tc.claim) + }() + + // Assert the error + if tc.errExpected { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func baseClaim(appAddr, supplierAddr string, sum uint64) suppliertypes.Claim { + return suppliertypes.Claim{ + SupplierAddress: supplierAddr, + SessionHeader: &sessiontypes.SessionHeader{ + ApplicationAddress: appAddr, + Service: &sharedtypes.Service{ + Id: "svc1", + Name: "svcName1", + }, + SessionStartBlockHeight: 1, + SessionId: "1", + SessionEndBlockHeight: 5, + }, + RootHash: smstRootWithSum(sum), + } +} + +func smstRootWithSum(sum uint64) smt.MerkleRoot { + root := make([]byte, 40) + copy(root[:32], []byte("This is exactly 32 characters!!!")) + binary.BigEndian.PutUint64(root[32:], sum) + return smt.MerkleRoot(root) +} diff --git a/x/tokenomics/simulation/update_params.go b/x/tokenomics/simulation/update_params.go index 70b7599cc..87a1b03da 100644 --- a/x/tokenomics/simulation/update_params.go +++ b/x/tokenomics/simulation/update_params.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/pokt-network/poktroll/x/tokenomics/keeper" "github.com/pokt-network/poktroll/x/tokenomics/types" ) diff --git a/x/tokenomics/types/errors.go b/x/tokenomics/types/errors.go index 441d57847..4e269fac3 100644 --- a/x/tokenomics/types/errors.go +++ b/x/tokenomics/types/errors.go @@ -8,13 +8,19 @@ import ( // x/tokenomics module sentinel errors var ( - ErrTokenomicsAuthorityAddressInvalid = sdkerrors.Register(ModuleName, 1, "the provided authority address is not a valid bech32 address") - ErrTokenomicsAuthorityAddressMismatch = sdkerrors.Register(ModuleName, 2, "the provided authority address does not match the on-chain governance address") - ErrTokenomicsClaimNil = sdkerrors.Register(ModuleName, 3, "provided claim is nil") - ErrTokenomicsSessionHeaderNil = sdkerrors.Register(ModuleName, 4, "provided claim's session header is nil") - ErrTokenomicsSupplierModuleMintFailed = sdkerrors.Register(ModuleName, 5, "failed to mint uPOKT to supplier module account") - ErrTokenomicsSupplierRewardFailed = sdkerrors.Register(ModuleName, 6, "failed to send uPOKT from supplier module account to supplier") - ErrTokenomicsApplicationModuleBurn = sdkerrors.Register(ModuleName, 7, "failed to burn uPOKT from application module account") - ErrTokenomicsApplicationModuleFeeFailed = sdkerrors.Register(ModuleName, 8, "failed to send uPOKT from application module account to application") - ErrTokenomicsParamsInvalid = sdkerrors.Register(ModuleName, 9, "provided params are invalid") + ErrTokenomicsAuthorityAddressInvalid = sdkerrors.Register(ModuleName, 1, "the provided authority address is not a valid bech32 address") + ErrTokenomicsAuthorityAddressMismatch = sdkerrors.Register(ModuleName, 2, "the provided authority address does not match the on-chain governance address") + ErrTokenomicsClaimNil = sdkerrors.Register(ModuleName, 3, "provided claim is nil") + ErrTokenomicsSessionHeaderNil = sdkerrors.Register(ModuleName, 4, "provided claim's session header is nil") + ErrTokenomicsSessionHeaderInvalid = sdkerrors.Register(ModuleName, 5, "provided claim's session header is invalid") + ErrTokenomicsSupplierModuleMintFailed = sdkerrors.Register(ModuleName, 6, "failed to mint uPOKT to supplier module account") + ErrTokenomicsSupplierRewardFailed = sdkerrors.Register(ModuleName, 7, "failed to send uPOKT from supplier module account to supplier") + ErrTokenomicsSupplierAddressInvalid = sdkerrors.Register(ModuleName, 8, "the supplier address in the claim is not a valid bech32 address") + ErrTokenomicsApplicationNotFound = sdkerrors.Register(ModuleName, 9, "application not found") + ErrTokenomicsApplicationModuleBurn = sdkerrors.Register(ModuleName, 10, "failed to burn uPOKT from application module account") + ErrTokenomicsApplicationModuleFeeFailed = sdkerrors.Register(ModuleName, 11, "failed to send uPOKT from application module account to application") + ErrTokenomicsApplicationUndelegationFailed = sdkerrors.Register(ModuleName, 12, "failed to undelegate uPOKT from the application module to the application account") + ErrTokenomicsApplicationAddressInvalid = sdkerrors.Register(ModuleName, 13, "the application address in the claim is not a valid bech32 address") + ErrTokenomicsParamsInvalid = sdkerrors.Register(ModuleName, 14, "provided params are invalid") + ErrTokenomicsRootHashInvalid = sdkerrors.Register(ModuleName, 15, "the root hash in the claim is invalid") ) diff --git a/x/tokenomics/types/expected_keepers.go b/x/tokenomics/types/expected_keepers.go index bd77b8bef..ba3e8ce22 100644 --- a/x/tokenomics/types/expected_keepers.go +++ b/x/tokenomics/types/expected_keepers.go @@ -1,10 +1,13 @@ package types -//go:generate mockgen -destination ../../../testutil/tokenomics/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper +//go:generate mockgen -destination ../../../testutil/tokenomics/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper,ApplicationKeeper,SupplierKeeper import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" + + apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) @@ -18,6 +21,15 @@ type BankKeeper interface { MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error - SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +type ApplicationKeeper interface { + GetApplication(ctx sdk.Context, appAddr string) (app apptypes.Application, found bool) + SetApplication(ctx sdk.Context, app apptypes.Application) +} + +type SupplierKeeper interface { + GetSupplier(ctx sdk.Context, suppAddr string) (supplier sharedtypes.Supplier, found bool) }