diff --git a/app/app.go b/app/app.go index 0a6dcc2f..9e8c92f0 100644 --- a/app/app.go +++ b/app/app.go @@ -550,6 +550,7 @@ func New( app.AccountKeeper, app.BankKeeper, app.provideFS, + app.WasmKeeper, ) wasmDir := filepath.Join(homePath, "wasm") diff --git a/proto/logic/v1beta2/query.proto b/proto/logic/v1beta2/query.proto index 5cc6611e..18099439 100644 --- a/proto/logic/v1beta2/query.proto +++ b/proto/logic/v1beta2/query.proto @@ -44,6 +44,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 exts = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } // QueryServiceAskResponse is response type for the QueryService/Ask RPC method. diff --git a/x/logic/interpreter/interpreter.go b/x/logic/interpreter/interpreter.go index 35a89059..425288d9 100644 --- a/x/logic/interpreter/interpreter.go +++ b/x/logic/interpreter/interpreter.go @@ -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 { diff --git a/x/logic/interpreter/registry.go b/x/logic/interpreter/registry.go index 63833c2d..38cd143f 100644 --- a/x/logic/interpreter/registry.go +++ b/x/logic/interpreter/registry.go @@ -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)) diff --git a/x/logic/keeper/grpc_query_ask.go b/x/logic/keeper/grpc_query_ask.go index c574cbce..9157e03a 100644 --- a/x/logic/keeper/grpc_query_ask.go +++ b/x/logic/keeper/grpc_query_ask.go @@ -43,7 +43,9 @@ func (k Keeper) Ask(ctx goctx.Context, req *types.QueryServiceAskRequest) (respo return k.execute( sdkCtx, req.Program, - req.Query) + req.Query, + req.Extensions, + ) } // withGasMeter returns a new context with a gas meter that has the given limit. diff --git a/x/logic/keeper/grpc_query_ask_test.go b/x/logic/keeper/grpc_query_ask_test.go index 8dd1458f..6343b0a5 100644 --- a/x/logic/keeper/grpc_query_ask_test.go +++ b/x/logic/keeper/grpc_query_ask_test.go @@ -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, @@ -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()) diff --git a/x/logic/keeper/grpc_query_params_test.go b/x/logic/keeper/grpc_query_params_test.go index f7eedb30..f71a7c17 100644 --- a/x/logic/keeper/grpc_query_params_test.go +++ b/x/logic/keeper/grpc_query_params_test.go @@ -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, @@ -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() { diff --git a/x/logic/keeper/interpreter.go b/x/logic/keeper/interpreter.go index 7da7f8f7..ccea69d9 100644 --- a/x/logic/keeper/interpreter.go +++ b/x/logic/keeper/interpreter.go @@ -2,6 +2,7 @@ package keeper import ( goctx "context" + "encoding/json" "math" "github.com/ichiban/prolog" @@ -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" @@ -37,11 +39,34 @@ func (k Keeper) enhanceContext(ctx goctx.Context) goctx.Context { 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.ExtensionManifest, 0) + manifestMsg := types.PrologQueryMsg { + PrologExtensionManifest: &types.PrologExtensionManifest {}, + } + 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.Manifests...) + } + + i, userOutputBuffer, err := k.newInterpreter(ctx, manifests) if err != nil { return nil, errorsmod.Wrapf(types.Internal, "error creating interpreter: %v", err.Error()) } @@ -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 []ExtensionManifest) (*prolog.Interpreter, *util.BoundedBuffer, error) { sdkctx := sdk.UnwrapSDKContext(ctx) params := k.GetParams(sdkctx) @@ -136,6 +161,13 @@ 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 + extendedRegistry[manifest.Name] = predicate.NewWasmExtension(manifest.ContractAddress, manifest.Name) + } + whitelistUrls := lo.Map( util.NonZeroOrDefault(interpreterParams.VirtualFilesFilter.Whitelist, []string{}), util.Indexed(util.ParseURLMust)) @@ -144,7 +176,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))), } diff --git a/x/logic/keeper/keeper.go b/x/logic/keeper/keeper.go index eb6ef225..6d2f8687 100644 --- a/x/logic/keeper/keeper.go +++ b/x/logic/keeper/keeper.go @@ -27,6 +27,7 @@ type ( authKeeper types.AccountKeeper bankKeeper types.BankKeeper fsProvider FSProvider + wasmKeeper types.WasmKeeper } ) @@ -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 { @@ -52,6 +54,7 @@ func NewKeeper( authKeeper: authKeeper, bankKeeper: bankKeeper, fsProvider: fsProvider, + wasmKeeper: wasmKeeper, } } diff --git a/x/logic/keeper/msg_server_test.go b/x/logic/keeper/msg_server_test.go index 04cb24da..f6a87297 100644 --- a/x/logic/keeper/msg_server_test.go +++ b/x/logic/keeper/msg_server_test.go @@ -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, @@ -69,6 +70,7 @@ func TestUpdateParams(t *testing.T) { func(ctx gocontext.Context) fs.FS { return fsProvider }, + wasmKeeper, ) msgServer := keeper.NewMsgServerImpl(*logicKeeper) diff --git a/x/logic/predicate/wasm.go b/x/logic/predicate/wasm.go new file mode 100644 index 00000000..6f76d381 --- /dev/null +++ b/x/logic/predicate/wasm.go @@ -0,0 +1,156 @@ +package predicate + +import ( + "fmt" + "context" + "encoding/json" + "strings" + + + "github.com/ichiban/prolog/engine" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/okp4/okp4d/x/logic/util" + "github.com/okp4/okp4d/x/logic/types" +) + +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 + } +} + +type PrologQueryMsg struct { + PrologExtensionManifest *PrologExtensionManifest `json:"prolog_extension_manifest,omitempty"` +} + +type PrologExtensionManifest struct { + PredicateName string `json:"predicate_name"` + Args []string `json:"args"` +} + +type PrologQueryResult struct { + Solve *PrologSolveResult `json:"solve"` +} + +type PrologSolveResult struct { + Solutions [][]string `json:"solutions"` + // Continuation *SolveContinuation `json:"continuation"` +} + +func solvePredicate(ctx sdk.Context, wasm types.WasmKeeper, contractAddr sdk.AccAddress, predicateName string, termArgs []engine.Term) ([][]engine.Term, error) { + args := make([]string, len(termArgs)) + for i, arg := range termArgs { + switch arg := arg.(type) { + case engine.Atom: + args[i] = arg.String() + case engine.Variable: + args[i] = "" + } + } + + msg := PrologQueryMsg { + PrologExtensionManifest: &PrologExtensionManifest { + PredicateName: predicateName, + Args: args, + }, + } + bz, err := json.Marshal(msg) + + resbz, err := wasm.QuerySmart(ctx, contractAddr, bz) + if err != nil { + return nil, err + } + + var res PrologQueryResult + err = json.Unmarshal(resbz, &res) + if err != nil { + return nil, err + } + + solutions := make([][]engine.Term, len(res.Solve.Solutions)) + for i, solution := range res.Solve.Solutions { + solutions[i] = make([]engine.Term, len(solution)) + for j, atom := range solution { + arg := termArgs[j] + switch arg := arg.(type) { + case engine.Atom: + if arg.String() != atom { + return nil, fmt.Errorf("unexpected atom: %s", atom) + } + solutions[i][j] = engine.NewAtom(atom) + case engine.Variable: + solutions[i][j] = engine.NewAtom(atom) // will be unified in CosmwasmQuery + } + } + } + + return solutions, 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) + + solutions, err := solvePredicate(sdkContext, wasmKeeper, contractAddress, predicate, args) + if err != nil { + return engine.Error(fmt.Errorf("%s: %w", predicate, err)) + } + + promises := make([]func(ctx context.Context) *engine.Promise, len(solutions)) + for i, solution := range solutions { + promise := func(ctx context.Context) *engine.Promise { + return engine.Unify( + vm, + Tuple(solution...), + Tuple(args[2:]...), + cont, + env, + ) + } + promises[i] = promise + } + + return engine.Delay(promises...) + }) +} +/* +func CosmWasmQuery3(vm *engine.VM, contractAddress engine.Term, predicateName engine.Term, arg0 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + return CosmWasmQuery("cosmwasm_query/3", vm, contractAddress, predicateName, []engine.Term{arg0}, cont, env) +} + +func CosmWasmQuery4(vm *engine.VM, contractAddress engine.Term, predicateName engine.Term, arg0, arg1 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + return CosmWasmQuery("cosmwasm_query/4", vm, contractAddress, predicateName, []engine.Term{arg0, arg1}, cont, env) +} + +func CosmWasmQuery5(vm *engine.VM, contractAddress engine.Term, predicateName engine.Term, arg0, arg1, arg2 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + return CosmWasmQuery("cosmwasm_query/5", vm, contractAddress, predicateName, []engine.Term{arg0, arg1, arg2}, cont, env) +} + +func CosmWasmQuery6(vm *engine.VM, contractAddress engine.Term, predicateName engine.Term, arg0, arg1, arg2, arg3 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { + return CosmWasmQuery("cosmwasm_query/6", vm, contractAddress, predicateName, []engine.Term{arg0, arg1, arg2, arg3}, cont, env) +} +*/ \ No newline at end of file diff --git a/x/logic/types/context.go b/x/logic/types/context.go index 780ae714..aa7d9d18 100644 --- a/x/logic/types/context.go +++ b/x/logic/types/context.go @@ -8,4 +8,6 @@ const ( AuthKeeperContextKey = ContextKey("authKeeper") // BankKeeperContextKey is the context key for the bank keeper. BankKeeperContextKey = ContextKey("bankKeeper") + + CosmWasmKeeperContextKey = ContextKey("cosmWasmKeeper") )