diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 90761a0fc..ee64dc5b9 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -76277,6 +76277,8 @@ definitions: type: object pocket.supplier.MsgStakeSupplierResponse: type: object + pocket.supplier.MsgSubmitProofResponse: + type: object pocket.supplier.MsgUnstakeSupplierResponse: type: object pocket.supplier.Params: diff --git a/proto/pocket/supplier/tx.proto b/proto/pocket/supplier/tx.proto index 729632b40..1c92d1650 100644 --- a/proto/pocket/supplier/tx.proto +++ b/proto/pocket/supplier/tx.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package pocket.supplier; +import "cosmos_proto/cosmos.proto"; +import "cosmos/msg/v1/msg.proto"; import "pocket/session/session.proto"; option go_package = "pocket/x/supplier/types"; @@ -11,6 +13,7 @@ service Msg { rpc StakeSupplier (MsgStakeSupplier ) returns (MsgStakeSupplierResponse ); rpc UnstakeSupplier (MsgUnstakeSupplier) returns (MsgUnstakeSupplierResponse); rpc CreateClaim (MsgCreateClaim ) returns (MsgCreateClaimResponse ); + rpc SubmitProof (MsgSubmitProof ) returns (MsgSubmitProofResponse ); } message MsgUnstakeSupplier { string address = 1; @@ -25,10 +28,26 @@ message MsgUnstakeSupplierResponse {} message MsgStakeSupplierResponse {} message MsgCreateClaim { - string supplier_address = 1; + option (cosmos.msg.v1.signer) = "supplier_address"; + + string supplier_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; pocket.session.SessionHeader session_header = 2; + // root returned from smt.SMST#Root() bytes root_hash = 3; } message MsgCreateClaimResponse {} + + +message MsgSubmitProof { + option (cosmos.msg.v1.signer) = "supplier_address"; + + string supplier_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + pocket.session.SessionHeader session_header = 2; + // serialized version of *smt.SparseMerkleClosestProof + bytes proof = 3; +} + +message MsgSubmitProofResponse {} + diff --git a/x/supplier/client/cli/tx.go b/x/supplier/client/cli/tx.go index 4d4e6c722..c1059e51d 100644 --- a/x/supplier/client/cli/tx.go +++ b/x/supplier/client/cli/tx.go @@ -32,6 +32,7 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(CmdStakeSupplier()) cmd.AddCommand(CmdUnstakeSupplier()) cmd.AddCommand(CmdCreateClaim()) + cmd.AddCommand(CmdSubmitProof()) // this line is used by starport scaffolding # 1 return cmd diff --git a/x/supplier/client/cli/tx_submit_proof.go b/x/supplier/client/cli/tx_submit_proof.go new file mode 100644 index 000000000..798d67492 --- /dev/null +++ b/x/supplier/client/cli/tx_submit_proof.go @@ -0,0 +1,57 @@ +package cli + +import ( + "encoding/base64" + "strconv" + + "encoding/json" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/spf13/cobra" + + sessiontypes "pocket/x/session/types" + "pocket/x/supplier/types" +) + +var _ = strconv.Itoa(0) + +// TODO(@bryanchriswhite): Add unit tests for the CLI command when implementing the business logic. + +func CmdSubmitProof() *cobra.Command { + cmd := &cobra.Command{ + Use: "submit-proof [session-header] [proof-base64]", + Short: "Broadcast message submit-proof", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + argSessionHeader := new(sessiontypes.SessionHeader) + err = json.Unmarshal([]byte(args[0]), argSessionHeader) + if err != nil { + return err + } + argSmstProof, err := base64.StdEncoding.DecodeString(args[1]) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := types.NewMsgSubmitProof( + clientCtx.GetFromAddress().String(), + argSessionHeader, + argSmstProof, + ) + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/supplier/keeper/msg_server_create_claim.go b/x/supplier/keeper/msg_server_create_claim.go index abb92c2ef..5958a4068 100644 --- a/x/supplier/keeper/msg_server_create_claim.go +++ b/x/supplier/keeper/msg_server_create_claim.go @@ -16,12 +16,12 @@ func (k msgServer) CreateClaim(goCtx context.Context, msg *types.MsgCreateClaim) ## Validation ### Session validation - 1. [ ] claimed session ID == retrieved session ID + 1. [ ] claimed session ID matches on-chain session ID 2. [ ] this supplier is in the session's suppliers list ### Msg distribution validation (depends on session validation) - 1. [ ] pseudo-randomize earliest block offset - 2. [ ] governance-based earliest block offset + 1. [ ] governance-based earliest block offset + 2. [ ] pseudo-randomize earliest block offset ### Claim validation 1. [ ] session validation diff --git a/x/supplier/keeper/msg_server_submit_proof.go b/x/supplier/keeper/msg_server_submit_proof.go new file mode 100644 index 000000000..cf44efc17 --- /dev/null +++ b/x/supplier/keeper/msg_server_submit_proof.go @@ -0,0 +1,50 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "pocket/x/supplier/types" +) + +func (k msgServer) SubmitProof(goCtx context.Context, msg *types.MsgSubmitProof) (*types.MsgSubmitProofResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + /* + INCOMPLETE: Handling the message + + ## Validation + + ### Session validation + 1. [ ] claimed session ID == retrieved session ID + 2. [ ] this supplier is in the session's suppliers list + 3. [ ] proof signer addr == session application addr + + ### Msg distribution validation (depends on session validation) + 1. [ ] pseudo-randomize earliest block offset + 2. [ ] governance-based earliest block offset + + ### Proof validation + 1. [ ] session validation + 2. [ ] msg distribution validation + 3. [ ] claim with matching session ID exists + 4. [ ] proof path matches last committed block hash at claim height - 1 + 5. [ ] proof validates with claimed root hash + + ## Persistence + 1. [ ] submit proof message + - supplier address + - session header + - proof + + ## Accounting + 1. [ ] extract work done from root hash + 2. [ ] calculate reward/burn token with governance-based multiplier + 3. [ ] reward supplier + 4. [ ] burn application tokens + */ + + _ = ctx + + return &types.MsgSubmitProofResponse{}, nil +} diff --git a/x/supplier/module_simulation.go b/x/supplier/module_simulation.go index 4e69a7c08..6f0e01ba5 100644 --- a/x/supplier/module_simulation.go +++ b/x/supplier/module_simulation.go @@ -36,6 +36,10 @@ const ( // TODO: Determine the simulation weight value defaultWeightMsgCreateClaim int = 100 + opWeightMsgSubmitProof = "op_weight_msg_submit_proof" + // TODO: Determine the simulation weight value + defaultWeightMsgSubmitProof int = 100 + // this line is used by starport scaffolding # simapp/module/const ) @@ -97,6 +101,17 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp suppliersimulation.SimulateMsgCreateClaim(am.accountKeeper, am.bankKeeper, am.keeper), )) + var weightMsgSubmitProof int + simState.AppParams.GetOrGenerate(simState.Cdc, opWeightMsgSubmitProof, &weightMsgSubmitProof, nil, + func(_ *rand.Rand) { + weightMsgSubmitProof = defaultWeightMsgSubmitProof + }, + ) + operations = append(operations, simulation.NewWeightedOperation( + weightMsgSubmitProof, + suppliersimulation.SimulateMsgSubmitProof(am.accountKeeper, am.bankKeeper, am.keeper), + )) + // this line is used by starport scaffolding # simapp/module/operation return operations @@ -129,6 +144,14 @@ func (am AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.Wei return nil }, ), + simulation.NewWeightedProposalMsg( + opWeightMsgSubmitProof, + defaultWeightMsgSubmitProof, + func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) sdk.Msg { + suppliersimulation.SimulateMsgSubmitProof(am.accountKeeper, am.bankKeeper, am.keeper) + return nil + }, + ), // this line is used by starport scaffolding # simapp/module/OpMsg } } diff --git a/x/supplier/simulation/submit_proof.go b/x/supplier/simulation/submit_proof.go new file mode 100644 index 000000000..8996997d6 --- /dev/null +++ b/x/supplier/simulation/submit_proof.go @@ -0,0 +1,29 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "pocket/x/supplier/keeper" + "pocket/x/supplier/types" +) + +func SimulateMsgSubmitProof( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + msg := &types.MsgSubmitProof{ + SupplierAddress: simAccount.Address.String(), + } + + // TODO: Handling the SubmitProof simulation + + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "SubmitProof simulation not implemented"), nil, nil + } +} diff --git a/x/supplier/types/codec.go b/x/supplier/types/codec.go index 39914d30b..3bb7fcd12 100644 --- a/x/supplier/types/codec.go +++ b/x/supplier/types/codec.go @@ -11,6 +11,7 @@ func RegisterCodec(cdc *codec.LegacyAmino) { cdc.RegisterConcrete(&MsgStakeSupplier{}, "supplier/StakeSupplier", nil) cdc.RegisterConcrete(&MsgUnstakeSupplier{}, "supplier/UnstakeSupplier", nil) cdc.RegisterConcrete(&MsgCreateClaim{}, "supplier/CreateClaim", nil) + cdc.RegisterConcrete(&MsgSubmitProof{}, "supplier/SubmitProof", nil) // this line is used by starport scaffolding # 2 } @@ -22,6 +23,9 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { registry.RegisterImplementations((*sdk.Msg)(nil), &MsgCreateClaim{}, ) + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgSubmitProof{}, + ) // this line is used by starport scaffolding # 3 msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) diff --git a/x/supplier/types/message_submit_proof.go b/x/supplier/types/message_submit_proof.go new file mode 100644 index 000000000..ee2dec47a --- /dev/null +++ b/x/supplier/types/message_submit_proof.go @@ -0,0 +1,49 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + sessiontypes "pocket/x/session/types" +) + +const TypeMsgSubmitProof = "submit_proof" + +var _ sdk.Msg = &MsgSubmitProof{} + +func NewMsgSubmitProof(supplierAddress string, sessionHeader *sessiontypes.SessionHeader, proof []byte) *MsgSubmitProof { + return &MsgSubmitProof{ + SupplierAddress: supplierAddress, + SessionHeader: sessionHeader, + Proof: proof, + } +} + +func (msg *MsgSubmitProof) Route() string { + return RouterKey +} + +func (msg *MsgSubmitProof) Type() string { + return TypeMsgSubmitProof +} + +func (msg *MsgSubmitProof) GetSigners() []sdk.AccAddress { + supplierAddress, err := sdk.AccAddressFromBech32(msg.SupplierAddress) + if err != nil { + panic(err) + } + return []sdk.AccAddress{supplierAddress} +} + +func (msg *MsgSubmitProof) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +func (msg *MsgSubmitProof) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.SupplierAddress) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid supplierAddress address (%s)", err) + } + return nil +} diff --git a/x/supplier/types/message_submit_proof_test.go b/x/supplier/types/message_submit_proof_test.go new file mode 100644 index 000000000..7f7a15862 --- /dev/null +++ b/x/supplier/types/message_submit_proof_test.go @@ -0,0 +1,42 @@ +package types + +import ( + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" + "pocket/testutil/sample" +) + +// TODO(@bryanchriswhite): Add unit tests for message validation when adding the business logic. + +func TestMsgSubmitProof_ValidateBasic(t *testing.T) { + tests := []struct { + name string + msg MsgSubmitProof + err error + }{ + { + name: "invalid address", + msg: MsgSubmitProof{ + SupplierAddress: "invalid_address", + }, + err: sdkerrors.ErrInvalidAddress, + }, { + name: "valid address", + msg: MsgSubmitProof{ + SupplierAddress: sample.AccAddress(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.msg.ValidateBasic() + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + }) + } +}