diff --git a/x/logic/interpreter/instrument.go b/x/logic/interpreter/instrument.go index f63f498c..d9736577 100644 --- a/x/logic/interpreter/instrument.go +++ b/x/logic/interpreter/instrument.go @@ -4,105 +4,123 @@ import ( "github.com/ichiban/prolog/engine" ) -type Hook[T any] func() T +type Invariant func(env *engine.Env) error -// Instrument0 is a higher order function that given a 0arg-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. -func Instrument0[T any](hook Hook[T], p engine.Predicate0) engine.Predicate0 { +// Instrument0 is a higher order function that given a 0arg-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. +func Instrument0(invariant Invariant, p engine.Predicate0) engine.Predicate0 { return func(vm *engine.VM, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, cont, env) } } -// Instrument1 is a higher order function that given a 1arg-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. -func Instrument1[T any](hook Hook[T], p engine.Predicate1) engine.Predicate1 { +// Instrument1 is a higher order function that given a 1arg-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. +func Instrument1(invariant Invariant, p engine.Predicate1) engine.Predicate1 { return func(vm *engine.VM, t1 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, cont, env) } } -// Instrument2 is a higher order function that given a 2args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. -func Instrument2[T any](hook Hook[T], p engine.Predicate2) engine.Predicate2 { +// Instrument2 is a higher order function that given a 2args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. +func Instrument2(invariant Invariant, p engine.Predicate2) engine.Predicate2 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, cont, env) } } -// Instrument3 is a higher order function that given a 3args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. -func Instrument3[T any](hook Hook[T], p engine.Predicate3) engine.Predicate3 { +// Instrument3 is a higher order function that given a 3args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. +func Instrument3(invariant Invariant, p engine.Predicate3) engine.Predicate3 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, cont engine.Cont, env *engine.Env, ) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, cont, env) } } -// Instrument4 is a higher order function that given a 4args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. +// Instrument4 is a higher order function that given a 4args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. // //nolint:lll -func Instrument4[T any](hook Hook[T], p engine.Predicate4) engine.Predicate4 { +func Instrument4(invariant Invariant, p engine.Predicate4) engine.Predicate4 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, t4, cont, env) } } -// Instrument5 is a higher order function that given a 5args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. +// Instrument5 is a higher order function that given a 5args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. // //nolint:lll -func Instrument5[T any](hook Hook[T], p engine.Predicate5) engine.Predicate5 { +func Instrument5(invariant Invariant, p engine.Predicate5) engine.Predicate5 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, t4, t5, cont, env) } } -// Instrument6 is a higher order function that given a 6args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. +// Instrument6 is a higher order function that given a 6args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. // //nolint:lll -func Instrument6[T any](hook Hook[T], p engine.Predicate6) engine.Predicate6 { +func Instrument6(invariant Invariant, p engine.Predicate6) engine.Predicate6 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, t4, t5, t6, cont, env) } } -// Instrument7 is a higher order function that given a 7args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. +// Instrument7 is a higher order function that given a 7args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. // //nolint:lll -func Instrument7[T any](hook Hook[T], p engine.Predicate7) engine.Predicate7 { +func Instrument7(invariant Invariant, p engine.Predicate7) engine.Predicate7 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, t7 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, t4, t5, t6, t7, cont, env) } } -// Instrument8 is a higher order function that given a 8args-predicate and a hook returns a new predicate that calls the -// hook before calling the predicate. +// Instrument8 is a higher order function that given a 8args-predicate and an invariant returns a new predicate that calls the +// invariant before calling the predicate. // //nolint:lll -func Instrument8[T any](hook Hook[T], p engine.Predicate8) engine.Predicate8 { +func Instrument8(invariant Invariant, p engine.Predicate8) engine.Predicate8 { return func(vm *engine.VM, t1 engine.Term, t2 engine.Term, t3 engine.Term, t4 engine.Term, t5 engine.Term, t6 engine.Term, t7 engine.Term, t8 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise { - hook() + if err := invariant(env); err != nil { + return engine.Error(err) + } return p(vm, t1, t2, t3, t4, t5, t6, t7, t8, cont, env) } diff --git a/x/logic/interpreter/interpreter.go b/x/logic/interpreter/interpreter.go index 4a316998..eaabf567 100644 --- a/x/logic/interpreter/interpreter.go +++ b/x/logic/interpreter/interpreter.go @@ -8,29 +8,38 @@ import ( "github.com/ichiban/prolog" "github.com/ichiban/prolog/engine" - - storetypes "cosmossdk.io/store/types" ) -// Predicates is a map of predicate names to their execution costs. -type Predicates map[string]uint64 - // Option is a function that configures an Interpreter. 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 storetypes.GasMeter) Option { +// See WithPredicate for more details. +func WithPredicates(ctx goctx.Context, predicates []string, hook Hook) Option { return func(i *prolog.Interpreter) error { - for predicate, cost := range predicates { - if err := Register(i, predicate, cost, meter); err != nil { - return fmt.Errorf("error registering predicate '%s': %w", predicate, err) + for _, predicate := range predicates { + if err := WithPredicate(ctx, predicate, hook)(i); err != nil { + return err } } return nil } } +// WithPredicate configures the interpreter to register the specified predicate with the specified hook. +// The hook is a function that is called before the predicate is executed and can be used to check some conditions, +// like the gas consumption or the permission to execute the predicate. +// +// The predicates names must be present in the registry, otherwise the function will return an error. +func WithPredicate(_ goctx.Context, predicate string, hook Hook) Option { + return func(i *prolog.Interpreter) error { + if err := Register(i, predicate, hook); err != nil { + return fmt.Errorf("error registering predicate '%s': %w", predicate, err) + } + return nil + } +} + // WithBootstrap configures the interpreter to compile the specified bootstrap script to serve as setup context. // If compilation of the bootstrap script fails, the function will return an error. func WithBootstrap(ctx goctx.Context, bootstrap string) Option { diff --git a/x/logic/interpreter/registry.go b/x/logic/interpreter/registry.go index 18e2877c..d80371b8 100644 --- a/x/logic/interpreter/registry.go +++ b/x/logic/interpreter/registry.go @@ -8,8 +8,6 @@ import ( "github.com/ichiban/prolog" "github.com/ichiban/prolog/engine" - storetypes "cosmossdk.io/store/types" - "github.com/okp4/okp4d/x/logic/predicate" ) @@ -131,6 +129,8 @@ var RegistryNames = func() []string { return names }() +type Hook = func(functor string) func(env *engine.Env) error + // Register registers a well-known predicate in the interpreter with support for consumption measurement. // name is the name of the predicate in the form of "atom/arity". // cost is the cost of executing the predicate. @@ -138,7 +138,7 @@ var RegistryNames = func() []string { // executing the predicate(ctx). // //nolint:lll -func Register(i *prolog.Interpreter, name string, cost uint64, meter storetypes.GasMeter) error { +func Register(i *prolog.Interpreter, name string, hook Hook) error { if p, ok := registry[name]; ok { parts := strings.Split(name, "/") if len(parts) == 2 { @@ -148,31 +148,27 @@ func Register(i *prolog.Interpreter, name string, cost uint64, meter storetypes. return err } - hook := func() storetypes.Gas { - meter.ConsumeGas(cost, fmt.Sprintf("predicate %s", name)) - - return meter.GasRemaining() - } + invariant := hook(name) switch arity { case 0: - i.Register0(atom, Instrument0(hook, p.(func(*engine.VM, engine.Cont, *engine.Env) *engine.Promise))) + i.Register0(atom, Instrument0(invariant, p.(func(*engine.VM, engine.Cont, *engine.Env) *engine.Promise))) case 1: - i.Register1(atom, Instrument1(hook, p.(func(*engine.VM, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register1(atom, Instrument1(invariant, p.(func(*engine.VM, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 2: - i.Register2(atom, Instrument2(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register2(atom, Instrument2(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 3: - i.Register3(atom, Instrument3(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register3(atom, Instrument3(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 4: - i.Register4(atom, Instrument4(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register4(atom, Instrument4(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 5: - i.Register5(atom, Instrument5(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register5(atom, Instrument5(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 6: - i.Register6(atom, Instrument6(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register6(atom, Instrument6(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 7: - i.Register7(atom, Instrument7(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register7(atom, Instrument7(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) case 8: - i.Register8(atom, Instrument8(hook, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) + i.Register8(atom, Instrument8(invariant, p.(func(*engine.VM, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Term, engine.Cont, *engine.Env) *engine.Promise))) default: panic(fmt.Sprintf("unsupported arity: %s", name)) } diff --git a/x/logic/keeper/interpreter.go b/x/logic/keeper/interpreter.go index 37d3f4ce..379c0800 100644 --- a/x/logic/keeper/interpreter.go +++ b/x/logic/keeper/interpreter.go @@ -5,10 +5,12 @@ import ( "math" "github.com/ichiban/prolog" + "github.com/ichiban/prolog/engine" "github.com/samber/lo" errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -86,19 +88,31 @@ func (k Keeper) newInterpreter(ctx context.Context) (*prolog.Interpreter, *util. whitelistPredicates := util.NonZeroOrDefault(interpreterParams.PredicatesFilter.Whitelist, interpreter.RegistryNames) blacklistPredicates := interpreterParams.PredicatesFilter.Blacklist - predicates := lo.Reduce( - lo.Map( - lo.Filter( - interpreter.RegistryNames, - util.Indexed(util.WhitelistBlacklistMatches(whitelistPredicates, blacklistPredicates, prolog2.PredicateMatches))), - toPredicate( - nonNilNorZeroOrDefaultUint64(gasPolicy.DefaultPredicateCost, defaultPredicateCost), - gasPolicy.GetPredicateCosts())), - func(agg interpreter.Predicates, item lo.Tuple2[string, uint64], _ int) interpreter.Predicates { - agg[item.A] = item.B - return agg - }, - interpreter.Predicates{}) + + hook := func(predicate string) func(env *engine.Env) (err error) { + return func(env *engine.Env) (err error) { + if !util.WhitelistBlacklistMatches(whitelistPredicates, blacklistPredicates, prolog2.PredicateMatches)(predicate) { + return engine.PermissionError( + prolog2.AtomOperationExecute, prolog2.AtomPermissionForbiddenPredicate, engine.NewAtom(predicate), env) + } + cost := lookupCost(predicate, defaultPredicateCost, gasPolicy.PredicateCosts) + + defer func() { + if r := recover(); r != nil { + if gasError, ok := r.(storetypes.ErrorOutOfGas); ok { + err = engine.ResourceError(prolog2.ResourceGas(gasError.Descriptor, gasMeter.GasConsumed(), gasMeter.Limit()), env) + return + } + + panic(r) + } + }() + + gasMeter.ConsumeGas(cost, predicate) + + return err + } + } whitelistUrls := lo.Map( util.NonZeroOrDefault(interpreterParams.VirtualFilesFilter.Whitelist, []string{}), @@ -108,7 +122,7 @@ func (k Keeper) newInterpreter(ctx context.Context) (*prolog.Interpreter, *util. util.Indexed(util.ParseURLMust)) options := []interpreter.Option{ - interpreter.WithPredicates(ctx, predicates, gasMeter), + interpreter.WithPredicates(ctx, interpreter.RegistryNames, hook), interpreter.WithBootstrap(ctx, util.NonZeroOrDefault(interpreterParams.GetBootstrap(), bootstrap.Bootstrap())), interpreter.WithFS(fs.NewFilteredFS(whitelistUrls, blacklistUrls, k.fsProvider(ctx))), } @@ -134,18 +148,14 @@ func checkLimits(request *types.QueryServiceAskRequest, limits types.Limits) err return nil } -// toPredicate converts the given predicate costs to a function that returns the cost for the given predicate as -// a pair of predicate name and cost. -func toPredicate(defaultCost uint64, predicateCosts []types.PredicateCost) func(string, int) lo.Tuple2[string, uint64] { - return func(predicate string, _ int) lo.Tuple2[string, uint64] { - for _, c := range predicateCosts { - if prolog2.PredicateMatches(predicate)(c.Predicate) { - return lo.T2(predicate, nonNilNorZeroOrDefaultUint64(c.Cost, defaultCost)) - } +func lookupCost(predicate string, defaultCost uint64, costs []types.PredicateCost) uint64 { + for _, c := range costs { + if prolog2.PredicateMatches(predicate)(c.Predicate) { + return nonNilNorZeroOrDefaultUint64(c.Cost, defaultCost) } - - return lo.T2(predicate, defaultCost) } + + return defaultCost } // nonNilNorZeroOrDefaultUint64 returns the value of the given sdkmath.Uint if it is not nil and not zero, otherwise it returns the diff --git a/x/logic/prolog/error.go b/x/logic/prolog/error.go index 768919ea..18b446a3 100644 --- a/x/logic/prolog/error.go +++ b/x/logic/prolog/error.go @@ -2,6 +2,8 @@ package prolog import ( "github.com/ichiban/prolog/engine" + + "cosmossdk.io/store/types" ) var ( @@ -62,6 +64,7 @@ var ( AtomValidEmptyList = engine.NewAtom("empty_list") ) +// ValidEncoding returns a term representing the valid encoding with the given name. func ValidEncoding(encoding string) engine.Term { return AtomValidEncoding.Apply(engine.NewAtom(encoding)) } @@ -79,19 +82,35 @@ var ( // The module resource is the representation of the module with which the interaction is made. // The module resource is denoted as a compound with the name of the module. AtomResourceModule = engine.NewAtom("resource_module") + // AtomResourceGas is the atom denoting the "gas" resource. + AtomResourceGas = engine.NewAtom("gas") ) +// ResourceContext returns a term representing the context resource. func ResourceContext() engine.Term { return AtomResourceContext } +// ResourceModule returns a term representing the module resource with the given name. func ResourceModule(module string) engine.Term { return AtomResourceModule.Apply(engine.NewAtom(module)) } -var AtomOperationInput = engine.NewAtom("input") +// ResourceGas returns a term representing the gas resource with the given descriptor, consumed and limit at the +// given context. +func ResourceGas(descriptor string, consumed types.Gas, limit types.Gas) engine.Term { + return AtomResourceGas.Apply(engine.NewAtom(descriptor), engine.Integer(int64(consumed)), engine.Integer(int64(limit))) +} -var AtomPermissionTypeStream = engine.NewAtom("stream") +var ( + AtomOperationInput = engine.NewAtom("input") + AtomOperationExecute = engine.NewAtom("execute") +) + +var ( + AtomPermissionTypeStream = engine.NewAtom("stream") + AtomPermissionForbiddenPredicate = engine.NewAtom("forbidden_predicate") +) var AtomObjectTypeSourceSink = engine.NewAtom("source_sink")