Skip to content

Commit

Permalink
Merge pull request cosmos#533 from CosmWasm/gas_525
Browse files Browse the repository at this point in the history
Add cost and api cost options
  • Loading branch information
alpe authored Jun 11, 2021
2 parents 5252f22 + 7f5200b commit e7526cc
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 91 deletions.
1 change: 0 additions & 1 deletion x/wasm/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const (
ProposalTypeMigrateContract = types.ProposalTypeMigrateContract
ProposalTypeUpdateAdmin = types.ProposalTypeUpdateAdmin
ProposalTypeClearAdmin = types.ProposalTypeClearAdmin
GasMultiplier = keeper.GasMultiplier
QueryListContractByCode = keeper.QueryListContractByCode
QueryGetContract = keeper.QueryGetContract
QueryGetContractState = keeper.QueryGetContractState
Expand Down
17 changes: 12 additions & 5 deletions x/wasm/keeper/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,28 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

const (
// DefaultGasCostHumanAddress is how moch SDK gas we charge to convert to a human address format
DefaultGasCostHumanAddress = 5
// DefaultGasCostCanonicalAddress is how moch SDK gas we charge to convert to a canonical address format
DefaultGasCostCanonicalAddress = 4
)

var (
CostHumanize = 5 * GasMultiplier
CostCanonical = 4 * GasMultiplier
costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier
costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier
)

func humanAddress(canon []byte) (string, uint64, error) {
if len(canon) != sdk.AddrLen {
return "", CostHumanize, fmt.Errorf("Expected %d byte address", sdk.AddrLen)
return "", costHumanize, fmt.Errorf("Expected %d byte address", sdk.AddrLen)
}
return sdk.AccAddress(canon).String(), CostHumanize, nil
return sdk.AccAddress(canon).String(), costHumanize, nil
}

func canonicalAddress(human string) ([]byte, uint64, error) {
bz, err := sdk.AccAddressFromBech32(human)
return bz, CostCanonical, err
return bz, costCanonical, err
}

var cosmwasmAPI = wasmvm.GoAPI{
Expand Down
101 changes: 57 additions & 44 deletions x/wasm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ import (
"time"
)

// GasMultiplier is how many cosmwasm gas points = 1 sdk gas point
// DefaultGasMultiplier is how many cosmwasm gas points = 1 sdk gas point
// SDK reference costs can be found here: https://github.com/cosmos/cosmos-sdk/blob/02c6c9fafd58da88550ab4d7d494724a477c8a68/store/types/gas.go#L153-L164
// A write at ~3000 gas and ~200us = 10 gas per us (microsecond) cpu/io
// Rough timing have 88k gas at 90us, which is equal to 1k sdk gas... (one read)
//
// Please note that all gas prices returned to the wasmer engine should have this multiplied
const GasMultiplier uint64 = 100
const DefaultGasMultiplier uint64 = 100

// InstanceCost is how much SDK gas we charge each time we load a WASM instance.
// DefaultInstanceCost is how much SDK gas we charge each time we load a WASM instance.
// Creating a new instance is costly, and this helps put a recursion limit to contracts calling contracts.
const InstanceCost uint64 = 40_000
const DefaultInstanceCost uint64 = 40_000

// CompileCost is how much SDK gas we charge *per byte* for compiling WASM code.
const CompileCost uint64 = 2
// DefaultCompileCost is how much SDK gas we charge *per byte* for compiling WASM code.
const DefaultCompileCost uint64 = 2

// contractMemoryLimit is the memory limit of each contract execution (in MiB)
// constant value so all nodes run with the same limit.
Expand Down Expand Up @@ -83,6 +83,9 @@ type Keeper struct {
// queryGasLimit is the max wasmvm gas that can be spent on executing a query with a contract
queryGasLimit uint64
paramSpace paramtypes.Subspace
instanceCost uint64
compileCost uint64
gasMultiplier uint64
}

// NewKeeper creates a new contract Keeper instance
Expand Down Expand Up @@ -126,6 +129,9 @@ func NewKeeper(
messenger: NewDefaultMessageHandler(router, channelKeeper, capabilityKeeper, bankKeeper, cdc, portSource),
queryGasLimit: wasmConfig.SmartQueryGasLimit,
paramSpace: paramSpace,
instanceCost: DefaultInstanceCost,
compileCost: DefaultCompileCost,
gasMultiplier: DefaultGasMultiplier,
}

keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, keeper)
Expand Down Expand Up @@ -174,7 +180,7 @@ func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte,
if err != nil {
return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error())
}
ctx.GasMeter().ConsumeGas(CompileCost*uint64(len(wasmCode)), "Compiling WASM Bytecode")
ctx.GasMeter().ConsumeGas(k.compileCost*uint64(len(wasmCode)), "Compiling WASM Bytecode")

