Charge gas to unzip wasm code (#898)

* Charge gas for unzip wasm code

* Add uncompress test

* Apply review feedback

* Add testcase to uncompress spec
This commit is contained in:
Alexander Peters
2022-08-31 16:17:03 +02:00
committed by GitHub
parent 49d571edbe
commit e714fdf3b4
12 changed files with 114 additions and 40 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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)
})
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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())

BIN
x/wasm/keeper/testdata/broken_crc.gzip vendored Normal file

Binary file not shown.

View File

@@ -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")