Skip to main content

x/act

Overview

The x/act module is a Cosmos SDK module executing messages (called Actions) under certain conditions, or Rules, defined by users.

This module implements the following concepts, which you can find in our Glossary:

Concepts

Rule

The Rule struct represents a set of user-defined conditions that must be met before something can be executed.

Users can register Rules on-chain, writing their expressions in the Intent-Specific Language.

Other modules can plug their variables into the execution runtime of Rules. This enables users to base their Rules on data available on-chain. To learn more, see Hooks.

See also Glossary: Approval Rule.

Action

An Action wraps another message. Each Action contains a Rule: when the conditions specified in the Rule are met, the wrapped message is executed.

When created, an Action has a pending state. When the wrapped message is executed, the Action state changes to completed. The creator of the Action can revoke it at any time, changing the Action status to revoked.

Optionally, it's possible to specify a timeout height for an Action. After this height is reached by the blockchain, the Action state will change to timeout.

An Action can be approved by one or more users. The addresses of the users that approved the Action are stored in its approvers field. These addresses can be used as boolean conditions in the Rule expression.

See also Glossary: Action.

Intent-Specific Language

The Intent-Specific Language (ISL) is a language designed to define Rules, functioning as a very simple smart contract language. Its current version is codenamed shield.

Here is an example of a basic Rule that is satisfied when one of two addresses approve an Action:

any(2, [warden1jdeysw88gtzz8da6qr6cqepl7ghleane5u46yh, warden1r4d7gh3ysfy3dz3nufpsmj4ad6t5qz2cs33xu3])

See also Glossary: ISL.

State

The x/act module keeps the state of the following primary objects:

  • Rules
  • Actions

To manage this state, the module also keeps the following indexes:

  • Action by address referenced in its Rule

Hooks

This section explains how other modules can hook into the x/act module, customizing its behavior.

Rule handlers

A Rule handler is a handler returning the Rule that will be applied to an Action.

Each Rule handler is associated with a certain message type. When a new Action is created, the x/act module invokes a Rule handler for the wrapped message type.

Handlers are invoked only once per Action, during its creation. They aren't invoked again every time the Action's Rule is re-evaluated.

Example

The code sample below is a dummy x/satellites module. It registers a handler for fetching the Rule of its MsgLaunchSatellite message:

package keeper

import (
// ...
acttypes "github.com/warden-protocol/wardenprotocol/warden/x/act/types/v1beta1"
)

func NewKeeper(
// ...
actKeeper types.ActKeeper,
) Keeper {
// ...

k := Keeper{...}
acttypes.Register(reg, k.launchSatelliteRule)

return k
}

func (k Keeper) launchSatelliteRule(ctx context.Context, msg *types.MsgLaunchSatellite) (acttypes.Rule, error) {
// for example, from here you can access the database to fetch specific Rules
satellite, err := k.satellites.Get(ctx, msg.SatelliteID)
return satellite.LaunchRule, err
}

Every time a new Action with a MsgLaunchSatellite message is created, the x/act module invokes launchSatelliteRule to fetch the Rule that will be applied to a particular satellite.

Rule preprocessing

After a Rule handler is invoked and the Rule is fetched, the x/act module invokes the registered Rule preprocessor. This enables other modules to expand some of the identifiers in the Rule expression into values, similar to a macro expansion.

Example

You can register a preprocessor in app.go. For example, an expander for the dummy x/satellites module from the previous section would look like this:

appConfig = depinject.Configs(
AppConfig(),
depinject.Supply(
// ...
func() ast.Expander {
return cosmoshield.NewExpanderManager(
cosmoshield.NewPrefixedExpander(
satellitetypes.ModuleName,
app.SatelliteKeeper.ShieldExpander(),
),
// add more expanders here
)
},

PrefixedExpander handles all identifiers that start with the module name (satellitetypes.ModuleName) followed by a dot. ShieldExpander receives the rest of the identifiers and returns any other abstract syntax tree node to replace it.

SatelliteKeeper could implement an expander like this:

// ast.Expander is defined like this:
type Expander interface {
Expand(ctx context.Context, ident *Identifier) (Expression, error)
}

type SatelliteExpander struct{Keeper}

func (k Keeper) ShieldExpander() ast.Expander { return SatelliteExpander{k} }

func (e SatelliteExpander) Expand(ctx context.Context, ident *Identifier) (Expression, error) {
if ident.Name == "123.cost" {
cost := e.k.GetSatelliteCost(ctx, 123) // access data provided by Keeper
return ast.NewIntegerLiteral(&ast.IntegerLiteral{
Value: cost,
}), nil
}
return nil, fmt.Errorf("unknown identifier: %s", ident.Value)
}

A user can then write a Rule – for example, to automatically approve any satellite launch for satellites with a cost lower than 100 or to require at least 2 out of 3 approvers:

satellite.123.cost <= 100 || any(2, [warden1j6yh, warden1rxu3, warden1r4d7])

When an Action is created, the Rule gets preprocessed by the expander, resulting in the following new Rule (assuming the cost for the satellite 123 is 900):

900 <= 100 || any(2, [warden1j6yh, warden1rxu3, warden1r4d7])

Rule evaluation

warning

This feature is still in development and is not available yet.

The x/act module re-evaluates the Rules of pending Actions to check if they are satisfied by the current state of the blockchain.

Every time an Action is approved, it gets re-evaluated. During evaluation, all identifiers left after preprocessing must have an associated value in the environment.

Example

The preprocessing example uses a value that needs to be fetched only once – when an Action is created. By contrast, in the evaluation example below, a value is provided in the runtime environment and can be re-fetched at every evaluation. This approach is suitable for values that change over time.

You can register a module environment in app.go. For example, registering an environment for the dummy x/satellites module would look like this:

appConfig = depinject.Configs(
AppConfig(),
depinject.Supply(
// ...
func() shield.Environment {
return cosmoshield.NewEnvironmentManager(
cosmoshield.NewPrefixedEnvironment(
satellitetypes.ModuleName,
app.SatelliteKeeper.ShieldEnv(),
),
// add more environments here
)
},

PrefixedEnvironment handles all identifiers that start with the module name (satellitetypes.ModuleName) followed by a dot. The ShieldEnv receives the identifier name and returns its value.

type SatelliteEnv struct{Keeper}

func (k Keeper) ShieldEnv() ast.Env { return SatelliteEnv{k} }

func (e SatelliteEnv) Get(ctx context.Context, name string) (object.Object, bool) {
if name == "fuel_price" {
price := e.k.FuelPrice(ctx) // access data provided by Keeper
return object.NewInteger(price), true
}

// returning false means that the identifier is not found
// this will abort the evaluation of the Rule with an error
return nil, false
}

A user can then write a Rule – for example, to keep launches on hold until the fuel cost is lower than 100:

satellite.fuel_price < 100

Messages

MsgNewRule

Creates a new Rule with a given human-readable name. The Rule contains an expression (string) that will be parsed into an abstract syntax tree and stored on-chain.

This message is expected to fail in the following cases:

MsgUpdateRule

Updates an existing Rule with a given human-readable name and a new expression.

This message is expected to fail in the following cases:

MsgNewAction

Creates a new Action with a wrapped message, optionally specifying a timeout height.

During this message execution, the x/act module invokes the registered Rule handler for the wrapped message type. The final Rule is stored in the rule field of the Action.

This message is expected to fail in the following cases:

  • The message doesn't have a registered Rule handler.
  • The timeout height is in the past.

MsgApproveAction

Adds an approval to an Action with a given ID.

This message is expected to fail in the following cases:

  • An approval from this address is already present.
  • The Action state isn't pending.

MsgRevokeAction

Revokes a pending Action, aborting its execution.

This message is expected to fail in the following cases:

  • The creator of the message isn't the creator of the Action.
  • The Action state isn't pending (ACTION_STATUS_PENDING).