codeHash, err := k.wasmVM.Create(wasmCode)
if err != nil {
Expand Down Expand Up @@ -222,7 +228,7 @@ func (k Keeper) importCode(ctx sdk.Context, codeID uint64, codeInfo types.CodeIn
func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, deposit sdk.Coins, authZ AuthorizationPolicy) (sdk.AccAddress, []byte, error) {
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "instantiate")
if !k.IsPinnedCode(ctx, codeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: instantiate")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: instantiate")
}

// create contract address
Expand Down Expand Up @@ -268,12 +274,12 @@ func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.A
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)

// prepare querier
querier := NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress)
querier := k.newQueryHandler(ctx, contractAddress)

// instantiate wasm contract
gas := gasForContract(ctx)
res, gasUsed, err := k.wasmVM.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas)
consumeGas(ctx, gasUsed)
gas := k.gasForContract(ctx)
res, gasUsed, err := k.wasmVM.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas)
k.consumeGas(ctx, gasUsed)
if err != nil {
return contractAddress, nil, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error())
}
Expand Down Expand Up @@ -324,7 +330,7 @@ func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
}

if !k.IsPinnedCode(ctx, contractInfo.CodeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: execute")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: execute")
}

// add more funds
Expand All @@ -338,10 +344,10 @@ func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
info := types.NewInfo(caller, coins)

// prepare querier
querier := NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress)
gas := gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas)
consumeGas(ctx, gasUsed)
querier := k.newQueryHandler(ctx, contractAddress)
gas := k.gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas)
k.consumeGas(ctx, gasUsed)
if execErr != nil {
return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
}
Expand All @@ -364,7 +370,7 @@ func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) (*sdk.Result, error) {
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "migrate")
if !k.IsPinnedCode(ctx, newCodeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: migrate")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: migrate")
}

contractInfo := k.GetContractInfo(ctx, contractAddress)
Expand Down Expand Up @@ -399,13 +405,13 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
env := types.NewEnv(ctx, contractAddress)

// prepare querier
querier := NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress)
querier := k.newQueryHandler(ctx, contractAddress)

prefixStoreKey := types.GetContractStorePrefix(contractAddress)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)
gas := gasForContract(ctx)
res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, &prefixStore, cosmwasmAPI, &querier, gasMeter(ctx), gas)
consumeGas(ctx, gasUsed)
gas := k.gasForContract(ctx)
res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, &prefixStore, cosmwasmAPI, &querier, k.gasMeter(ctx), gas)
k.consumeGas(ctx, gasUsed)
if err != nil {
return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error())
}
Expand Down Expand Up @@ -444,16 +450,16 @@ func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte
}

if !k.IsPinnedCode(ctx, contractInfo.CodeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: sudo")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: sudo")
}

env := types.NewEnv(ctx, contractAddress)

// prepare querier
querier := NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress)
gas := gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Sudo(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas)
consumeGas(ctx, gasUsed)
querier := k.newQueryHandler(ctx, contractAddress)
gas := k.gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Sudo(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas)
k.consumeGas(ctx, gasUsed)
if execErr != nil {
return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
}
Expand Down Expand Up @@ -483,7 +489,7 @@ func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply was

// current thought is to charge gas like a fresh run, we can revisit whether to give it a discount later
if !k.IsPinnedCode(ctx, contractInfo.CodeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: reply")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: reply")
}

