Skip to content

Commit efd39e6

Browse files
authored
Merge pull request #2263 from CosmWasm/co/raw-range
Implement `RawRange` query
2 parents 0461d17 + e1bc372 commit efd39e6

File tree

5 files changed

+333
-0
lines changed

5 files changed

+333
-0
lines changed

x/wasm/keeper/keeper.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,45 @@ func (k Keeper) QueryRaw(ctx context.Context, contractAddress sdk.AccAddress, ke
939939
return prefixStore.Get(key)
940940
}
941941

942+
func (k Keeper) QueryRawRange(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte) {
943+
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "query-raw-range")
944+
945+
prefixStoreKey := types.GetContractStorePrefix(contractAddress)
946+
prefixStore := prefix.NewStore(runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx)), prefixStoreKey)
947+
var iter storetypes.Iterator
948+
if reverse {
949+
iter = prefixStore.ReverseIterator(start, end)
950+
} else {
951+
iter = prefixStore.Iterator(start, end)
952+
}
953+
defer iter.Close()
954+
955+
// Make sure to set to empty array because the contract doesn't expect a null JSON value
956+
results = []wasmvmtypes.RawRangeEntry{}
957+
958+
var count uint16 = 0
959+
for ; iter.Valid(); iter.Next() {
960+
// keep track of count to honor the limit
961+
if count == limit {
962+
break
963+
}
964+
count++
965+
966+
// add key-value pair
967+
results = append(results, wasmvmtypes.RawRangeEntry{Key: iter.Key(), Value: iter.Value()})
968+
}
969+
970+
if iter.Valid() {
971+
// if there are more results, set the next key
972+
key := iter.Key()
973+
nextKey = key
974+
} else {
975+
nextKey = nil
976+
}
977+
978+
return results, nextKey
979+
}
980+
942981
// internal helper function
943982
func (k Keeper) contractInstance(ctx context.Context, contractAddress sdk.AccAddress) (types.ContractInfo, types.CodeInfo, wasmvm.KVStore, error) {
944983
store := k.storeService.OpenKVStore(ctx)

x/wasm/keeper/keeper_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package keeper
33
import (
44
"bytes"
55
_ "embed"
6+
"encoding/binary"
67
"encoding/json"
78
"errors"
89
"fmt"
@@ -51,6 +52,9 @@ var hackatomWasm []byte
5152
//go:embed testdata/replier.wasm
5253
var replierWasm []byte
5354

55+
//go:embed testdata/queue.wasm
56+
var queueWasm []byte
57+
5458
var AvailableCapabilities = []string{
5559
"iterator", "staking", "stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3",
5660
"cosmwasm_1_4", "cosmwasm_2_0", "cosmwasm_2_1", "cosmwasm_2_2", "ibc2",
@@ -2985,3 +2989,170 @@ func TestCheckDiscountEligibility(t *testing.T) {
29852989
})
29862990
}
29872991
}
2992+
2993+
func TestQueryRawRange(t *testing.T) {
2994+
ctx, keepers := CreateTestInput(t, false, AvailableCapabilities)
2995+
k := keepers.WasmKeeper
2996+
2997+
// Create queue contract and instantiate
2998+
creator := RandomAccountAddress(t)
2999+
codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, queueWasm, nil)
3000+
require.NoError(t, err)
3001+
initMsgBz := []byte("{}")
3002+
contractAddress, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "queue", nil)
3003+
3004+
type EnqueueMsg struct {
3005+
Value int32 `json:"value"`
3006+
}
3007+
type QueueExecMsg struct {
3008+
Enqueue *EnqueueMsg `json:"enqueue"`
3009+
// ...
3010+
}
3011+
3012+
// fill contract storage with 100 items
3013+
for i := range 100 {
3014+
enqueueMsg := QueueExecMsg{
3015+
Enqueue: &EnqueueMsg{Value: int32(i)},
3016+
}
3017+
execMsg, err := json.Marshal(enqueueMsg)
3018+
require.NoError(t, err)
3019+
_, err = keepers.ContractKeeper.Execute(ctx, contractAddress, creator, execMsg, nil)
3020+
require.NoError(t, err)
3021+
}
3022+
3023+
type QueueEntry struct {
3024+
key uint32
3025+
val int32
3026+
}
3027+
3028+
optUint32 := func(v uint32) *uint32 {
3029+
return &v
3030+
}
3031+
3032+
specs := map[string]struct {
3033+
start *uint32
3034+
end *uint32
3035+
limit uint16
3036+
reverse bool
3037+
expEntries []QueueEntry
3038+
expNext *uint32
3039+
}{
3040+
"non-existent range": {
3041+
start: optUint32(100),
3042+
end: optUint32(200),
3043+
limit: 10,
3044+
expEntries: []QueueEntry{},
3045+
expNext: nil,
3046+
},
3047+
"limited middle range": {
3048+
start: optUint32(10),
3049+
end: optUint32(50),
3050+
limit: 5,
3051+
expEntries: []QueueEntry{
3052+
{key: 10, val: 10},
3053+
{key: 11, val: 11},
3054+
{key: 12, val: 12},
3055+
{key: 13, val: 13},
3056+
{key: 14, val: 14},
3057+
},
3058+
expNext: optUint32(15),
3059+
},
3060+
"limited range with no end": {
3061+
start: optUint32(10),
3062+
end: nil,
3063+
limit: 2,
3064+
expEntries: []QueueEntry{
3065+
{key: 10, val: 10},
3066+
{key: 11, val: 11},
3067+
},
3068+
expNext: optUint32(12),
3069+
},
3070+
"limited range with no start": {
3071+
start: nil,
3072+
end: optUint32(50),
3073+
limit: 2,
3074+
expEntries: []QueueEntry{
3075+
{key: 0, val: 0},
3076+
{key: 1, val: 1},
3077+
},
3078+
expNext: optUint32(2),
3079+
},
3080+
"unbounded range": {
3081+
start: nil,
3082+
end: nil,
3083+
limit: 1,
3084+
expEntries: []QueueEntry{
3085+
{key: 0, val: 0},
3086+
},
3087+
expNext: optUint32(1),
3088+
},
3089+
"unbounded reversed range": {
3090+
start: nil,
3091+
end: nil,
3092+
limit: 1,
3093+
reverse: true,
3094+
expEntries: []QueueEntry{
3095+
{key: 99, val: 99},
3096+
},
3097+
expNext: optUint32(98),
3098+
},
3099+
"full bounded reversed range": {
3100+
start: optUint32(0),
3101+
end: optUint32(2),
3102+
limit: 100,
3103+
reverse: true,
3104+
expEntries: []QueueEntry{
3105+
{key: 1, val: 1},
3106+
{key: 0, val: 0},
3107+
},
3108+
expNext: nil, // no next key because range is fully covered
3109+
},
3110+
"start > end, reversed": {
3111+
start: optUint32(50),
3112+
end: optUint32(10),
3113+
limit: 5,
3114+
reverse: true,
3115+
expEntries: []QueueEntry{},
3116+
expNext: nil,
3117+
},
3118+
}
3119+
3120+
toBytes := func(v *uint32) []byte {
3121+
if v == nil {
3122+
return nil
3123+
}
3124+
return binary.BigEndian.AppendUint32(nil, *v)
3125+
}
3126+
3127+
for name, spec := range specs {
3128+
t.Run(name, func(t *testing.T) {
3129+
// queue contract uses big endian encoded uint32 as key
3130+
startBytes := toBytes(spec.start)
3131+
endBytes := toBytes(spec.end)
3132+
3133+
entries, next := k.QueryRawRange(ctx, contractAddress, startBytes, endBytes, spec.limit, spec.reverse)
3134+
// contract cannot handle nil, so we disallow it
3135+
require.NotNil(t, entries)
3136+
3137+
// converting the entries we get back instead of the entries we put in the spec because
3138+
// it makes for easier to read test outputs (actual integers instead of byte arrays)
3139+
convertedEntries := make([]QueueEntry, len(entries))
3140+
for i, entry := range entries {
3141+
// values are json-encoded as `{"value":<value>}`
3142+
// so we need to unmarshal it and extract the value
3143+
var value EnqueueMsg
3144+
err := json.Unmarshal(entry.Value, &value)
3145+
require.NoError(t, err)
3146+
3147+
convertedEntries[i] = QueueEntry{
3148+
key: binary.BigEndian.Uint32(entry.Key),
3149+
val: value.Value,
3150+
}
3151+
}
3152+
3153+
expNextBz := toBytes(spec.expNext)
3154+
assert.Equal(t, spec.expEntries, convertedEntries)
3155+
assert.Equal(t, expNextBz, next)
3156+
})
3157+
}
3158+
}

