Skip to content

Commit

Permalink
refactor(logic)!: improve predicate call policy (blacklist, gas)
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Feb 29, 2024
1 parent 16e42f8 commit 17fdaed
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 90 deletions.
92 changes: 55 additions & 37 deletions x/logic/interpreter/instrument.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 19 additions & 10 deletions x/logic/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 13 additions & 17 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -131,14 +129,16 @@ 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.
// meter is the gas meter object that is called when the predicate is called and which allows to count the cost of
// 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 {
Expand All @@ -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))
}
Expand Down
58 changes: 34 additions & 24 deletions x/logic/keeper/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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{}),
Expand All @@ -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))),
}
Expand All @@ -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
Expand Down
Loading

0 comments on commit 17fdaed

Please sign in to comment.