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.
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