x/wasm/keeper/query_plugins.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ type wasmQueryKeeper interface {
108108
contractMetaDataSource
109109
GetCodeInfo(ctx context.Context, codeID uint64) *types.CodeInfo
110110
QueryRaw(ctx context.Context, contractAddress sdk.AccAddress, key []byte) []byte
111+
QueryRawRange(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte)
111112
QuerySmart(ctx context.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error)
112113
IsPinnedCode(ctx context.Context, codeID uint64) bool
113114
}
@@ -705,6 +706,29 @@ func WasmQuerier(k wasmQueryKeeper) func(ctx sdk.Context, request *wasmvmtypes.W
705706
Checksum: info.CodeHash,
706707
}
707708
return json.Marshal(res)
709+
case request.RawRange != nil:
710+
contractAddr := request.RawRange.ContractAddr
711+
addr, err := sdk.AccAddressFromBech32(contractAddr)
712+
if err != nil {
713+
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, contractAddr)
714+
}
715+
716+
var reverse bool
717+
switch request.RawRange.Order {
718+
case "ascending":
719+
reverse = false
720+
case "descending":
721+
reverse = true
722+
default:
723+
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unknown order %s", request.RawRange.Order)
724+
}
725+
data, nextKey := k.QueryRawRange(ctx, addr, request.RawRange.Start, request.RawRange.End, request.RawRange.Limit, reverse)
726+
res := wasmvmtypes.RawRangeResponse{
727+
Data: data,
728+
NextKey: nextKey,
729+
}
730+
return json.Marshal(res)
731+
708732
}
709733
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown WasmQuery variant"}
710734
}

