Skip to main content

Logic and Actor State

Coco supports two kinds of persistent state:

TypeStorage LocationParallelism
Logic stateUnder the logic itselfRequires lock (sequential)
Actor stateUnder each actor's contextParallel execution

Logic state (left) vs Actor state (right). Each actor carries slots for multiple logics.

Performance

Use actor state for frequently accessed data to enable parallel execution. Reserve logic state for initialization and shared configuration.

Declaring State

coco MyLogic

state logic:
name String
total_supply U256

state actor:
balance U256
registered Bool

Accessing State

Use three-component identifiers:

PatternDescription
Module.Logic.fieldLogic state
Module.Sender.fieldCurrent sender's actor state
Module.Actor(id).fieldSpecific actor's state
// Logic state
observe supply <- MyLogic.Logic.total_supply

// Sender's actor state
observe bal <- MyLogic.Sender.balance

// Specific actor's state
observe other_bal <- MyLogic.Actor(user_id).balance

Observing State (Read)

Use observe to read from state:

// Simple read
observe name <- MyLogic.Logic.name

// Multiple values
observe name, supply <- MyLogic.Logic.name, MyLogic.Logic.total_supply

// With block (value available inside)
observe counter <- MyLogic.Sender.counter:
if counter > 100:
throw "Limit exceeded"

Mutating State (Write)

Use mutate to write to state:

// Direct assignment
mutate "TokenName" -> MyLogic.Logic.name
mutate 1000 -> MyLogic.Logic.total_supply

// With block (modify and save)
mutate counter <- MyLogic.Sender.counter:
counter += 1

// Nested mutations
mutate supply <- MyLogic.Logic.supply:
supply -= amount
mutate bal <- MyLogic.Sender.balance:
bal += amount

Complex Values

For efficiency, Coco uses atomic storage — complex objects (maps, arrays, classes) are scattered across storage slots.

gather and disperse

To transfer entire complex objects, use explicit keywords:

KeywordDirectionUsage
gatherStorage → MemoryReading complete objects
disperseMemory → StorageWriting complete objects
// Reading a complete map entry
observe operators <- MyLogic.Logic.Operators:
gather op <- operators[0] // Load entire Operator object

// Writing a complete object
mutate operators <- MyLogic.Logic.Operators:
disperse operators[0] <- Operator{
name: "Admin",
permissions: make([]String, 0)
}
warning

gather and disperse can be expensive for large objects. Prefer accessing individual fields when possible.

storage variables

As gather and disperse are expensive, there's a way to avoid transferring complete objects if we only want to observe or mutate a single field. Instead of gathering into a memory variable and dispersing it after change, we can use storage variable that serves only as a pointer to an object inside atomic storage. Using it, we can perform much cheaper operations, like in this example:

coco StorageVarExample

class LargeData:
field category String
field data []Bytes // a huge dataset
field exists Bool

state actor:
store []LargeData

endpoint static FindCategoryExpensive(category String) -> (data LargeData):
memory mem_data LargeData
observe st <- StorageVarExample.Sender.store:
memory last_index = len(st)
for idx in range(last_index):
memory slot LargeData
gather slot <- st[idx]
if slot.category == category:
return (data: slot)
// if it's not found, a zero-value of LargeData is returned

endpoint static FindCategoryEfficient(category String) -> (data LargeData):
memory mem_data LargeData
observe st <- StorageVarExample.Sender.store:
memory last_index = len(st)
for idx in range(last_index):
storage slot_ptr LargeData
slot_ptr = st[idx]
if slot_ptr.category == category:
memory slot LargeData
gather slot <- st[idx]
return (data: slot)
// if it's not found, a zero-value of LargeData is returned

The example above shows how we can use storage variable - a pointer to a storage object - that allows us to read a simple boolean value without gathering a complete object from storage, and only gather the data when found. In the worst-case scenario, where we're searching for a category that doesn't exist, the FindCategoryExpensive transfers a complete store of all LargaData objects, just to realize the data doesn't exist. FindCategoryEfficient just checks the string field category of each element and doesn't unnecessarily transfer large data fields.

Cleaning up with sweep

When removing the last element from a collection in state, use sweep to remove the empty collection from storage:

mutate operators <- MyLogic.Logic.operators:
sweep remove(operators, key) // Remove map entry
sweep(operators) // Remove empty map from storage

mutate arr <- MyLogic.Logic.arr:
memory removed = sweep popend(arr) // Remove last and capture value

Complete Example

coco Token

state logic:
name String
supply U256

state actor:
balance U256

endpoint deploy Init(name String, supply U256):
mutate name -> Token.Logic.name
mutate supply -> Token.Logic.supply

endpoint enlist Register():
mutate 0 -> Token.Sender.balance

endpoint dynamic Transfer(to Identifier, amount U256):
mutate bal <- Token.Sender.balance:
if bal < amount:
throw "Insufficient balance"
bal -= amount
mutate to_bal <- Token.Actor(to).balance:
to_bal += amount

endpoint static GetBalance() -> (balance U256):
observe balance <- Token.Sender.balance

Parallelism Note

Mutating logic state requires a lock, preventing parallel execution:

// Anti-pattern: blocks parallelism
endpoint dynamic Claim():
mutate supply <- Logic.supply: // Lock required
supply -= 1

For high-throughput endpoints, use actor state or native assets.