Skip to content

Commit

Permalink
Charge gas to unzip wasm code (#898)
Browse files Browse the repository at this point in the history
* Charge gas for unzip wasm code

* Add uncompress test

* Apply review feedback

* Add testcase to uncompress spec
  • Loading branch information
alpe authored Aug 31, 2022
1 parent 49d571e commit e714fdf
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 40 deletions.
2 changes: 1 addition & 1 deletion app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 4 additions & 10 deletions x/wasm/ioutils/ioutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
21 changes: 0 additions & 21 deletions x/wasm/ioutils/ioutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"io"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion x/wasm/ioutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions x/wasm/ioutils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down
25 changes: 25 additions & 0 deletions x/wasm/keeper/gas_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -107,6 +123,7 @@ func DefaultGasRegisterConfig() WasmGasRegisterConfig {
EventAttributeDataCost: DefaultEventAttributeDataCost,
EventAttributeDataFreeTier: DefaultEventAttributeDataFreeTier,
ContractMessageDataCost: DefaultContractMessageDataCost,
UncompressCost: DefaultPerByteUncompressCost(),
}
}

Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions x/wasm/keeper/gas_register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
}
20 changes: 13 additions & 7 deletions x/wasm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions x/wasm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions x/wasm/keeper/snapshotter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Binary file added x/wasm/keeper/testdata/broken_crc.gzip
Binary file not shown.
8 changes: 8 additions & 0 deletions x/wasm/keeper/wasmtesting/gas_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
Expand Down

0 comments on commit e714fdf

Please sign in to comment.