wasmd/x/wasm/keeper/recurse_test.go

300 lines
8.1 KiB
Go

package keeper
import (
"encoding/json"
"testing"
"github.com/CosmWasm/wasmd/x/wasm/types"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
type Recurse struct {
Depth uint32 `json:"depth"`
Work uint32 `json:"work"`
Contract sdk.AccAddress `json:"contract"`
}
type recurseWrapper struct {
Recurse Recurse `json:"recurse"`
}
func buildRecurseQuery(t *testing.T, msg Recurse) []byte {
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, creator sdk.AccAddress, ctx sdk.Context, keeper *Keeper) {
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, SupportedFeatures, WithQueryHandlerDecorator(countingQuerierDec))
keeper = keepers.WasmKeeper
exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers)
return exampleContract.Contract, exampleContract.CreatorAddr, ctx, keeper
}
func TestGasCostOnQuery(t *testing.T) {
const (
GasNoWork uint64 = 63_958
// Note: about 100 SDK gas (10k wasmer gas) for each round of sha256
GasWork50 uint64 = 64_401 // this is a little shy of 50k gas - to keep an eye on the limit
GasReturnUnhashed uint64 = 33
GasReturnHashed uint64 = 25
)
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: 2*GasNoWork + GasReturnUnhashed,
},
"recursion 1, some work": {
gasLimit: 400_000,
msg: Recurse{
Depth: 1,
Work: 50,
},
expectedGas: 2*GasWork50 + GasReturnHashed,
},
"recursion 4, some work": {
gasLimit: 400_000,
msg: Recurse{
Depth: 4,
Work: 50,
},
expectedGas: 5*GasWork50 + 4*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(sdk.NewGasMeter(tc.gasLimit))
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
// do the query
recurse := tc.msg
recurse.Contract = contractAddr
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 = DefaultInstanceCost + 8_464
)
cases := map[string]struct {
gasLimit uint64
msg Recurse
expectPanic 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,
},
expectPanic: true,
},
"recursion 4, external gas limit": {
// this uses 244708 gas but give less
gasLimit: 4 * GasWork50,
msg: Recurse{
Depth: 4,
Work: 50,
},
expectPanic: true,
},
}
contractAddr, _, ctx, keeper := initRecurseContract(t)
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
recurse := tc.msg
recurse.Contract = contractAddr
msg := buildRecurseQuery(t, recurse)
// do the query
path := []string{QueryGetContractState, contractAddr.String(), QueryMethodContractStateSmart}
req := abci.RequestQuery{Data: msg}
if tc.expectPanic {
require.Panics(t, func() {
// this should run out of gas
_, err := NewLegacyQuerier(keeper, tc.gasLimit)(ctx, path, req)
t.Logf("%v", err)
})
} else {
// otherwise, make sure we get a good success
_, err := NewLegacyQuerier(keeper, tc.gasLimit)(ctx, path, req)
require.NoError(t, err)
}
})
}
}
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 wasmer gas) for each round of sha256
GasWork2k uint64 = 84_236 // = NewContractInstanceCosts + x // we have 6x gas used in cpu than in the instance
// This is overhead for calling into a sub-contract
GasReturnHashed uint64 = 26
)
cases := map[string]struct {
gasLimit uint64
msg Recurse
expectQueriesFromContract int
expectedGas uint64
expectOutOfGas bool
}{
"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: 4_000_000,
msg: Recurse{
Depth: 5,
Work: 2000,
},
expectQueriesFromContract: 5,
// FIXME: why -1 ... confused a bit by calculations, seems like rounding issues
expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed) - 1,
},
// this is where we expect an error...
// it has enough gas to run 4 times and die on the 5th (4th 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 5th level": {
gasLimit: 400_000,
msg: Recurse{
Depth: 50,
Work: 2000,
},
expectQueriesFromContract: 4,
expectOutOfGas: true,
},
}
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(sdk.NewGasMeter(tc.gasLimit))
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
// prepare the query
recurse := tc.msg
recurse.Contract = contractAddr
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)
require.NoError(t, err)
if types.EnableGasVerification {
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
}
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
})
}
}