Skip to content

Commit

Permalink
Merge pull request #1799 from CosmWasm/setup-discounts
Browse files Browse the repository at this point in the history
Generalize "pinned" to "discount" for cases where contract is in memory
  • Loading branch information
webmaster128 authored Feb 1, 2024
2 parents 796907e + 3206540 commit 5f444cd
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 43 deletions.
2 changes: 1 addition & 1 deletion x/wasm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1861,7 +1861,7 @@ func TestPinnedContractLoops(t *testing.T) {
},
}, 0, nil
}
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(20000))
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(30_000))
require.PanicsWithValue(t, storetypes.ErrorOutOfGas{Descriptor: "ReadFlat"}, func() {
_, err := k.execute(ctx, example.Contract, RandomAccountAddress(t), anyMsg, nil)
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion x/wasm/keeper/relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ func TestOnRecvPacket(t *testing.T) {
},
"submessage reply can overwrite ack data": {
contractAddr: example.Contract,
expContractGas: myContractGas + storageCosts,
expContractGas: types.DefaultInstanceCostDiscount + myContractGas + storageCosts,
contractResp: &wasmvmtypes.IBCReceiveResult{
Ok: &wasmvmtypes.IBCReceiveResponse{
Acknowledgement: []byte("myAck"),
Expand Down
10 changes: 5 additions & 5 deletions x/wasm/keeper/submsg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,14 @@ func TestDispatchSubMsgErrorHandling(t *testing.T) {
"send tokens": {
submsgID: 5,
msg: validBankSend,
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(105000, 106000)},
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(107_000, 108_000)},
},
"not enough tokens": {
submsgID: 6,
msg: invalidBankSend,
subMsgError: true,
// uses less gas than the send tokens (cost of bank transfer)
resultAssertions: []assertion{assertGasUsed(76000, 79000), assertErrorString("codespace: sdk, code: 5")},
resultAssertions: []assertion{assertGasUsed(78_000, 81_000), assertErrorString("codespace: sdk, code: 5")},
},
"out of gas panic with no gas limit": {
submsgID: 7,
Expand All @@ -263,23 +263,23 @@ func TestDispatchSubMsgErrorHandling(t *testing.T) {
msg: validBankSend,
gasLimit: &subGasLimit,
// uses same gas as call without limit (note we do not charge the 40k on reply)
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(105000, 106000)},
resultAssertions: []assertion{assertReturnedEvents(0), assertGasUsed(107_000, 108_000)},
},
"not enough tokens with limit": {
submsgID: 16,
msg: invalidBankSend,
subMsgError: true,
gasLimit: &subGasLimit,
// uses same gas as call without limit (note we do not charge the 40k on reply)
resultAssertions: []assertion{assertGasUsed(77700, 77800), assertErrorString("codespace: sdk, code: 5")},
resultAssertions: []assertion{assertGasUsed(79_700, 79_800), assertErrorString("codespace: sdk, code: 5")},
},
"out of gas caught with gas limit": {
submsgID: 17,
msg: infiniteLoop,
subMsgError: true,
gasLimit: &subGasLimit,
// uses all the subGasLimit, plus the 52k or so for the main contract
resultAssertions: []assertion{assertGasUsed(subGasLimit+73000, subGasLimit+74000), assertErrorString("codespace: sdk, code: 11")},
resultAssertions: []assertion{assertGasUsed(subGasLimit+75_000, subGasLimit+76_000), assertErrorString("codespace: sdk, code: 11")},
},
"instantiate contract gets address in data and events": {
submsgID: 21,
Expand Down
12 changes: 6 additions & 6 deletions x/wasm/keeper/wasmtesting/gas_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
// MockGasRegister mock that implements keeper.GasRegister
type MockGasRegister struct {
CompileCostFn func(byteLength int) storetypes.Gas
SetupContractCostFn func(pinned bool, msgLen int) storetypes.Gas
ReplyCostFn func(pinned bool, reply wasmvmtypes.Reply) storetypes.Gas
SetupContractCostFn func(discount bool, msgLen int) storetypes.Gas
ReplyCostFn func(discount bool, reply wasmvmtypes.Reply) storetypes.Gas
EventCostsFn func(evts []wasmvmtypes.EventAttribute) storetypes.Gas
ToWasmVMGasFn func(source storetypes.Gas) uint64
FromWasmVMGasFn func(source uint64) storetypes.Gas
Expand All @@ -31,18 +31,18 @@ func (m MockGasRegister) UncompressCosts(byteLength int) storetypes.Gas {
return m.UncompressCostsFn(byteLength)
}

func (m MockGasRegister) SetupContractCost(pinned bool, msgLen int) storetypes.Gas {
func (m MockGasRegister) SetupContractCost(discount bool, msgLen int) storetypes.Gas {
if m.SetupContractCostFn == nil {
panic("not expected to be called")
}
return m.SetupContractCostFn(pinned, msgLen)
return m.SetupContractCostFn(discount, msgLen)
}

func (m MockGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) storetypes.Gas {
func (m MockGasRegister) ReplyCosts(discount bool, reply wasmvmtypes.Reply) storetypes.Gas {
if m.ReplyCostFn == nil {
panic("not expected to be called")
}
return m.ReplyCostFn(pinned, reply)
return m.ReplyCostFn(discount, reply)
}

func (m MockGasRegister) EventCosts(evts []wasmvmtypes.EventAttribute, _ wasmvmtypes.Events) storetypes.Gas {
Expand Down
43 changes: 31 additions & 12 deletions x/wasm/types/gas_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const (
// Creating a new instance is costly, and this helps put a recursion limit to contracts calling contracts.
// Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803
DefaultInstanceCost uint64 = 60_000
// DefaultInstanceCostDiscount is charged instead of DefaultInstanceCost for cases where
// we assume the contract is loaded from an in-memory cache.
// For a long time it was implicitly just 0 in those cases.
// Now we use something small that roughly reflects the 45µs startup time (30x cheaper than DefaultInstanceCost).
DefaultInstanceCostDiscount uint64 = 2_000
// DefaultCompileCost is how much SDK gas is charged *per byte* for compiling WASM code.
// Benchmarks and numbers were discussed in: https://github.com/CosmWasm/wasmd/pull/634#issuecomment-938056803
DefaultCompileCost uint64 = 3
Expand Down Expand Up @@ -75,9 +80,9 @@ type GasRegister interface {
UncompressCosts(byteLength int) storetypes.Gas
// SetupContractCost are charged when interacting with a Wasm contract, i.e. every time
// the contract is prepared for execution through any entry point (execute/instantiate/sudo/query/ibc_*/...).
SetupContractCost(pinned bool, msgLen int) storetypes.Gas
SetupContractCost(discount bool, msgLen int) storetypes.Gas
// ReplyCosts costs to to handle a message reply
ReplyCosts(pinned bool, reply wasmvmtypes.Reply) storetypes.Gas
ReplyCosts(discount bool, reply wasmvmtypes.Reply) storetypes.Gas
// EventCosts costs to persist an event
EventCosts(attrs []wasmvmtypes.EventAttribute, events wasmvmtypes.Events) storetypes.Gas
// ToWasmVMGas converts from Cosmos SDK gas units to [CosmWasm gas] (aka. wasmvm gas)
Expand All @@ -92,8 +97,16 @@ type GasRegister interface {

// WasmGasRegisterConfig config type
type WasmGasRegisterConfig struct {
// InstanceCost costs when interacting with a wasm contract
// InstanceCost are charged when interacting with a Wasm contract.
// "Instance" refers to the in-memory Instance of the Wasm runtime, not the contract address on chain.
// InstanceCost are part of a contract's setup cost.
InstanceCost storetypes.Gas
// InstanceCostDiscount is a discounted version of InstanceCost. It is charged whenever
// we can reasonably assume that a contract is in one of the in-memory caches. E.g.
// when the contract is pinned or we send a reply to a contract that was executed before.
// See also https://github.com/CosmWasm/wasmd/issues/1798 for more thinking around
// discount cases.
InstanceCostDiscount storetypes.Gas
// CompileCosts costs to persist and "compile" a new wasm contract
CompileCost storetypes.Gas
// UncompressCost costs per byte to unpack a contract
Expand All @@ -120,6 +133,7 @@ type WasmGasRegisterConfig struct {
func DefaultGasRegisterConfig() WasmGasRegisterConfig {
return WasmGasRegisterConfig{
InstanceCost: DefaultInstanceCost,
InstanceCostDiscount: DefaultInstanceCostDiscount,
CompileCost: DefaultCompileCost,
GasMultiplier: DefaultGasMultiplier,
EventPerAttributeCost: DefaultPerAttributeCost,
Expand Down Expand Up @@ -167,20 +181,25 @@ func (g WasmGasRegister) UncompressCosts(byteLength int) storetypes.Gas {
return g.c.UncompressCost.Mul(uint64(byteLength)).Floor()
}

// SetupContractCost costs when interacting with a wasm contract
func (g WasmGasRegister) SetupContractCost(pinned bool, msgLen int) storetypes.Gas {
// SetupContractCost costs when interacting with a wasm contract.
// Set discount to true in cases where you can reasonably assume the contract
// is loaded from an in-memory cache (e.g. pinned contracts or replys).
func (g WasmGasRegister) SetupContractCost(discount bool, msgLen int) storetypes.Gas {
if msgLen < 0 {
panic(errorsmod.Wrap(ErrInvalid, "negative length"))
}
dataCosts := storetypes.Gas(msgLen) * g.c.ContractMessageDataCost
if pinned {
return dataCosts
dataCost := storetypes.Gas(msgLen) * g.c.ContractMessageDataCost
if discount {
return g.c.InstanceCostDiscount + dataCost
} else {
return g.c.InstanceCost + dataCost
}
return g.c.InstanceCost + dataCosts
}

// ReplyCosts costs to to handle a message reply
func (g WasmGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) storetypes.Gas {
// ReplyCosts costs to to handle a message reply.
// Set discount to true in cases where you can reasonably assume the contract
// is loaded from an in-memory cache (e.g. pinned contracts or replys).
func (g WasmGasRegister) ReplyCosts(discount bool, reply wasmvmtypes.Reply) storetypes.Gas {
var eventGas storetypes.Gas
msgLen := len(reply.Result.Err)
if reply.Result.Ok != nil {
Expand All @@ -193,7 +212,7 @@ func (g WasmGasRegister) ReplyCosts(pinned bool, reply wasmvmtypes.Reply) storet
// apply free tier on the whole set not per event
eventGas += g.EventCosts(attrs, nil)
}
return eventGas + g.SetupContractCost(pinned, msgLen)
return eventGas + g.SetupContractCost(discount, msgLen)
}

// EventCosts costs to persist an event
Expand Down
35 changes: 17 additions & 18 deletions x/wasm/types/gas_register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,19 @@ func TestSetupContractCost(t *testing.T) {
srcLen: 1,
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: DefaultContractMessageDataCost,
exp: DefaultInstanceCostDiscount + DefaultContractMessageDataCost,
},
"big msg - pinned": {
srcLen: math.MaxUint32,
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: DefaultContractMessageDataCost * math.MaxUint32,
exp: DefaultInstanceCostDiscount + DefaultContractMessageDataCost*math.MaxUint32,
},
"empty msg - pinned": {
srcLen: 0,
pinned: true,
srcConfig: DefaultGasRegisterConfig(),
exp: storetypes.Gas(0),
exp: DefaultInstanceCostDiscount,
},
"small msg - unpinned": {
srcLen: 1,
Expand All @@ -89,7 +89,6 @@ func TestSetupContractCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost,
},

"negative len": {
srcLen: -1,
srcConfig: DefaultGasRegisterConfig(),
Expand Down Expand Up @@ -118,7 +117,7 @@ func TestReplyCost(t *testing.T) {
exp storetypes.Gas
expPanic bool
}{
"subcall response with events and data - pinned": {
"submessage reply with events and data - pinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -131,9 +130,9 @@ func TestReplyCost(t *testing.T) {
},
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost, // 3 == len("foo")
exp: DefaultInstanceCostDiscount + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost, // 3 == len("foo")
},
"subcall response with events - pinned": {
"submessage reply with events - pinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -145,9 +144,9 @@ func TestReplyCost(t *testing.T) {
},
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost, // 3 == len("foo")
exp: DefaultInstanceCostDiscount + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost, // 3 == len("foo")
},
"subcall response with events exceeds free tier- pinned": {
"submessage reply with events exceeds free tier - pinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -159,19 +158,19 @@ func TestReplyCost(t *testing.T) {
},
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: (3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost, // 3 == len("foo"), 6 == len("myData")
exp: DefaultInstanceCostDiscount + (3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost, // 3 == len("foo"), 6 == len("myData")
},
"subcall response error - pinned": {
"submessage reply error - pinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Err: "foo",
},
},
srcConfig: DefaultGasRegisterConfig(),
pinned: true,
exp: 3 * DefaultContractMessageDataCost,
exp: DefaultInstanceCostDiscount + 3*DefaultContractMessageDataCost,
},
"subcall response with events and data - unpinned": {
"submessage reply with events and data - unpinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -185,7 +184,7 @@ func TestReplyCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost + DefaultContractMessageDataCost,
},
"subcall response with events - unpinned": {
"submessage reply with events - unpinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -198,7 +197,7 @@ func TestReplyCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost + 3*DefaultEventAttributeDataCost + DefaultPerAttributeCost,
},
"subcall response with events exceeds free tier- unpinned": {
"submessage reply with events exceeds free tier - unpinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -211,7 +210,7 @@ func TestReplyCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost + (3+6)*DefaultEventAttributeDataCost + DefaultPerAttributeCost, // 3 == len("foo"), 6 == len("myData")
},
"subcall response error - unpinned": {
"submessage reply error - unpinned": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Err: "foo",
Expand All @@ -220,7 +219,7 @@ func TestReplyCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost + 3*DefaultContractMessageDataCost,
},
"subcall response with empty events": {
"submessage reply with empty events": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{
Expand All @@ -231,7 +230,7 @@ func TestReplyCost(t *testing.T) {
srcConfig: DefaultGasRegisterConfig(),
exp: DefaultInstanceCost,
},
"subcall response with events unset": {
"submessage reply with events unset": {
src: wasmvmtypes.Reply{
Result: wasmvmtypes.SubMsgResult{
Ok: &wasmvmtypes.SubMsgResponse{},
Expand Down

0 comments on commit 5f444cd

Please sign in to comment.