Skip to main content

Rule Syntax

Rules express temporal constraints using modal mu-calculus.

Submitting Rules

When adding a rule to a contract, you must include a model that witnesses satisfiability:

modal c commit \
--method rule \
--rule 'rule my_rule { formula { always(<+signed_by(/members/alice.id)> true) } }' \
--model 'model witness { initial s; s -> s [] }' \
--sign key.pem

The model proves the rule can be satisfied. Without a satisfying model, the rule commit is rejected. This prevents adding unsatisfiable rules that would deadlock the contract.

Basic Structure

rule <name> {
starting_at <commit_ref>
formula {
<modal_formula>
}
}

// Or as default export
export default rule {
starting_at $PARENT
formula {
<modal_formula>
}
}

Anchoring (starting_at)

starting_at $PARENT           // Parent of this commit
starting_at $ROOT // Genesis commit
starting_at abc123... // Specific commit hash
OperatorMeaning
[ACTION] φAfter ALL ACTION transitions, φ holds
<ACTION> φAfter SOME ACTION transition, φ holds
[-ACTION] φIf ACTION is refused/impossible, φ holds
[<+ACTION>] φCommitted: CAN do ACTION and CANNOT refuse

Commitment Versus Enabledness

Use the operator that matches the claim you want the contract to make:

<+PAY> true

PAY is enabled from the current witness state.

[<+PAY>] true

PAY is committed: it is enabled and its refusal edge is unavailable.

[+PAY] +signed_by(/parties/alice.id)

Every matching PAY transition must carry Alice's signature predicate.

Avoid [+PAY] true as a guard. A box formula with inner true is satisfied even when there is no matching PAY transition, so it does not prove PAY happened or that PAY is committed. Run modality model lint <file> before signing rules; it reports this as modality/vacuous-box-guard.

Temporal Operators (Syntactic Sugar)

always(φ)           // φ holds forever (invariant)
// = gfp(X, φ & []X)

eventually(φ) // φ holds now or sometime later
// = lfp(X, φ | <>X)

until(p, q) // p holds until q becomes true
// = lfp(X, q | (p & <>X))

Fixed Points

// Greatest fixed point (νX) - invariants, safety
gfp(X, property & []X)

// Least fixed point (μX) - reachability, liveness
lfp(X, target | <>X)

// Unicode alternatives
νX. (property & []X)
μX. (target | <>X)

Boolean Connectives

φ & ψ           // Conjunction (and)
φ | ψ // Disjunction (or)
!φ // Negation (not)
true // Always true
false // Always false

Prefer explicit boolean form for conditional rules:

!+modifies(/members) | +all_signed(/members)

The parser still accepts implication syntax in some contexts, but docs and onboarding examples avoid it so temporal steps and proof implication are not conflated. modality model lint <file> reports signed-rule uses as modality/implication-sugar; rewrite them to explicit Boolean form before signing.