x/wasm/keeper/query_plugins_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,97 @@ func TestCodeInfoWasmQuerier(t *testing.T) {
587587
}
588588
}
589589

590+
func TestRawRangeWasmQuerier(t *testing.T) {
591+
myValidContractAddr := keeper.RandomBech32AccountAddress(t)
592+
validResponse := wasmvmtypes.RawRangeResponse{
593+
Data: []wasmvmtypes.RawRangeEntry{
594+
{
595+
Key: []byte("key1"),
596+
Value: []byte("value"),
597+
},
598+
},
599+
NextKey: nil,
600+
}
601+
var ctx sdk.Context
602+
specs := map[string]struct {
603+
req *wasmvmtypes.WasmQuery
604+
mock mockWasmQueryKeeper
605+
expRes wasmvmtypes.RawRangeResponse
606+
expErr bool
607+
}{
608+
"all good": {
609+
req: &wasmvmtypes.WasmQuery{
610+
RawRange: &wasmvmtypes.RawRangeQuery{
611+
ContractAddr: myValidContractAddr,
612+
Start: []byte("key0"),
613+
End: []byte("key2"),
614+
Limit: 10,
615+
Order: "ascending",
616+
},
617+
},
618+
mock: mockWasmQueryKeeper{
619+
QueryRawRangeFn: func(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte) {
620+
return validResponse.Data, validResponse.NextKey
621+
},
622+
},
623+
expRes: validResponse,
624+
},
625+
"all good - descending": {
626+
req: &wasmvmtypes.WasmQuery{
627+
RawRange: &wasmvmtypes.RawRangeQuery{
628+
ContractAddr: myValidContractAddr,
629+
Start: []byte("start"),
630+
End: []byte("end"),
631+
Limit: 10,
632+
Order: "descending",
633+
},
634+
},
635+
mock: mockWasmQueryKeeper{
636+
QueryRawRangeFn: func(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte) {
637+
return []wasmvmtypes.RawRangeEntry{}, nil
638+
},
639+
},
640+
expRes: wasmvmtypes.RawRangeResponse{
641+
Data: []wasmvmtypes.RawRangeEntry{},
642+
NextKey: nil,
643+
},
644+
},
645+
"invalid addr": {
646+
req: &wasmvmtypes.WasmQuery{
647+
RawRange: &wasmvmtypes.RawRangeQuery{
648+
ContractAddr: "not a valid addr",
649+
Order: "ascending",
650+
},
651+
},
652+
expErr: true,
653+
},
654+
"invalid order": {
655+
req: &wasmvmtypes.WasmQuery{
656+
RawRange: &wasmvmtypes.RawRangeQuery{
657+
ContractAddr: myValidContractAddr,
658+
Order: "not a valid order",
659+
},
660+
},
661+
expErr: true,
662+
},
663+
}
664+
665+
for name, spec := range specs {
666+
t.Run(name, func(t *testing.T) {
667+
q := keeper.WasmQuerier(spec.mock)
668+
gotBz, gotErr := q(ctx, spec.req)
669+
if spec.expErr {
670+
require.Error(t, gotErr)
671+
return
672+
}
673+
require.NoError(t, gotErr)
674+
var gotRes wasmvmtypes.RawRangeResponse
675+
require.NoError(t, json.Unmarshal(gotBz, &gotRes))
676+
assert.Equal(t, spec.expRes, gotRes)
677+
})
678+
}
679+
}
680+
590681
func TestQueryErrors(t *testing.T) {
591682
specs := map[string]struct {
592683
src error
@@ -628,6 +719,7 @@ type mockWasmQueryKeeper struct {
628719
GetContractInfoFn func(ctx context.Context, contractAddress sdk.AccAddress) *types.ContractInfo
629720
QueryRawFn func(ctx context.Context, contractAddress sdk.AccAddress, key []byte) []byte
630721
QuerySmartFn func(ctx context.Context, contractAddr sdk.AccAddress, req types.RawContractMessage) ([]byte, error)
722+
QueryRawRangeFn func(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte)
631723
IsPinnedCodeFn func(ctx context.Context, codeID uint64) bool
632724
GetCodeInfoFn func(ctx context.Context, codeID uint64) *types.CodeInfo
633725
}
@@ -646,6 +738,13 @@ func (m mockWasmQueryKeeper) QueryRaw(ctx context.Context, contractAddress sdk.A
646738
return m.QueryRawFn(ctx, contractAddress, key)
647739
}
648740

741+
func (m mockWasmQueryKeeper) QueryRawRange(ctx context.Context, contractAddress sdk.AccAddress, start, end []byte, limit uint16, reverse bool) (results []wasmvmtypes.RawRangeEntry, nextKey []byte) {
742+
if m.QueryRawRangeFn == nil {
743+
panic("not expected to be called")
744+
}
745+
return m.QueryRawRangeFn(ctx, contractAddress, start, end, limit, reverse)
746+
}
747+
649748
func (m mockWasmQueryKeeper) QuerySmart(ctx context.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) {
650749
if m.QuerySmartFn == nil {
651750
panic("not expected to be called")

x/wasm/keeper/testdata/queue.wasm

195 KB
Binary file not shown.

0 commit comments

Comments
 (0)