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

CosmWasm custom predicate extension #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ chain-init: build ## Initialize the blockchain with default settings.
chain-start: build ## Start the blockchain with existing configuration (see chain-init)
@echo "${COLOR_CYAN} 🛠️ Starting chain ${COLOR_RESET}${CHAIN}${COLOR_CYAN} with configuration ${COLOR_YELLOW}${CHAIN_HOME}${COLOR_RESET}"; \
${CHAIN_BINARY} start --moniker ${CHAIN_MONIKER} \
--home ${CHAIN_HOME}
--home ${CHAIN_HOME} --trace

chain-stop: ## Stop the blockchain
@echo "${COLOR_CYAN} ✋️ Stopping chain ${COLOR_RESET}${CHAIN}${COLOR_CYAN} with configuration ${COLOR_YELLOW}${CHAIN_HOME}${COLOR_RESET}"
Expand Down
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ func New(
app.AccountKeeper,
app.BankKeeper,
app.provideFS,
app.WasmKeeper,
)

wasmDir := filepath.Join(homePath, "wasm")
Expand Down
2 changes: 2 additions & 0 deletions proto/logic/v1beta2/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "logic/v1beta2/params.proto";
import "logic/v1beta2/types.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "github.com/okp4/okp4d/x/logic/types";

Expand Down Expand Up @@ -44,6 +45,7 @@ message QueryServiceAskRequest {
string program = 1 [(gogoproto.moretags) = "yaml:\"program\",omitempty"];
// query is the query string to be executed.
string query = 2 [(gogoproto.moretags) = "yaml:\"query\",omitempty"];
repeated string extensions = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

// QueryServiceAskResponse is response type for the QueryService/Ask RPC method.
Expand Down
2 changes: 1 addition & 1 deletion x/logic/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Option func(*prolog.Interpreter) error

// WithPredicates configures the interpreter to register the specified predicates.
// The predicates names must be present in the registry, otherwise the function will return an error.
func WithPredicates(_ goctx.Context, predicates Predicates, meter sdk.GasMeter) Option {
func WithPredicates(_ goctx.Context, registry map[string]any, predicates Predicates, meter sdk.GasMeter) Option {
return func(i *prolog.Interpreter) error {
for predicate, cost := range predicates {
if err := Register(i, predicate, cost, meter); err != nil {
Expand Down
9 changes: 9 additions & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ var registry = map[string]any{
"read_string/3": predicate.ReadString,
}

func NewRegistry() map[string]any {
r := make(map[string]any)
for k, v := range registry {
r[k] = v
}

return r
}

// RegistryNames is the list of the predicate names in the Registry.
var RegistryNames = func() []string {
names := make([]string, 0, len(registry))
Expand Down
9 changes: 8 additions & 1 deletion x/logic/keeper/grpc_query_ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,20 @@ func (k Keeper) Ask(ctx goctx.Context, req *types.QueryServiceAskRequest) (respo
panic(r)
}
}()

sdkCtx.GasMeter().ConsumeGas(sdkCtx.GasMeter().GasConsumed(), types.ModuleName)

extensionAddrs := make([]sdk.AccAddress, len(req.Extensions))
for i, ext := range req.Extensions {
extensionAddrs[i] = sdk.MustAccAddressFromBech32(ext)
}
//nolint:contextcheck
return k.execute(
sdkCtx,
req.Program,
req.Query)
req.Query,
extensionAddrs,
)
}

// withGasMeter returns a new context with a gas meter that has the given limit.
Expand Down
2 changes: 2 additions & 0 deletions x/logic/keeper/grpc_query_ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func TestGRPCAsk(t *testing.T) {
accountKeeper := logictestutil.NewMockAccountKeeper(ctrl)
bankKeeper := logictestutil.NewMockBankKeeper(ctrl)
fsProvider := logictestutil.NewMockFS(ctrl)
wasmKeeper := logictestutil.NewMockWasmKeeper(ctrl)

logicKeeper := keeper.NewKeeper(
encCfg.Codec,
Expand All @@ -108,6 +109,7 @@ func TestGRPCAsk(t *testing.T) {
func(ctx gocontext.Context) fs.FS {
return fsProvider
},
wasmKeeper,
)
err := logicKeeper.SetParams(testCtx.Ctx, types.DefaultParams())

Expand Down
2 changes: 2 additions & 0 deletions x/logic/keeper/grpc_query_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func TestGRPCParams(t *testing.T) {
accountKeeper := logictestutil.NewMockAccountKeeper(ctrl)
bankKeeper := logictestutil.NewMockBankKeeper(ctrl)
fsProvider := logictestutil.NewMockFS(ctrl)
wasmKeeper := logictestutil.NewMockWasmKeeper(ctrl)

logicKeeper := keeper.NewKeeper(
encCfg.Codec,
Expand All @@ -72,6 +73,7 @@ func TestGRPCParams(t *testing.T) {
func(ctx gocontext.Context) fs.FS {
return fsProvider
},
wasmKeeper,
)

Convey("and given params to the keeper", func() {
Expand Down
44 changes: 40 additions & 4 deletions x/logic/keeper/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
goctx "context"
"encoding/json"
"math"

"github.com/ichiban/prolog"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/okp4/okp4d/x/logic/fs"
"github.com/okp4/okp4d/x/logic/interpreter"
"github.com/okp4/okp4d/x/logic/interpreter/bootstrap"
"github.com/okp4/okp4d/x/logic/predicate"
"github.com/okp4/okp4d/x/logic/meter"
"github.com/okp4/okp4d/x/logic/types"
"github.com/okp4/okp4d/x/logic/util"
Expand All @@ -34,14 +36,37 @@ func (k Keeper) enhanceContext(ctx goctx.Context) goctx.Context {
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx = sdkCtx.WithValue(types.AuthKeeperContextKey, k.authKeeper)
sdkCtx = sdkCtx.WithValue(types.BankKeeperContextKey, k.bankKeeper)
sdkCtx = sdkCtx.WithValue(types.CosmWasmKeeperContextKey, k.wasmKeeper)
return sdkCtx
}

func (k Keeper) execute(ctx goctx.Context, program, query string) (*types.QueryServiceAskResponse, error) {
func (k Keeper) execute(ctx goctx.Context, program, query string, exts []sdk.AccAddress) (*types.QueryServiceAskResponse, error) {
ctx = k.enhanceContext(ctx)
sdkCtx := sdk.UnwrapSDKContext(ctx)

i, userOutputBuffer, err := k.newInterpreter(ctx)
manifests := make([]types.PredicateManifest, 0)
manifestMsg := types.PrologQueryRequest{
PrologExtensionManifest: &types.PrologExtensionManifestRequest{},
}
manifestMsgBz, err := json.Marshal(manifestMsg)
if err != nil {
return nil, errorsmod.Wrapf(types.Internal, "error marshalling manifest request: %v", err.Error())
}
for _, ext := range exts {
resbz, err := k.wasmKeeper.QuerySmart(sdkCtx, ext, manifestMsgBz)
if err != nil {
return nil, errorsmod.Wrapf(types.InvalidArgument, "error querying extension manifest: %v", err.Error())
}

var manifestRes types.PrologQueryResponse
if err := json.Unmarshal(resbz, &manifestRes); err != nil {
return nil, errorsmod.Wrapf(types.Internal, "error unmarshalling manifest response: %v", err.Error())
}

manifests = append(manifests, manifestRes.PrologExtensionManifest.Predicates...)
}

i, userOutputBuffer, err := k.newInterpreter(ctx, manifests)
if err != nil {
return nil, errorsmod.Wrapf(types.Internal, "error creating interpreter: %v", err.Error())
}
Expand Down Expand Up @@ -111,7 +136,7 @@ func checkLimits(request *types.QueryServiceAskRequest, limits types.Limits) err
}

// newInterpreter creates a new interpreter properly configured.
func (k Keeper) newInterpreter(ctx goctx.Context) (*prolog.Interpreter, *util.BoundedBuffer, error) {
func (k Keeper) newInterpreter(ctx goctx.Context, manifests []types.PredicateManifest) (*prolog.Interpreter, *util.BoundedBuffer, error) {
sdkctx := sdk.UnwrapSDKContext(ctx)
params := k.GetParams(sdkctx)

Expand All @@ -136,6 +161,17 @@ func (k Keeper) newInterpreter(ctx goctx.Context) (*prolog.Interpreter, *util.Bo
},
interpreter.Predicates{})

extendedRegistry := interpreter.NewRegistry()

for _, manifest := range manifests {
predicates[manifest.Name] = manifest.Cost
contractAddress, err := sdk.AccAddressFromBech32(manifest.Address)
if err != nil {
return nil, nil, errorsmod.Wrapf(types.InvalidArgument, "error parsing contract address: %v", err.Error())
}
extendedRegistry[manifest.Name] = predicate.NewWasmExtension(contractAddress, manifest.Name)
}

whitelistUrls := lo.Map(
util.NonZeroOrDefault(interpreterParams.VirtualFilesFilter.Whitelist, []string{}),
util.Indexed(util.ParseURLMust))
Expand All @@ -144,7 +180,7 @@ func (k Keeper) newInterpreter(ctx goctx.Context) (*prolog.Interpreter, *util.Bo
util.Indexed(util.ParseURLMust))

options := []interpreter.Option{
interpreter.WithPredicates(ctx, predicates, gasMeter),
interpreter.WithPredicates(ctx, extendedRegistry, predicates, gasMeter),
interpreter.WithBootstrap(ctx, util.NonZeroOrDefault(interpreterParams.GetBootstrap(), bootstrap.Bootstrap())),
interpreter.WithFS(fs.NewFilteredFS(whitelistUrls, blacklistUrls, k.fsProvider(ctx))),
}
Expand Down
3 changes: 3 additions & 0 deletions x/logic/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type (
authKeeper types.AccountKeeper
bankKeeper types.BankKeeper
fsProvider FSProvider
wasmKeeper types.WasmKeeper
}
)

Expand All @@ -38,6 +39,7 @@ func NewKeeper(
authKeeper types.AccountKeeper,
bankKeeper types.BankKeeper,
fsProvider FSProvider,
wasmKeeper types.WasmKeeper,
) *Keeper {
// ensure gov module account is set and is not nil
if err := sdk.VerifyAddressFormat(authority); err != nil {
Expand All @@ -52,6 +54,7 @@ func NewKeeper(
authKeeper: authKeeper,
bankKeeper: bankKeeper,
fsProvider: fsProvider,
wasmKeeper: wasmKeeper,
}
}

Expand Down
2 changes: 2 additions & 0 deletions x/logic/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestUpdateParams(t *testing.T) {
accountKeeper := logictestutil.NewMockAccountKeeper(ctrl)
bankKeeper := logictestutil.NewMockBankKeeper(ctrl)
fsProvider := logictestutil.NewMockFS(ctrl)
wasmKeeper := logictestutil.NewMockWasmKeeper(ctrl)

logicKeeper := keeper.NewKeeper(
encCfg.Codec,
Expand All @@ -69,6 +70,7 @@ func TestUpdateParams(t *testing.T) {
func(ctx gocontext.Context) fs.FS {
return fsProvider
},
wasmKeeper,
)

msgServer := keeper.NewMsgServerImpl(*logicKeeper)
Expand Down
105 changes: 105 additions & 0 deletions x/logic/predicate/wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package predicate

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/ichiban/prolog/engine"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/okp4/okp4d/x/logic/types"
"github.com/okp4/okp4d/x/logic/util"
)

func NewWasmExtension(contractAddress sdk.AccAddress, name string) any {
parts := strings.Split(name, "/")
if len(parts) != 2 {
return nil
}

arity := parts[1]
switch arity {
case "1":
return func(vm *engine.VM, arg0 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return CosmWasmQuery(name, vm, contractAddress, []engine.Term{arg0}, cont, env)
}
case "2":
return func(vm *engine.VM, arg0, arg1 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return CosmWasmQuery(name, vm, contractAddress, []engine.Term{arg0, arg1}, cont, env)
}
case "3":
return func(vm *engine.VM, arg0, arg1, arg2 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return CosmWasmQuery(name, vm, contractAddress, []engine.Term{arg0, arg1, arg2}, cont, env)
}
default:
return nil
}
}

func solvePredicate(ctx sdk.Context, vm *engine.VM, wasm types.WasmKeeper, contractAddr sdk.AccAddress, predicateName string, termArgs []engine.Term, cont engine.Cont, env *engine.Env) (func(context.Context) *engine.Promise, error) {
args := make([]types.WasmTerm, len(termArgs))
for i, arg := range termArgs {
switch arg := arg.(type) {
case engine.Atom:
atomstr := arg.String()
args[i] = types.WasmTerm{Atom: &atomstr}
case engine.Variable:
varid := int64(arg)
args[i] = types.WasmTerm{Var: &varid}
}
}

msg := types.PrologQueryRequest{
RunPredicate: &types.RunPredicateRequest{
Name: predicateName,
Args: args,
},
}
bz, err := json.Marshal(msg)

resbz, err := wasm.QuerySmart(ctx, contractAddr, bz)
if err != nil {
return nil, err
}

var res types.PrologQueryResponse
err = json.Unmarshal(resbz, &res)
if err != nil {
return nil, err
}

return func(ctx context.Context) *engine.Promise {
xs := make([]engine.Term, len(res.RunPredicate.Commands))
ys := make([]engine.Term, len(res.RunPredicate.Commands))
for i, command := range res.RunPredicate.Commands {
xs[i] = command.Unify[0].ToTerm()
ys[i] = command.Unify[1].ToTerm()
}
return engine.Unify(
vm,
Tuple(xs...),
Tuple(ys...),
cont,
env,
)
}, nil
}

func CosmWasmQuery(predicate string, vm *engine.VM, contractAddress sdk.AccAddress, args []engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
sdkContext, err := util.UnwrapSDKContext(ctx)
if err != nil {
return engine.Error(err)
}
wasmKeeper := sdkContext.Value(types.CosmWasmKeeperContextKey).(types.WasmKeeper)

unification, err := solvePredicate(sdkContext, vm, wasmKeeper, contractAddress, predicate, args, cont, env)
if err != nil {
return engine.Error(fmt.Errorf("%s: %w", predicate, err))
}

return engine.Delay(unification)
})
}
2 changes: 2 additions & 0 deletions x/logic/types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ const (
AuthKeeperContextKey = ContextKey("authKeeper")
// BankKeeperContextKey is the context key for the bank keeper.
BankKeeperContextKey = ContextKey("bankKeeper")

CosmWasmKeeperContextKey = ContextKey("cosmWasmKeeper")
)