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

Implement contract state rent #32

wants to merge 2 commits into from

Conversation

codchen
Copy link
Collaborator

@codchen codchen commented Jun 26, 2023

Summary

Implement duration-based fee (rent) based on https://docs.google.com/document/d/1FbbV3vQZnyM1xgZF-ug7zNCVlt_3Z9aD7Hng2OIveX0/edit

Specifically, each contract will have a counter tracking the number of bytes it owns as well as the amount of usei people have deposited for it as rent. Each time a write operation happens to a contract (i.e. execute/sudo/migrate/reply), it will settle the rent charges since the last write. For example, if a contract was last written at block height 15 with a post-write state size of 500 bytes, and it's now written again at block height 20, it would need to pay (20 - 15) * 500 * rent price out of its rent balance. If the balance is insufficient, the write would fail. rent price is a wasm module param that can be updated via gov proposal. Anyone can deposit rent for any contract via the DepositRent transaction type. Note that deposited rent cannot be withdrawn.

One thing to note is that in order to track the size of a contract, we need to do an additional query before each Set and Delete to update the size properly.

Future steps:

  • Add exemption mechanism (so that the first X bytes of a contract is exempted from rent charge)
  • Rent withdrawal mechanism (tbd who is entitled to the unused rent)

Testing

x/wasm/keeper/keeper.go Fixed Show fixed Hide fixed
@codchen codchen force-pushed the tony-chen-rent-1 branch 2 times, most recently from 58c85e8 to 3d5783c Compare June 26, 2023 16:11
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

}
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

// 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.

// 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

gas := k.runtimeGasForContract(ctx)
res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, &prefixStore, cosmwasmAPI, &querier, k.gasMeter(ctx), gas, costJSONDeserialization)
res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, sizedPrefixStore, cosmwasmAPI, &querier, k.gasMeter(ctx), gas, costJSONDeserialization)
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 ok that we're no longer passing in the pointer (see &prefixStore) (instead passing sized store)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sizedPrefixStore is a pointer

rentInfo.Balance -= rentToBeCharged
}
rentInfo.LastChargedBlock = uint64(ctx.BlockHeight())
k.setRentInfo(ctx, contractAddress, rentInfo)
Copy link
Contributor

Choose a reason for hiding this comment

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

should we put this after the error check for slight optimization (then the write doesnt have to occur to just rollback if the balance is negative)

Copy link
Collaborator Author

@codchen codchen Jul 3, 2023

Choose a reason for hiding this comment

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

hm good point i'm doing it wrong here now that you mentioned rollback. My intention was to persist the negative balance so that the contract owner would still be responsible for the accrued rent for his state since the last time rent was charged even if the most recent charge attempt results in out of rent, but forgot it wouldn't persist due to the rollback. We might need to do something like:

DeliverTx -> branch cachekv
  WASM execution -> branch of branch of cachekv
  Charge rent
     -> if success, commits branch of branch
     -> if out of rent, revert branch of branch
DeliverTX -> branch cachekv commits/aborts regardless of out of rent or not

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

actually a better way would be to place the rent deduction logic into ante handler and the check in msgServer, so that we leverage the existing rollback infra instead of throwing in yet another layer

}
rentInfo.LastChargedBlock = uint64(ctx.BlockHeight())
k.setRentInfo(ctx, contractAddress, rentInfo)
if rentInfo.Balance < 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

just to confirm, the expected behavior here is that if a contract has insufficient duration rent, then the contract is borked until the deposit rent tx is done, right? and this would result in any executes, migrates, etc to fail

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah that's correct

@@ -1081,6 +1215,18 @@ func (c BankCoinTransferrer) TransferCoins(parentCtx sdk.Context, fromAddr sdk.A
return nil
}

func (c BankCoinTransferrer) TransferCoinsToModule(ctx sdk.Context, fromAddr sdk.AccAddress, recipientModule string, amount sdk.Coins) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, we may want to use the deferred send to module for this if we expect a lot of these action to occur (assuming this only happens on deposit rent, so maybe its infrequent enough as to not degrade concurrency, but if it becomes problematic we can make this a concurrent safe process)

assert.Equal(t, uint64(ctx.BlockHeight()), rentInfo.LastChargedBlock)

// bump block height
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add a test case for two txs in the same block that modify state size each?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah good call

"errors"
)

// custom struct instead of protobuf to minimize state needed
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just use protobuf for consistency with other store objects?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i was thinking that protobuf might be too heavy for such a simple struct but yeah i guess it probably won't be that bad. Better keep it consistent. Will change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants