Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Charge gas to unzip wasm code #898

Merged
merged 4 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
alpe marked this conversation as resolved.
Show resolved Hide resolved
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{
alpe marked this conversation as resolved.
Show resolved Hide resolved
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) {
alpe marked this conversation as resolved.
Show resolved Hide resolved
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) {
alpe marked this conversation as resolved.
Show resolved Hide resolved
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")
}
alpe marked this conversation as resolved.
Show resolved Hide resolved
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