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

Implement contract state rent #32

Open
wants to merge 2 commits into
base: main
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
25 changes: 25 additions & 0 deletions proto/cosmwasm/wasm/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ service Query {
rpc PinnedCodes(QueryPinnedCodesRequest) returns (QueryPinnedCodesResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/codes/pinned";
}

rpc RentInfo(QueryRentInfoRequest) returns (QueryRentInfoResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/rent/{address}";
}

rpc StateSize(QueryStateSizeRequest) returns (QueryStateSizeResponse) {
option (google.api.http).get = "/cosmwasm/wasm/v1/size/{address}";
}
}

// QueryContractInfoRequest is the request type for the Query/ContractInfo RPC
Expand Down Expand Up @@ -222,3 +230,20 @@ message QueryPinnedCodesResponse {
// pagination defines the pagination in the response.
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

message QueryRentInfoRequest {
string address = 1;
}

message QueryRentInfoResponse {
int64 balance = 1;
uint64 last_charged_block = 2;
}

message QueryStateSizeRequest {
string address = 1;
}

message QueryStateSizeResponse {
uint64 state_size = 1;
}
10 changes: 10 additions & 0 deletions proto/cosmwasm/wasm/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ service Msg {
rpc UpdateAdmin(MsgUpdateAdmin) returns (MsgUpdateAdminResponse);
// ClearAdmin removes any admin stored for a smart contract
rpc ClearAdmin(MsgClearAdmin) returns (MsgClearAdminResponse);
// DepositRent adds to the rent balance for a smart contract
rpc DepositRent(MsgDepositRent) returns (MsgDepositRentResponse);
}

// MsgStoreCode submit Wasm code to the system
Expand Down Expand Up @@ -133,3 +135,11 @@ message MsgClearAdmin {

// MsgClearAdminResponse returns empty data
message MsgClearAdminResponse {}

message MsgDepositRent {
string contract = 1;
uint64 amount = 2;
string sender = 3;
}

message MsgDepositRentResponse {}
9 changes: 9 additions & 0 deletions proto/cosmwasm/wasm/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ message Params {
];
AccessType instantiate_default_permission = 2
[ (gogoproto.moretags) = "yaml:\"instantiate_default_permission\"" ];
// rent price per byte per block
string unit_rent_price = 3
[
(gogoproto.moretags) = "yaml:\"unit_gas_price\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
Copy link
Contributor

Choose a reason for hiding this comment

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

i imagine this would be such a small value, would it be more worth it to make this param per 10kb per block or something and then have the unit price represented as an int (similar to how the wasm vm gas multiplier is represented)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we can do price per 10kb or more but I'd prefer keeping the type as dec for flexibility just in case in the future we want to decrease price by a lot

(gogoproto.nullable) = false
];
string rent_denom = 4
[ (gogoproto.moretags) = "yaml:\"rent_denom\"" ];
}

// CodeInfo is data for the uploaded contract WASM code
Expand Down
93 changes: 93 additions & 0 deletions store/sized/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package sized

import (
"io"

"github.com/cosmos/cosmos-sdk/store/types"
)

var _ types.KVStore = &Store{}

type Store struct {
parent types.KVStore
sizeChange int64
}

func NewStore(parent types.KVStore) *Store {
return &Store{parent: parent}
}
func (store *Store) GetWorkingHash() ([]byte, error) {
return store.parent.GetWorkingHash()
}

func (s *Store) GetSizeChanged() int64 {
return s.sizeChange
}

func (s *Store) Get(key []byte) []byte {
value := s.parent.Get(key)
return value
}

func (s *Store) Set(key []byte, value []byte) {
oldValue := s.Get(key)
if oldValue != nil {
// reduce size due to overwrite
s.sizeChange -= int64(len(key))
s.sizeChange -= int64(len(oldValue))
}
s.parent.Set(key, value)
if key != nil {
s.sizeChange += int64(len(key))
Copy link
Contributor

Choose a reason for hiding this comment

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

do we care about key size? i thought the limiting factor on performance for keys was key cardinality due to tree traversal operational time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we care about key size for the storage space it consumes

}
if value != nil {
s.sizeChange += int64(len(value))
}
}

func (s *Store) Delete(key []byte) {
// has to perform a read here to know the size change
value := s.Get(key)
s.parent.Delete(key)
if value != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible for a key to be set with a nil value (such that the key existed previously). in this case, we would still need to decrement for keyLen bytes, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

// only reduce size if the key used to have a value
s.sizeChange -= int64(len(key))
s.sizeChange -= int64(len(value))
}
}

func (s *Store) Has(key []byte) bool {
return s.parent.Has(key)
}

func (s *Store) Iterator(start, end []byte) types.Iterator {
return s.parent.Iterator(start, end)
}

func (s *Store) ReverseIterator(start, end []byte) types.Iterator {
return s.parent.ReverseIterator(start, end)
}

// GetStoreType implements the KVStore interface. It returns the underlying
// KVStore type.
func (s *Store) GetStoreType() types.StoreType {
return s.parent.GetStoreType()
}

