Skip to Content
🎉 Listen to the Choreo's launch event on Sep 23rd
ChoreoCustom Effects and Environment Extensions

Using Custom Effects and Environment Extensions

Custom effects and environment extensions allow you to extend Choreo beyond basic message passing. This guide shows how Tendermint uses these features for evidence collection and accountability.

Environment Extensions

Extensions provide global state that all processes can access and modify. In Tendermint, this tracks evidence for Byzantine fault detection:

type Extensions = Bookkeeping type Bookkeeping = { evidence_propose: Set[ProposeMsg], evidence_prevote: Set[PreVoteMsg], evidence_precommit: Set[PreVoteMsg], }

Initializing Extensions

Set up the initial extension state during system initialization:

pure val initial_bookkeeping = { evidence_propose: Set(), evidence_prevote: Set(), evidence_precommit: Set(), } action init = choreo::init({ system: NODES.mapBy(n => initialize_process(n)), messages: NODES.mapBy(n => Set(initial_message).union(byzantine_messages_1)), events: NODES.mapBy(n => Set()), extensions: initial_bookkeeping // Global state accessible by all processes })

CustomEffect

CustomEffect allows processes to modify the global environment extensions during transitions:

type CustomEffects = CollectEvidence(Message)

Emitting Custom Effects

Processes can produce custom effects as part of their transitions:

pure def broadcast_prevote_for_proposal(ctx: LocalContext, p: ProposeMsg): Transition = { val s = ctx.state pure val effects = if (valid(p.proposal) and (s.locked_round == -1 or s.locked_value == Some(p.proposal))) { Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: Some(id(p.proposal)) }))) } else { Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None }))) } { post_state: { ...s, stage: PreVoteStage }, effects: effects.setAdd(choreo::CustomEffect(CollectEvidence(Propose(p)))) } }

Processing Custom Effects

When using custom effects, you should define how they modify the global environment. For this example, we simply want to store the information in the extension we created for bookkeeping:

def apply_custom_effect(env: GlobalContext, effect: CustomEffects): GlobalContext = { match effect { | CollectEvidence(msg) => { pure val updated_bookkeeping = match msg { | Propose(p) => { ...env.extensions, evidence_propose: env.extensions.evidence_propose.setAdd(p) } | PreVote(p) => { ...env.extensions, evidence_prevote: env.extensions.evidence_prevote.setAdd(p) } | PreCommit(p) => { ...env.extensions, evidence_precommit: env.extensions.evidence_precommit.setAdd(p) } } { ...env, extensions: updated_bookkeeping } } } }

Connecting to Step Function

Wire everything together in the step function:

action step = choreo::step( main_listener, apply_custom_effect // Pass the custom effect processor )

Using Extensions for Properties

Accessing the extensions in the properties is as simple as accessing the record field with s.extensions (aliased to choreo::s.extensions). See the following example of accountability checking:

/// Equivocation by a node n def equivocation_by(n: Node): bool = { or { n.equivocation_in(s.extensions.evidence_propose), n.equivocation_in(s.extensions.evidence_prevote), n.equivocation_in(s.extensions.evidence_precommit), } } pure def equivocation_in(n: Node, evidence: Set[{ src: Node, round: Round | other }]): bool = { tuples(evidence, evidence).exists(((e1, e2)) => { e1 != e2 and e1.src == n and e2.src == n and e1.round == e2.round }) } val accountability = or { agreement, FAULTY.powerset().exists(f => and { f.size() >= F + 1, f.forall(n => equivocation_by(n) or amnesia_by(n)), }) }

Final Thoughts

These extensions generalize Choreo to most, if not all, use-cases. Another example of its usage is in the Monad BFT spec, where we add an extension for a database mock which processes can use to lookup past proposals. Extensions might not be necessary in many scenarios, but they are there for when you do.

Last updated on