diff --git a/app/test_helpers.go b/app/test_helpers.go index 5aec56108..ccf49656a 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -284,7 +284,7 @@ func CheckBalance(t *testing.T, app *WasmApp, addr sdk.AccAddress, balances sdk. require.True(t, balances.IsEqual(app.BankKeeper.GetAllBalances(ctxCheck, addr))) } -const DefaultGas = 1200000 +const DefaultGas = 1_500_000 // SignCheckDeliver checks a generated signed transaction and simulates a // block commitment with the given transaction. A test assertion is made using diff --git a/x/wasm/ioutils/ioutil.go b/x/wasm/ioutils/ioutil.go index 574013234..a34a43e9e 100644 --- a/x/wasm/ioutils/ioutil.go +++ b/x/wasm/ioutils/ioutil.go @@ -8,18 +8,12 @@ import ( "github.com/CosmWasm/wasmd/x/wasm/types" ) -// Uncompress returns gzip uncompressed content if input was gzip, or original src otherwise -func Uncompress(src []byte, limit uint64) ([]byte, error) { - switch n := uint64(len(src)); { - case n < 3: - return src, nil - case n > limit: +// Uncompress expects a valid gzip source to unpack or fails. See IsGzip +func Uncompress(gzipSrc []byte, limit uint64) ([]byte, error) { + if uint64(len(gzipSrc)) > limit { return nil, types.ErrLimit } - if !bytes.Equal(gzipIdent, src[0:3]) { - return src, nil - } - zr, err := gzip.NewReader(bytes.NewReader(src)) + zr, err := gzip.NewReader(bytes.NewReader(gzipSrc)) if err != nil { return nil, err } diff --git a/x/wasm/ioutils/ioutil_test.go b/x/wasm/ioutils/ioutil_test.go index 93a478ae0..3399abf29 100644 --- a/x/wasm/ioutils/ioutil_test.go +++ b/x/wasm/ioutils/ioutil_test.go @@ -6,7 +6,6 @@ import ( "errors" "io" "os" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -29,30 +28,10 @@ func TestUncompress(t *testing.T) { expError error expResult []byte }{ - "handle wasm uncompressed": { - src: wasmRaw, - expResult: wasmRaw, - }, "handle wasm compressed": { src: wasmGzipped, expResult: wasmRaw, }, - "handle nil slice": { - src: nil, - expResult: nil, - }, - "handle short unidentified": { - src: []byte{0x1, 0x2}, - expResult: []byte{0x1, 0x2}, - }, - "handle input slice exceeding limit": { - src: []byte(strings.Repeat("a", maxSize+1)), - expError: types.ErrLimit, - }, - "handle input slice at limit": { - src: []byte(strings.Repeat("a", maxSize)), - expResult: []byte(strings.Repeat("a", maxSize)), - }, "handle gzip identifier only": { src: gzipIdent, expError: io.ErrUnexpectedEOF, diff --git a/x/wasm/ioutils/utils.go b/x/wasm/ioutils/utils.go index d4b8abf34..197c44c36 100644 --- a/x/wasm/ioutils/utils.go +++ b/x/wasm/ioutils/utils.go @@ -17,7 +17,7 @@ var ( // IsGzip returns checks if the file contents are gzip compressed func IsGzip(input []byte) bool { - return bytes.Equal(input[:3], gzipIdent) + return len(input) >= 3 && bytes.Equal(gzipIdent, input[0:3]) } // IsWasm checks if the file contents are of wasm binary diff --git a/x/wasm/ioutils/utils_test.go b/x/wasm/ioutils/utils_test.go index cd4846f09..2dea0c5c0 100644 --- a/x/wasm/ioutils/utils_test.go +++ b/x/wasm/ioutils/utils_test.go @@ -41,6 +41,8 @@ func TestIsGzip(t *testing.T) { require.False(t, IsGzip(wasmCode)) require.False(t, IsGzip(someRandomStr)) + require.False(t, IsGzip(nil)) + require.True(t, IsGzip(gzipData[0:3])) require.True(t, IsGzip(gzipData)) } diff --git a/x/wasm/keeper/gas_register.go b/x/wasm/keeper/gas_register.go index c21606344..51f8e607c 100644 --- a/x/wasm/keeper/gas_register.go +++ b/x/wasm/keeper/gas_register.go @@ -54,12 +54,26 @@ const ( DefaultEventAttributeDataFreeTier = 100 ) +// default: 0.15 gas. +// see https://github.com/CosmWasm/wasmd/pull/898#discussion_r937727200 +var defaultPerByteUncompressCost = wasmvmtypes.UFraction{ + Numerator: 15, + Denominator: 100, +} + +// DefaultPerByteUncompressCost is how much SDK gas we charge per source byte to unpack +func DefaultPerByteUncompressCost() wasmvmtypes.UFraction { + return defaultPerByteUncompressCost +} + // GasRegister abstract source for gas costs type GasRegister interface { // NewContractInstanceCosts costs to crate a new contract instance from code NewContractInstanceCosts(pinned bool, msgLen int) sdk.Gas // CompileCosts costs to persist and "compile" a new wasm contract CompileCosts(byteLength int) sdk.Gas + // UncompressCosts costs to unpack a new wasm contract + UncompressCosts(byteLength int) sdk.Gas // InstantiateContractCosts costs when interacting with a wasm contract InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas // ReplyCosts costs to to handle a message reply @@ -78,6 +92,8 @@ type WasmGasRegisterConfig struct { InstanceCost sdk.Gas // CompileCosts costs to persist and "compile" a new wasm contract CompileCost sdk.Gas + // UncompressCost costs per byte to unpack a contract + UncompressCost wasmvmtypes.UFraction // GasMultiplier 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 GasMultiplier sdk.Gas @@ -107,6 +123,7 @@ func DefaultGasRegisterConfig() WasmGasRegisterConfig { EventAttributeDataCost: DefaultEventAttributeDataCost, EventAttributeDataFreeTier: DefaultEventAttributeDataFreeTier, ContractMessageDataCost: DefaultContractMessageDataCost, + UncompressCost: DefaultPerByteUncompressCost(), } } @@ -143,6 +160,14 @@ func (g WasmGasRegister) CompileCosts(byteLength int) storetypes.Gas { return g.c.CompileCost * uint64(byteLength) } +// UncompressCosts costs to unpack a new wasm contract +func (g WasmGasRegister) UncompressCosts(byteLength int) sdk.Gas { + if byteLength < 0 { + panic(sdkerrors.Wrap(types.ErrInvalid, "negative length")) + } + return g.c.UncompressCost.Mul(uint64(byteLength)).Floor() +} + // InstantiateContractCosts costs when interacting with a wasm contract func (g WasmGasRegister) InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas { if msgLen < 0 { diff --git a/x/wasm/keeper/gas_register_test.go b/x/wasm/keeper/gas_register_test.go index bc3e761a2..03f6cdd86 100644 --- a/x/wasm/keeper/gas_register_test.go +++ b/x/wasm/keeper/gas_register_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -430,3 +432,41 @@ func TestFromWasmVMGasConversion(t *testing.T) { }) } } + +func TestUncompressCosts(t *testing.T) { + specs := map[string]struct { + lenIn int + exp sdk.Gas + expPanic bool + }{ + "0": { + exp: 0, + }, + "even": { + lenIn: 100, + exp: 15, + }, + "round down when uneven": { + lenIn: 19, + exp: 2, + }, + "max len": { + lenIn: types.MaxWasmSize, + exp: 122880, + }, + "invalid len": { + lenIn: -1, + expPanic: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + if spec.expPanic { + assert.Panics(t, func() { NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn) }) + return + } + got := NewDefaultWasmGasRegister().UncompressCosts(spec.lenIn) + assert.Equal(t, spec.exp, got) + }) + } +} diff --git a/x/wasm/keeper/keeper.go b/x/wasm/keeper/keeper.go index 372cb4e9a..c5511f3e4 100644 --- a/x/wasm/keeper/keeper.go +++ b/x/wasm/keeper/keeper.go @@ -178,12 +178,15 @@ func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "instantiate access must be subset of default upload access") } - wasmCode, err = ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) - if err != nil { - return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + if ioutils.IsGzip(wasmCode) { + ctx.GasMeter().ConsumeGas(k.gasRegister.UncompressCosts(len(wasmCode)), "Uncompress gzip bytecode") + wasmCode, err = ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) + if err != nil { + return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } } - ctx.GasMeter().ConsumeGas(k.gasRegister.CompileCosts(len(wasmCode)), "Compiling WASM Bytecode") + ctx.GasMeter().ConsumeGas(k.gasRegister.CompileCosts(len(wasmCode)), "Compiling wasm bytecode") checksum, err := k.wasmVM.Create(wasmCode) if err != nil { return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) @@ -216,9 +219,12 @@ func (k Keeper) storeCodeInfo(ctx sdk.Context, codeID uint64, codeInfo types.Cod } func (k Keeper) importCode(ctx sdk.Context, codeID uint64, codeInfo types.CodeInfo, wasmCode []byte) error { - wasmCode, err := ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) - if err != nil { - return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + if ioutils.IsGzip(wasmCode) { + var err error + wasmCode, err = ioutils.Uncompress(wasmCode, uint64(types.MaxWasmSize)) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } } newCodeHash, err := k.wasmVM.Create(wasmCode) if err != nil { diff --git a/x/wasm/keeper/keeper_test.go b/x/wasm/keeper/keeper_test.go index ba1ccdc73..ac26f1039 100644 --- a/x/wasm/keeper/keeper_test.go +++ b/x/wasm/keeper/keeper_test.go @@ -360,6 +360,23 @@ func TestCreateWithGzippedPayload(t *testing.T) { require.Equal(t, hackatomWasm, storedCode) } +func TestCreateWithBrokenGzippedPayload(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, SupportedFeatures) + keeper := keepers.ContractKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedAccount(ctx, deposit...) + + wasmCode, err := os.ReadFile("./testdata/broken_crc.gzip") + require.NoError(t, err, "reading gzipped WASM code") + + gm := sdk.NewInfiniteGasMeter() + contractID, err := keeper.Create(ctx.WithGasMeter(gm), creator, wasmCode, nil) + require.Error(t, err) + assert.Empty(t, contractID) + assert.GreaterOrEqual(t, gm.GasConsumed(), sdk.Gas(121384)) // 809232 * 0.15 (default uncompress costs) = 121384 +} + func TestInstantiate(t *testing.T) { ctx, keepers := CreateTestInput(t, false, SupportedFeatures) keeper := keepers.ContractKeeper diff --git a/x/wasm/keeper/snapshotter.go b/x/wasm/keeper/snapshotter.go index a781cb07a..fcece6371 100644 --- a/x/wasm/keeper/snapshotter.go +++ b/x/wasm/keeper/snapshotter.go @@ -99,6 +99,9 @@ func (ws *WasmSnapshotter) Restore( } func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error { + if !ioutils.IsGzip(compressedCode) { + return types.ErrInvalid.Wrap("not a gzip") + } wasmCode, err := ioutils.Uncompress(compressedCode, uint64(types.MaxWasmSize)) if err != nil { return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) diff --git a/x/wasm/keeper/testdata/broken_crc.gzip b/x/wasm/keeper/testdata/broken_crc.gzip new file mode 100644 index 000000000..378713e2f Binary files /dev/null and b/x/wasm/keeper/testdata/broken_crc.gzip differ diff --git a/x/wasm/keeper/wasmtesting/gas_register.go b/x/wasm/keeper/wasmtesting/gas_register.go index 1c1a319ba..d1975f763 100644 --- a/x/wasm/keeper/wasmtesting/gas_register.go +++ b/x/wasm/keeper/wasmtesting/gas_register.go @@ -14,6 +14,7 @@ type MockGasRegister struct { EventCostsFn func(evts []wasmvmtypes.EventAttribute) sdk.Gas ToWasmVMGasFn func(source sdk.Gas) uint64 FromWasmVMGasFn func(source uint64) sdk.Gas + UncompressCostsFn func(byteLength int) sdk.Gas } func (m MockGasRegister) NewContractInstanceCosts(pinned bool, msgLen int) sdk.Gas { @@ -30,6 +31,13 @@ func (m MockGasRegister) CompileCosts(byteLength int) sdk.Gas { return m.CompileCostFn(byteLength) } +func (m MockGasRegister) UncompressCosts(byteLength int) sdk.Gas { + if m.UncompressCostsFn == nil { + panic("not expected to be called") + } + return m.UncompressCostsFn(byteLength) +} + func (m MockGasRegister) InstantiateContractCosts(pinned bool, msgLen int) sdk.Gas { if m.InstantiateContractCostFn == nil { panic("not expected to be called")