mirror of https://github.com/certusone/wasmd.git
321 lines
9.1 KiB
Go
321 lines
9.1 KiB
Go
package keeper
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
storetypes "cosmossdk.io/store/types"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
|
|
"github.com/CosmWasm/wasmd/x/wasm/types"
|
|
)
|
|
|
|
type Recurse struct {
|
|
Depth uint32 `json:"depth"`
|
|
Work uint32 `json:"work"`
|
|
}
|
|
|
|
type recurseWrapper struct {
|
|
Recurse Recurse `json:"recurse"`
|
|
}
|
|
|
|
func buildRecurseQuery(t *testing.T, msg Recurse) []byte {
|
|
t.Helper()
|
|
wrapper := recurseWrapper{Recurse: msg}
|
|
bz, err := json.Marshal(wrapper)
|
|
require.NoError(t, err)
|
|
return bz
|
|
}
|
|
|
|
type recurseResponse struct {
|
|
Hashed []byte `json:"hashed"`
|
|
}
|
|
|
|
// number os wasm queries called from a contract
|
|
var totalWasmQueryCounter int
|
|
|
|
func initRecurseContract(t *testing.T) (contract sdk.AccAddress, ctx sdk.Context, keeper *Keeper) {
|
|
t.Helper()
|
|
countingQuerierDec := func(realWasmQuerier WasmVMQueryHandler) WasmVMQueryHandler {
|
|
return WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) {
|
|
totalWasmQueryCounter++
|
|
return realWasmQuerier.HandleQuery(ctx, caller, request)
|
|
})
|
|
}
|
|
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities, WithQueryHandlerDecorator(countingQuerierDec))
|
|
keeper = keepers.WasmKeeper
|
|
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
|
|
return exampleContract.Contract, ctx, keeper
|
|
}
|
|
|
|
func TestGasCostOnQuery(t *testing.T) {
|
|
const (
|
|
GasNoWork uint64 = 64_209
|
|
GasWork50 uint64 = 64_456
|
|
// should be discounted exactly by the difference between normal instance cost and discounted instance cost
|
|
GasNoWorkDiscounted uint64 = GasNoWork - (types.DefaultInstanceCost - types.DefaultInstanceCostDiscount)
|
|
GasWork50Discounted uint64 = GasWork50 - (types.DefaultInstanceCost - types.DefaultInstanceCostDiscount)
|
|
|
|
GasReturnUnhashed uint64 = 74
|
|
GasReturnHashed uint64 = 63
|
|
)
|
|
|
|
cases := map[string]struct {
|
|
gasLimit uint64
|
|
msg Recurse
|
|
expectedGas uint64
|
|
}{
|
|
"no recursion, no work": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{},
|
|
expectedGas: GasNoWork,
|
|
},
|
|
"no recursion, some work": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Work: 50, // 50 rounds of sha256 inside the contract
|
|
},
|
|
expectedGas: GasWork50,
|
|
},
|
|
"recursion 1, no work": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Depth: 1,
|
|
},
|
|
expectedGas: GasNoWork + GasNoWorkDiscounted + GasReturnUnhashed,
|
|
},
|
|
"recursion 1, some work": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Depth: 1,
|
|
Work: 50,
|
|
},
|
|
expectedGas: GasWork50 + GasWork50Discounted + GasReturnHashed,
|
|
},
|
|
"recursion 4, some work": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Depth: 4,
|
|
Work: 50,
|
|
},
|
|
expectedGas: GasWork50 + 4*(GasWork50Discounted+GasReturnHashed),
|
|
},
|
|
}
|
|
|
|
contractAddr, ctx, keeper := initRecurseContract(t)
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
// external limit has no effect (we get a panic if this is enforced)
|
|
keeper.queryGasLimit = 1000
|
|
|
|
// make sure we set a limit before calling
|
|
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(tc.gasLimit))
|
|
// init tx contracts in ctx
|
|
ctx = types.WithTxContracts(ctx, types.NewTxContracts())
|
|
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
|
|
|
|
// do the query
|
|
recurse := tc.msg
|
|
msg := buildRecurseQuery(t, recurse)
|
|
data, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
|
require.NoError(t, err)
|
|
|
|
// check the gas is what we expected
|
|
if types.EnableGasVerification {
|
|
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
|
|
}
|
|
// assert result is 32 byte sha256 hash (if hashed), or contractAddr if not
|
|
var resp recurseResponse
|
|
err = json.Unmarshal(data, &resp)
|
|
require.NoError(t, err)
|
|
if recurse.Work == 0 {
|
|
assert.Equal(t, len(contractAddr.String()), len(resp.Hashed))
|
|
} else {
|
|
assert.Equal(t, 32, len(resp.Hashed))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGasOnExternalQuery(t *testing.T) {
|
|
const (
|
|
GasWork50 uint64 = types.DefaultInstanceCost + 8_464
|
|
)
|
|
|
|
cases := map[string]struct {
|
|
gasLimit uint64
|
|
msg Recurse
|
|
expOutOfGas bool
|
|
}{
|
|
"no recursion, plenty gas": {
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Work: 50, // 50 rounds of sha256 inside the contract
|
|
},
|
|
},
|
|
"recursion 4, plenty gas": {
|
|
// this uses 244708 gas
|
|
gasLimit: 400_000,
|
|
msg: Recurse{
|
|
Depth: 4,
|
|
Work: 50,
|
|
},
|
|
},
|
|
"no recursion, external gas limit": {
|
|
gasLimit: 5000, // this is not enough
|
|
msg: Recurse{
|
|
Work: 50,
|
|
},
|
|
expOutOfGas: true,
|
|
},
|
|
"recursion 4, external gas limit": {
|
|
gasLimit: GasWork50,
|
|
msg: Recurse{
|
|
Depth: 4,
|
|
Work: 50,
|
|
},
|
|
expOutOfGas: true,
|
|
},
|
|
}
|
|
|
|
contractAddr, ctx, keeper := initRecurseContract(t)
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
recurse := tc.msg
|
|
msg := buildRecurseQuery(t, recurse)
|
|
|
|
querier := NewGrpcQuerier(keeper.cdc, keeper.storeService, keeper, tc.gasLimit)
|
|
req := &types.QuerySmartContractStateRequest{Address: contractAddr.String(), QueryData: msg}
|
|
_, gotErr := querier.SmartContractState(ctx, req)
|
|
if tc.expOutOfGas {
|
|
require.Error(t, gotErr, sdkerrors.ErrOutOfGas)
|
|
return
|
|
}
|
|
require.NoError(t, gotErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLimitRecursiveQueryGas(t *testing.T) {
|
|
// The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456
|
|
// Basically, if I burn 90% of gas in CPU loop, then query out (to my self)
|
|
// the sub-query will have all the original gas (minus the 40k instance charge)
|
|
// and can burn 90% and call a sub-contract again...
|
|
// This attack would allow us to use far more than the provided gas before
|
|
// eventually hitting an OutOfGas panic.
|
|
|
|
const (
|
|
// Note: about 100 SDK gas (10k CosmWasm gas) for each round of sha256
|
|
GasWork2k uint64 = 77_038 // = SetupContractCost + x // we have 6x gas used in cpu than in the instance
|
|
|
|
// should be discounted exactly by the difference between normal instance cost and discounted instance cost
|
|
GasWork2kDiscounted uint64 = GasWork2k - (types.DefaultInstanceCost - types.DefaultInstanceCostDiscount)
|
|
|
|
// This is overhead for calling into a sub-contract
|
|
GasReturnHashed uint64 = 64
|
|
|
|
// lots of additional gas for long error message
|
|
GasError uint64 = 3408
|
|
)
|
|
|
|
cases := map[string]struct {
|
|
gasLimit uint64
|
|
msg Recurse
|
|
expectQueriesFromContract int
|
|
expectedGas uint64
|
|
expectOutOfGas bool
|
|
expectError string
|
|
}{
|
|
"no recursion, lots of work": {
|
|
gasLimit: 4_000_000,
|
|
msg: Recurse{
|
|
Depth: 0,
|
|
Work: 2000,
|
|
},
|
|
expectQueriesFromContract: 0,
|
|
expectedGas: GasWork2k,
|
|
},
|
|
"recursion 5, lots of work": {
|
|
gasLimit: GasWork2k + 5*(GasWork2kDiscounted+GasReturnHashed) + 1,
|
|
msg: Recurse{
|
|
Depth: 5,
|
|
Work: 2000,
|
|
},
|
|
expectQueriesFromContract: 5,
|
|
expectedGas: GasWork2k + 5*(GasWork2kDiscounted+GasReturnHashed),
|
|
},
|
|
// this is where we expect an error...
|
|
// it has enough gas to run 5 times and die on the 6th (5th time dispatching to sub-contract)
|
|
// however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times
|
|
"deep recursion, should die on 6th level": {
|
|
gasLimit: GasWork2k + 5*(GasWork2kDiscounted+GasReturnHashed),
|
|
msg: Recurse{
|
|
Depth: 6,
|
|
Work: 2000,
|
|
},
|
|
expectQueriesFromContract: 6,
|
|
expectOutOfGas: true,
|
|
},
|
|
"very deep recursion, hits recursion limit": {
|
|
gasLimit: 10_000_000,
|
|
msg: Recurse{
|
|
Depth: 100,
|
|
Work: 2000,
|
|
},
|
|
expectQueriesFromContract: 10,
|
|
expectOutOfGas: false,
|
|
expectError: "query wasm contract failed", // Error we get from the contract instance doing the failing query, not wasmd
|
|
expectedGas: GasWork2k + GasReturnHashed + 9*(GasWork2kDiscounted+GasReturnHashed) + GasError,
|
|
},
|
|
}
|
|
|
|
contractAddr, ctx, keeper := initRecurseContract(t)
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
// reset the counter before test
|
|
totalWasmQueryCounter = 0
|
|
|
|
// make sure we set a limit before calling
|
|
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(tc.gasLimit))
|
|
// init tx contracts in ctx
|
|
ctx = types.WithTxContracts(ctx, types.NewTxContracts())
|
|
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
|
|
|
|
// prepare the query
|
|
recurse := tc.msg
|
|
msg := buildRecurseQuery(t, recurse)
|
|
|
|
// if we expect out of gas, make sure this panics
|
|
if tc.expectOutOfGas {
|
|
require.Panics(t, func() {
|
|
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
|
t.Logf("Got error not panic: %#v", err)
|
|
})
|
|
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
|
|
return
|
|
}
|
|
|
|
// otherwise, we expect a successful call
|
|
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
|
|
if tc.expectError != "" {
|
|
require.ErrorContains(t, err, tc.expectError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if types.EnableGasVerification {
|
|
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
|
|
}
|
|
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
|
|
})
|
|
}
|
|
}
|