env := types.NewEnv(ctx, contractAddress)
Expand All @@ -493,9 +499,9 @@ func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply was
Ctx: ctx,
Plugins: k.wasmVMQueryHandler,
}
gas := gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Reply(codeInfo.CodeHash, env, reply, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas)
consumeGas(ctx, gasUsed)
gas := k.gasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.Reply(codeInfo.CodeHash, env, reply, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), gas)
k.consumeGas(ctx, gasUsed)
if execErr != nil {
return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error())
}
Expand Down Expand Up @@ -598,15 +604,15 @@ func (k Keeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []b
return nil, err
}
if !k.IsPinnedCode(ctx, contractInfo.CodeID) {
ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: query")
ctx.GasMeter().ConsumeGas(k.instanceCost, "Loading CosmWasm module: query")
}

// prepare querier
querier := NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddr)
querier := k.newQueryHandler(ctx, contractAddr)

env := types.NewEnv(ctx, contractAddr)
queryResult, gasUsed, qErr := k.wasmVM.Query(codeInfo.CodeHash, env, req, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gasForContract(ctx))
consumeGas(ctx, gasUsed)
queryResult, gasUsed, qErr := k.wasmVM.Query(codeInfo.CodeHash, env, req, prefixStore, cosmwasmAPI, querier, k.gasMeter(ctx), k.gasForContract(ctx))
k.consumeGas(ctx, gasUsed)
if qErr != nil {
return nil, sdkerrors.Wrap(types.ErrQueryFailed, qErr.Error())
}
Expand Down Expand Up @@ -812,17 +818,17 @@ func (k *Keeper) handleContractResponse(ctx sdk.Context, contractAddr sdk.AccAdd
return k.wasmVMResponseHandler.Handle(ctx, contractAddr, ibcPort, res.Submessages, res.Messages, res.Data)
}

