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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
x/wasm/keeper/testdata/broken_crc.gzip
vendored
Normal file
Binary file not shown.
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user