From 19e6c65dbcbf38069a26d7be2ce2540ba89a775e Mon Sep 17 00:00:00 2001 From: vuong177 Date: Wed, 12 Jul 2023 15:08:59 +0700 Subject: [PATCH 1/2] add wasm client snapshotter --- .../light-clients/08-wasm/keeper/keeper.go | 6 + .../08-wasm/keeper/snapshotter.go | 134 ++++++++++++++++++ modules/light-clients/08-wasm/keeper/utils.go | 17 +++ .../light-clients/08-wasm/types/upgrade.go | 1 + .../light-clients/08-wasm/types/validation.go | 2 +- 5 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 modules/light-clients/08-wasm/keeper/snapshotter.go diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go index 2ce18789498..54f17fd6b3b 100644 --- a/modules/light-clients/08-wasm/keeper/keeper.go +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -199,4 +199,10 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { }) } return genesisState +func (k Keeper) IterateCodeInfos(ctx sdk.Context, fn func(id uint64, codeID string) (stop bool)) { + +} + +func (k Keeper) GetWasmByte(ctx sdk.Context, codeID string) ([]byte, error) { + return nil, nil } diff --git a/modules/light-clients/08-wasm/keeper/snapshotter.go b/modules/light-clients/08-wasm/keeper/snapshotter.go new file mode 100644 index 00000000000..1d3c807b141 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/snapshotter.go @@ -0,0 +1,134 @@ +package keeper + +import ( + "io" + + errorsmod "cosmossdk.io/errors" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + snapshot "github.com/cosmos/cosmos-sdk/snapshots/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/v7/modules/light-clients/08-wasm/types" +) + +var _ snapshot.ExtensionSnapshotter = &WasmSnapshotter{} + +// SnapshotFormat format 1 is just gzipped wasm byte code for each item payload. No protobuf envelope, no metadata. +const SnapshotFormat = 1 + +type WasmSnapshotter struct { + wasm *Keeper + cms sdk.MultiStore +} + +func NewWasmSnapshotter(cms sdk.MultiStore, wasm *Keeper) *WasmSnapshotter { + return &WasmSnapshotter{ + wasm: wasm, + cms: cms, + } +} + +func (ws *WasmSnapshotter) SnapshotName() string { + return types.ModuleName +} + +func (ws *WasmSnapshotter) SnapshotFormat() uint32 { + return SnapshotFormat +} + +func (ws *WasmSnapshotter) SupportedFormats() []uint32 { + // If we support older formats, add them here and handle them in Restore + return []uint32{SnapshotFormat} +} + +func (ws *WasmSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshot.ExtensionPayloadWriter) error { + cacheMS, err := ws.cms.CacheMultiStoreWithVersion(int64(height)) + if err != nil { + return err + } + + ctx := sdk.NewContext(cacheMS, tmproto.Header{}, false, nil) + seenBefore := make(map[string]bool) + var rerr error + + ws.wasm.IterateCodeInfos(ctx, func(id uint64, codeID string) bool { + if seenBefore[codeID] { + return false + } + seenBefore[codeID] = true + + // load code and abort on error + wasmBytes, err := ws.wasm.GetWasmByte(ctx, codeID) + if err != nil { + rerr = err + return true + } + + compressedWasm, err := types.GzipIt(wasmBytes) + if err != nil { + rerr = err + return true + } + + err = payloadWriter(compressedWasm) + if err != nil { + rerr = err + return true + } + + return false + }) + + return rerr +} + +func (ws *WasmSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshot.ExtensionPayloadReader) error { + if format == SnapshotFormat { + return ws.processAllItems(height, payloadReader, restoreV1, finalizeV1) + } + return snapshot.ErrUnknownFormat +} + +func restoreV1(_ sdk.Context, k *Keeper, compressedCode []byte) error { + if !types.IsGzip(compressedCode) { + return types.ErrInvalid.Wrap("not a gzip") + } + wasmCode, err := types.Uncompress(compressedCode, uint64(types.MaxWasmSize)) + if err != nil { + return errorsmod.Wrap(errorsmod.Wrap(err, "failed to store contract"), err.Error()) + } + + // FIXME: check which codeIDs the checksum matches?? + _, err = k.wasmVM.StoreCode(wasmCode) + if err != nil { + return errorsmod.Wrap(errorsmod.Wrap(err, "failed to store contract"), err.Error()) + } + return nil +} + +func finalizeV1(ctx sdk.Context, k *Keeper) error { + return nil +} + +func (ws *WasmSnapshotter) processAllItems( + height uint64, + payloadReader snapshot.ExtensionPayloadReader, + cb func(sdk.Context, *Keeper, []byte) error, + finalize func(sdk.Context, *Keeper) error, +) error { + ctx := sdk.NewContext(ws.cms, tmproto.Header{Height: int64(height)}, false, nil) + for { + payload, err := payloadReader() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if err := cb(ctx, ws.wasm, payload); err != nil { + return errorsmod.Wrap(err, "processing snapshot item") + } + } + + return finalize(ctx, ws.wasm) +} diff --git a/modules/light-clients/08-wasm/keeper/utils.go b/modules/light-clients/08-wasm/keeper/utils.go index b685a52ed39..c09f00bc149 100644 --- a/modules/light-clients/08-wasm/keeper/utils.go +++ b/modules/light-clients/08-wasm/keeper/utils.go @@ -62,3 +62,20 @@ func (l *LimitedReader) Read(p []byte) (n int, err error) { } return l.r.Read(p) } + +// GzipIt compresses the input ([]byte) +func GzipIt(input []byte) ([]byte, error) { + // Create gzip writer. + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write(input) + if err != nil { + return nil, err + } + err = w.Close() // You must close this first to flush the bytes to the buffer. + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} diff --git a/modules/light-clients/08-wasm/types/upgrade.go b/modules/light-clients/08-wasm/types/upgrade.go index 5c96cd2ff7e..10732b37c58 100644 --- a/modules/light-clients/08-wasm/types/upgrade.go +++ b/modules/light-clients/08-wasm/types/upgrade.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" ) diff --git a/modules/light-clients/08-wasm/types/validation.go b/modules/light-clients/08-wasm/types/validation.go index 8c4d6cda69d..96ff8c60366 100644 --- a/modules/light-clients/08-wasm/types/validation.go +++ b/modules/light-clients/08-wasm/types/validation.go @@ -1,6 +1,6 @@ package types -var MaxWasmSize = 3 * 1024 * 1024 +const MaxWasmSize = 3 * 1024 * 1024 func ValidateWasmCode(code []byte) (bool, error) { if len(code) == 0 { From 44a512d61fd978e425336aa01387efbf1e6bcd5e Mon Sep 17 00:00:00 2001 From: vuong177 Date: Wed, 12 Jul 2023 15:43:17 +0700 Subject: [PATCH 2/2] querier func in keeper.go --- .../light-clients/08-wasm/keeper/keeper.go | 28 +++++++- .../08-wasm/keeper/snapshotter.go | 2 +- modules/light-clients/08-wasm/types/utils.go | 71 +++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 modules/light-clients/08-wasm/types/utils.go diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go index 54f17fd6b3b..3843889eb04 100644 --- a/modules/light-clients/08-wasm/keeper/keeper.go +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -5,6 +5,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "fmt" "math" "path/filepath" "strings" @@ -199,10 +200,33 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { }) } return genesisState -func (k Keeper) IterateCodeInfos(ctx sdk.Context, fn func(id uint64, codeID string) (stop bool)) { +} + +// TODO: testing +func (k Keeper) IterateCodeInfos(ctx sdk.Context, fn func(codeID string) (stop bool)) { + store := ctx.KVStore(k.storeKey) + prefixStore := prefix.NewStore(store, []byte(fmt.Sprintf("%s/", types.PrefixCodeIDKey))) + iter := prefixStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + if fn(string(iter.Value())) { + break + } + } } +// TODO: testing func (k Keeper) GetWasmByte(ctx sdk.Context, codeID string) ([]byte, error) { - return nil, nil + store := ctx.KVStore(k.storeKey) + + byteCodeID, err := hex.DecodeString(codeID) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid code ID") + } + + codeKey := types.CodeID(byteCodeID) + wasmBytes := store.Get(codeKey) + return wasmBytes, nil } diff --git a/modules/light-clients/08-wasm/keeper/snapshotter.go b/modules/light-clients/08-wasm/keeper/snapshotter.go index 1d3c807b141..c77c973f19f 100644 --- a/modules/light-clients/08-wasm/keeper/snapshotter.go +++ b/modules/light-clients/08-wasm/keeper/snapshotter.go @@ -51,7 +51,7 @@ func (ws *WasmSnapshotter) SnapshotExtension(height uint64, payloadWriter snapsh seenBefore := make(map[string]bool) var rerr error - ws.wasm.IterateCodeInfos(ctx, func(id uint64, codeID string) bool { + ws.wasm.IterateCodeInfos(ctx, func(codeID string) bool { if seenBefore[codeID] { return false } diff --git a/modules/light-clients/08-wasm/types/utils.go b/modules/light-clients/08-wasm/types/utils.go new file mode 100644 index 00000000000..88d333490e7 --- /dev/null +++ b/modules/light-clients/08-wasm/types/utils.go @@ -0,0 +1,71 @@ +package types + +import ( + "bytes" + "compress/gzip" + "io" +) + +// Copied gzip feature from wasmd +// https://github.com/CosmWasm/wasmd/blob/v0.31.0/x/wasm/ioutils/utils.go + +// Note: []byte can never be const as they are inherently mutable + +// magic bytes to identify gzip. +// See https://www.ietf.org/rfc/rfc1952.txt +// and https://github.com/golang/go/blob/master/src/net/http/sniff.go#L186 +var gzipIdent = []byte("\x1F\x8B\x08") + +// IsGzip returns checks if the file contents are gzip compressed +func IsGzip(input []byte) bool { + return len(input) >= 3 && bytes.Equal(gzipIdent, input[0:3]) +} + +// 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, ErrWasmCodeTooLarge + } + zr, err := gzip.NewReader(bytes.NewReader(gzipSrc)) + if err != nil { + return nil, err + } + zr.Multistream(false) + defer zr.Close() + return io.ReadAll(limitReader(zr, int64(limit))) +} + +// limitReader returns a Reader that reads from r +// but stops with types.ErrLimit after n bytes. +// The underlying implementation is a *io.LimitedReader. +func limitReader(r io.Reader, n int64) io.Reader { + return &limitedReader{r: &io.LimitedReader{R: r, N: n}} +} + +type limitedReader struct { + r *io.LimitedReader +} + +func (l *limitedReader) Read(p []byte) (n int, err error) { + if l.r.N <= 0 { + return 0, ErrWasmCodeTooLarge + } + return l.r.Read(p) +} + +// GzipIt compresses the input ([]byte) +func GzipIt(input []byte) ([]byte, error) { + // Create gzip writer. + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write(input) + if err != nil { + return nil, err + } + err = w.Close() // You must close this first to flush the bytes to the buffer. + if err != nil { + return nil, err + } + + return b.Bytes(), nil +}