func gasForContract(ctx sdk.Context) uint64 {
func (k Keeper) gasForContract(ctx sdk.Context) uint64 {
meter := ctx.GasMeter()
if meter.IsOutOfGas() {
return 0
}
remaining := (meter.Limit() - meter.GasConsumedToLimit()) * GasMultiplier
remaining := (meter.Limit() - meter.GasConsumedToLimit()) * k.gasMultiplier
return remaining
}

func consumeGas(ctx sdk.Context, gas uint64) {
consumed := gas / GasMultiplier
func (k Keeper) consumeGas(ctx sdk.Context, gas uint64) {
consumed := gas / k.gasMultiplier
ctx.GasMeter().ConsumeGas(consumed, "wasm contract")
// throw OutOfGas error if we ran out (got exactly to zero due to better limit enforcing)
if ctx.GasMeter().IsOutOfGas() {
Expand Down Expand Up @@ -907,6 +913,10 @@ func (k Keeper) importContract(ctx sdk.Context, contractAddr sdk.AccAddress, c *
return k.importContractState(ctx, contractAddr, state)
}

func (k Keeper) newQueryHandler(ctx sdk.Context, contractAddress sdk.AccAddress) QueryHandler {
return NewQueryHandler(ctx, k.wasmVMQueryHandler, contractAddress, k.gasMultiplier)
}

func addrFromUint64(id uint64) sdk.AccAddress {
addr := make([]byte, 20)
addr[0] = 'C'
Expand All @@ -917,18 +927,21 @@ func addrFromUint64(id uint64) sdk.AccAddress {
// MultipliedGasMeter wraps the GasMeter from context and multiplies all reads by out defined multiplier
type MultipliedGasMeter struct {
originalMeter sdk.GasMeter
gasMultiplier uint64
}

func NewMultipliedGasMeter(originalMeter sdk.GasMeter, gasMultiplier uint64) MultipliedGasMeter {
return MultipliedGasMeter{originalMeter: originalMeter, gasMultiplier: gasMultiplier}
}

var _ wasmvm.GasMeter = MultipliedGasMeter{}

func (m MultipliedGasMeter) GasConsumed() sdk.Gas {
return m.originalMeter.GasConsumed() * GasMultiplier
return m.originalMeter.GasConsumed() * m.gasMultiplier
}

func gasMeter(ctx sdk.Context) MultipliedGasMeter {
return MultipliedGasMeter{
originalMeter: ctx.GasMeter(),
}
func (k Keeper) gasMeter(ctx sdk.Context) MultipliedGasMeter {
return NewMultipliedGasMeter(ctx.GasMeter(), k.gasMultiplier)
}

// Logger returns a module-specific logger.
Expand Down
23 changes: 23 additions & 0 deletions x/wasm/keeper/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,26 @@ func WithVMCacheMetrics(r prometheus.Registerer) Option {
NewWasmVMMetricsCollector(k.wasmVM).Register(r)
})
}

// WithCosts sets custom gas costs and multiplier.
// See DefaultCompileCost, DefaultInstanceCost, DefaultGasMultiplier
// Uses WithApiCosts with defaults and given multiplier.
func WithCosts(compile, instance, multiplier uint64) Option {
return optsFn(func(k *Keeper) {
k.compileCost = compile
k.instanceCost = instance
k.gasMultiplier = multiplier
WithApiCosts(
DefaultGasCostHumanAddress*multiplier,
DefaultGasCostCanonicalAddress*multiplier,
).apply(k)
})
}

// WithApiCosts sets custom api costs. Amounts are in cosmwasm gas Not SDK gas.
func WithApiCosts(human, canonical uint64) Option {
return optsFn(func(_ *Keeper) {
costHumanize = human
costCanonical = canonical
})
}
36 changes: 30 additions & 6 deletions x/wasm/keeper/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,62 @@ import (
func TestConstructorOptions(t *testing.T) {
specs := map[string]struct {
srcOpt Option
verify func(Keeper)
verify func(*testing.T, Keeper)
}{
"wasm engine": {
srcOpt: WithWasmEngine(&wasmtesting.MockWasmer{}),
verify: func(k Keeper) {
verify: func(t *testing.T, k Keeper) {
assert.IsType(t, k.wasmVM, &wasmtesting.MockWasmer{})
},
},
"message handler": {
srcOpt: WithMessageHandler(&wasmtesting.MockMessageHandler{}),
verify: func(k Keeper) {
verify: func(t *testing.T, k Keeper) {
assert.IsType(t, k.messenger, &wasmtesting.MockMessageHandler{})
},
},
"query plugins": {
srcOpt: WithQueryHandler(&wasmtesting.MockQueryHandler{}),
verify: func(k Keeper) {
verify: func(t *testing.T, k Keeper) {
assert.IsType(t, k.wasmVMQueryHandler, &wasmtesting.MockQueryHandler{})
},
},
"coin transferrer": {
srcOpt: WithCoinTransferrer(&wasmtesting.MockCoinTransferrer{}),
verify: func(k Keeper) {
verify: func(t *testing.T, k Keeper) {
assert.IsType(t, k.bank, &wasmtesting.MockCoinTransferrer{})
},
},
"costs": {
srcOpt: WithCosts(1, 2, 3),
verify: func(t *testing.T, k Keeper) {
t.Cleanup(setApiDefaults)
assert.Equal(t, uint64(1), k.compileCost)
assert.Equal(t, uint64(2), k.instanceCost)
assert.Equal(t, uint64(3), k.gasMultiplier)
assert.Equal(t, uint64(15), costHumanize)
assert.Equal(t, uint64(12), costCanonical)
},
},
"api costs": {
srcOpt: WithApiCosts(1, 2),
verify: func(t *testing.T, k Keeper) {
t.Cleanup(setApiDefaults)
assert.Equal(t, uint64(1), costHumanize)
assert.Equal(t, uint64(2), costCanonical)
},
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
k := NewKeeper(nil, nil, paramtypes.NewSubspace(nil, nil, nil, nil, ""), authkeeper.AccountKeeper{}, nil, stakingkeeper.Keeper{}, distributionkeeper.Keeper{}, nil, nil, nil, nil, nil, nil, "tempDir", types.DefaultWasmConfig(), SupportedFeatures, spec.srcOpt)
spec.verify(k)
spec.verify(t, k)
})
}

}

func setApiDefaults() {
costHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier
costCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier
}
Loading

0 comments on commit e7526cc

Please sign in to comment.