Skip to main content

Logic and Actor State

Logic and Actor state are the most important concepts in MOI blockchain. While logic state works as a regular smart-contract storage, known in other blockchains, actor state is the key feature that enables massive parallelism when executing logic calls. Actor state enables logics to store their data on actors, so when actors participate in an interaction with logic, they bring their own data, but the same logic can be used in other interactions with different actors at the same time.

Each actor can thus hold the data of multiple logics in its storage. And as even logics themselves can act as actors, they can contain their own storage (logic state) and also the data of other logics.

In short, Coco supports two kinds of states:

  • logic state — data stored under the logic itself (requires locking the logic to mutate).
  • actor state — data stored under an actor’s context (multiple actors can mutate their own state in parallel).

Access patterns

  • Logic state: Module.Logic.field
  • Actor state for current sender: Module.Sender.field
  • Actor state for a specific actor: Module.Actor(actor_id).field

Use observe to read and mutate to write. When working with large values (maps, arrays, classes), use gather/disperse to control data movement.

TokenLedger.coco
coco TokenLedger

state logic:
name String
symbol String
supply U64
balances Map[Identifier]U64

endpoint deploy Init(name String, symbol String):
mutate name -> TokenLedger.Logic.name
mutate symbol -> TokenLedger.Logic.symbol

endpoint deploy SeedSupply(name String, symbol String, supply U64, seed Identifier):
mutate name -> TokenLedger.Logic.name
mutate symbol -> TokenLedger.Logic.symbol
mutate supply -> TokenLedger.Logic.supply
mutate balances <- TokenLedger.Logic.balances:
balances[seed] = supply

endpoint dynamic LoadAllBalances():
memory local_balances Map[Identifier]U64
local_balances[Identifier(0x1111111111111111111111111111111111111111111111111111111111111111)] = 100
local_balances[Identifier(0x2222222222222222222222222222222222222222222222222222222222222222)] = 200
mutate balances <- TokenLedger.Logic.balances:
// this may be expensive if the map is large
// so we need to explicitly "disperse" the map
disperse balances <- local_balances

Accessing State

Logic state values are accessed using the module name, keyword Logic and the field name, e.g.

ERC20.Logic.name

Actor states are accessed using the module name, actor identifier, and field name. Sender is a commonly used identifier of the actor that has invoked the endpoint, so the sender's state can be accessed as:

Flipper.Sender.value

Instead of Sender, logic can also access its state on any Actor that participates in the invocation of the endpoint (interaction on MOI blockchain), including any participating logic. An actor's identifier is usually passed as an argument to the endpoint, but it can be just a fixed identifier if it's know before the logic is written and deployed to the blockchain.

coco ActorState

state actor:
data String

endpoint GetData(actor_id Identifier) -> (data String):
observe data <- ActorState.Actor(actor_id).data

endpoint GetSpecialData() -> (data String):
observe data <- ActorState.Actor(Identifier(0x1111111111111111111111111111111111111111111111111111111111111111)).data

Deploy and Enlist

endpoint deploy SeedSupply() // initialize state when deploying logic

endpoint enlist Seed() // enlisting means initializing logic's state on actor

In modules having a logic state, it is necessary to have at least one deploy endpoint to initialize this state (there can be multiple if there are different ways to deploy a logic). Modules with actor states need at least one enlist endpoint, but if a module has both logic and actor states, only deploy is mandatory.

dynamic keyword

Endpoint qualifier dynamic denotes the endpoint mutates the state. Without it, compiler throws an error as it would be a runtime error trying to mutate a read-only state.

Observing and Mutating State

coco Mod

state logic:
name String

state actor:
counter U64

endpoint deploy Init():
mutate "MyLogic" -> Mod.Logic.name

endpoint dynamic Tick():
mutate c <- Mod.Sender.counter:
c += 1

endpoint Counter() -> (name String, counter U64):
observe counter <- Mod.Sender.counter
observe name <- Mod.Logic.name

Observing State

Observe statement is used to capture values from the state and sets it to a value.

observe map <- ModMutate.Logic.num:

If an observe statement ends with a : sign then it can have a body in which the value that has been observed can be used as a local variable.

Multiple targets and values can be listed in a single statement, e.g.

observe name, symbol, value <- Mod.Logic.name, Mod.Logic.symbol, Mod.Sender.value

Mutating State

Mutate statement is used to set a module value to the state.

mutate n -> ModMutate.State.check

In this case the the value in ‘n’ is set to the check field of the persistent state.

mutate num <- ModMutate.State.num:

If a mutate statement ends with a ‘:’ then the statement can have a context block. In the case of a mutate statement with a context the final value of num after execution of the block is set back into the persistent state at the end of the mutation context. num can’t be a variable name that’s already declared and it’s local to the mutate block.

As with observe, mutate accepts multiple targets & values in a statement. Nesting is also allowed, so mutating values inside mutate block is possible.

Observing and Mutating Complex Values using disperse and gather

Coco takes care to avoid expensive operations like transferring a large amount of data from the storage (state). E.g., if we want to only alter a single value in the map, there’s no need to load a complete map and store it back.

If we actually want to transfer a complete array, map or a class from the storage into memory, we need to explicitly prepend the assignment or append statement with gather keyword, and if we’re storing the data from memory into storage, we need to prepend disperse.

coco Complex

state logic:
Operators Map[U64]Operator

class Operator:
field Identifier String
field Guardians []String

endpoint deploy Init(moid String):
mutate operators <- Complex.Logic.Operators:
disperse operators[0] <- Operator{
Identifier: moid,
Guardians: make([]String, 0),
}

endpoint Op0() -> (op Operator):
memory o Operator
observe operators <- Complex.Logic.Operators:
gather o <- operators[0]
op = o