diff --git a/x/wasm/keeper/keeper_test.go b/x/wasm/keeper/keeper_test.go index dfb7e3e45c..d81ee793c4 100644 --- a/x/wasm/keeper/keeper_test.go +++ b/x/wasm/keeper/keeper_test.go @@ -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) diff --git a/x/wasm/keeper/relay_test.go b/x/wasm/keeper/relay_test.go index 812d94fd8a..4008fd7bfa 100644 --- a/x/wasm/keeper/relay_test.go +++ b/x/wasm/keeper/relay_test.go @@ -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"), diff --git a/x/wasm/keeper/submsg_test.go b/x/wasm/keeper/submsg_test.go index 5f8c4932c6..abb9088ead 100644 --- a/x/wasm/keeper/submsg_test.go +++ b/x/wasm/keeper/submsg_test.go @@ -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, @@ -263,7 +263,7 @@ 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, @@ -271,7 +271,7 @@ func TestDispatchSubMsgErrorHandling(t *testing.T) { 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, @@ -279,7 +279,7 @@ func TestDispatchSubMsgErrorHandling(t *testing.T) { 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, diff --git a/x/wasm/keeper/wasmtesting/gas_register.go b/x/wasm/keeper/wasmtesting/gas_register.go index 8cfb2044d6..fd7abe5491 100644 --- a/x/wasm/keeper/wasmtesting/gas_register.go +++ b/x/wasm/keeper/wasmtesting/gas_register.go @@ -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 @@ -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 { diff --git a/x/wasm/types/gas_register.go b/x/wasm/types/gas_register.go index d388dfb0d3..0011d64163 100644 --- a/x/wasm/types/gas_register.go +++ b/x/wasm/types/gas_register.go @@ -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 @@ -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) @@ -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 @@ -120,6 +133,7 @@ type WasmGasRegisterConfig struct { func DefaultGasRegisterConfig() WasmGasRegisterConfig { return WasmGasRegisterConfig{ InstanceCost: DefaultInstanceCost, + InstanceCostDiscount: DefaultInstanceCostDiscount, CompileCost: DefaultCompileCost, GasMultiplier: DefaultGasMultiplier, EventPerAttributeCost: DefaultPerAttributeCost, @@ -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 { @@ -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 diff --git a/x/wasm/types/gas_register_test.go b/x/wasm/types/gas_register_test.go index e087a0a7fa..da7d123a9a 100644 --- a/x/wasm/types/gas_register_test.go +++ b/x/wasm/types/gas_register_test.go @@ -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, @@ -89,7 +89,6 @@ func TestSetupContractCost(t *testing.T) { srcConfig: DefaultGasRegisterConfig(), exp: DefaultInstanceCost, }, - "negative len": { srcLen: -1, srcConfig: DefaultGasRegisterConfig(), @@ -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{ @@ -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{ @@ -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{ @@ -159,9 +158,9 @@ 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", @@ -169,9 +168,9 @@ func TestReplyCost(t *testing.T) { }, 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{ @@ -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{ @@ -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{ @@ -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", @@ -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{ @@ -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{},