// CacheWrap implements the KVStore interface. It panics as a Store
// cannot be cache wrapped.
func (s *Store) CacheWrap(_ types.StoreKey) types.CacheWrap {
panic("cannot CacheWrap a ListenKVStore")
Copy link
Contributor

Choose a reason for hiding this comment

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

just curious, what is the normal behavior of cachewrap, and why do we want it to panic here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's used for store types that need the ability the "branch out" on that particular inheritance level. Unfortunately a lot of functions' parameter are using the interface that includes these methods. Panicing for wrapper store types that don't need branching at their inheritance levels seem to be common practice for types like gaskv/listenkv/tracekv

}

// CacheWrapWithTrace implements the KVStore interface. It panics as a
// Store cannot be cache wrapped.
func (s *Store) CacheWrapWithTrace(_ types.StoreKey, _ io.Writer, _ types.TraceContext) types.CacheWrap {
panic("cannot CacheWrapWithTrace a ListenKVStore")
}

// CacheWrapWithListeners implements the KVStore interface. It panics as a
// Store cannot be cache wrapped.
func (s *Store) CacheWrapWithListeners(_ types.StoreKey, _ []types.WriteListener) types.CacheWrap {
panic("cannot CacheWrapWithListeners a ListenKVStore")
}
2 changes: 2 additions & 0 deletions x/wasm/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ type (
MsgClearAdmin = types.MsgClearAdmin
MsgWasmIBCCall = types.MsgIBCSend
MsgClearAdminResponse = types.MsgClearAdminResponse
MsgDepositRent = types.MsgDepositRent
MsgDepositRentResponse = types.MsgDepositRentResponse
MsgServer = types.MsgServer
Model = types.Model
CodeInfo = types.CodeInfo
Expand Down
32 changes: 32 additions & 0 deletions x/wasm/client/cli/new_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,35 @@ func ClearContractAdminCmd() *cobra.Command {
flags.AddTxFlagsToCmd(cmd)
return cmd
}

func DepositRentCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "deposit-rent [contract_addr_bech32] [amount]",
Short: "deposits rent for a contract",
Aliases: []string{"deposit"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

amount, err := strconv.ParseUint(args[1], 10, 64)
if err != nil {
return err
}

msg := types.MsgDepositRent{
Sender: clientCtx.GetFromAddress().String(),
Contract: args[0],
Amount: amount,
}
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
74 changes: 74 additions & 0 deletions x/wasm/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func GetQueryCmd() *cobra.Command {
GetCmdGetContractState(),
GetCmdListPinnedCode(),
GetCmdLibVersion(),
GetCmdGetRentInfo(),
GetCmdGetStateSize(),
)
return queryCmd
}
Expand Down Expand Up @@ -481,6 +483,78 @@ func GetCmdListPinnedCode() *cobra.Command {
return cmd
}

func GetCmdGetRentInfo() *cobra.Command {
cmd := &cobra.Command{
Use: "rent-info [bech32_address]",
Short: "Gets the rent info for a contract given its address",
Long: "Gets the rent info for a contract given its address",
Aliases: []string{"rent"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

_, err = sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.RentInfo(
context.Background(),
&types.QueryRentInfoRequest{
Address: args[0],
},
)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}

func GetCmdGetStateSize() *cobra.Command {
cmd := &cobra.Command{
Use: "state-size [bech32_address]",
Short: "Gets total number of bytes for a contract state given its address",
Long: "Gets total number of bytes for a contract state given its address",
Aliases: []string{"ss"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

_, err = sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)
res, err := queryClient.StateSize(
context.Background(),
&types.QueryStateSizeRequest{
Address: args[0],
},
)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}

type argumentDecoder struct {
// dec is the default decoder
dec func(string) ([]byte, error)
Expand Down
1 change: 1 addition & 0 deletions x/wasm/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func GetTxCmd() *cobra.Command {
MigrateContractCmd(),
UpdateContractAdminCmd(),
ClearContractAdminCmd(),
DepositRentCmd(),
)
return txCmd
}
Expand Down
2 changes: 2 additions & 0 deletions x/wasm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func NewHandler(k types.ContractOpsKeeper) sdk.Handler {
res, err = msgServer.UpdateAdmin(sdk.WrapSDKContext(ctx), msg)
case *MsgClearAdmin:
res, err = msgServer.ClearAdmin(sdk.WrapSDKContext(ctx), msg)
case *MsgDepositRent:
res, err = msgServer.DepositRent(sdk.WrapSDKContext(ctx), msg)
default:
errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg)
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg)
Expand Down
5 changes: 5 additions & 0 deletions x/wasm/keeper/contract_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type decoratedKeeper interface {
Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte) ([]byte, error)
setContractInfoExtension(ctx sdk.Context, contract sdk.AccAddress, extra types.ContractInfoExtension) error
setAccessConfig(ctx sdk.Context, codeID uint64, config types.AccessConfig) error
DepositRent(ctx sdk.Context, contractAddress sdk.AccAddress, senderAddress sdk.AccAddress, amount int64) error
}

type PermissionedKeeper struct {
Expand Down Expand Up @@ -84,3 +85,7 @@ func (p PermissionedKeeper) SetContractInfoExtension(ctx sdk.Context, contract s
func (p PermissionedKeeper) SetAccessConfig(ctx sdk.Context, codeID uint64, config types.AccessConfig) error {
return p.nested.setAccessConfig(ctx, codeID, config)
}

func (p PermissionedKeeper) DepositRent(ctx sdk.Context, contractAddress sdk.AccAddress, senderAddress sdk.AccAddress, amount int64) error {
return p.nested.DepositRent(ctx, contractAddress, senderAddress, amount)
}
Loading