Previously, I discussed two approaches for implementing variable state access on the UTXO model: one based on Intent-Script (General Composable Intent Script (Covenant) and Asynchronous Invocation Paradigm - English / CKB Development & Technical Discussion - Nervos Talk) and another based on Open Transaction (Dynamic Fine-Grained Cobuild OTX (PSBT) Design - English / CKB Development & Technical Discussion - Nervos Talk). In BTC terminology, these can be referred to as Covenant-based or PSBT-based approaches.
Using these two approaches allows a user’s intent to be processed serially by dApps. However, in previous discussions, I only covered how users send intents to dApps and how dApps communicate asynchronously through intent cells. I did not address how dApps should communicate synchronously or chain multiple synchronous calls between dApps.
The Actor model is a mathematical theory of computation based on the concept of Actors. An Actor is a fundamental unit of computation that encompasses three elements:
- Information Processing (Computation).
- Storage (State).
- Communication.
This proposal will leverage the Actor model concept to describe how each dApp receives intents from users and requests from other dApps, and also how it can send requests to other dApps. However, since UTXO is not an on-chain computation system but rather a deterministic off-chain system, the inputs and outputs on-chain only represent the initial and final states, without any intermediate states.
This proposal aims to solve this problem by embedding the entire Actor communication process within the Witness.
First, let’s define the application of this proposal. Each application will generally include the following cells:
-
Configs Cell:
- Stores configuration dependencies for the entire application.
Type Script
conforms totype_id
rules to ensure global uniqueness.Lock
determines who can modify the configs, which can also be set toalways-success
, controlling modifications within the configs cell’sType Script
.
-
Actor Cell:
- Contains global state data for the application.
Type Script
conforms to type_id rules to ensure global uniqueness. It scans all inputs (intent-script or OTX), processes intents, and scans all requests from other Actor Cells, along with responses to its requests. TypeScript must also check that the outputs of its controlled Asset Cell and Data Cell, after processing all intents and requests, align with the actual outputs of the transaction.Lock
determines who can unlock the Actor Cell, which can also be set toalways-success
, controlling modifications within theType Script
.
-
Asset Cell: Such as UDT Cell, DOB Cell, Capacity Cell.
Data
is generally determined by asset type.Type Script
defines asset types.Lock
is a proxy lock, typicallyinput_type_proxy
, which determines unlockability based on whether the correspondingType Script
exists in the inputs.
-
Data Cell: Used to store variable-length data, such as appendable arrays or key-value dictionaries.
-
Data
typically stores required data. -
Type Script
defines data types. -
Lock
can control data modification or can be set toalways-success
to integrate permission management into thetype
. -
Example:
Linked-list based Data Cell
, confirmed by its type, can read data directly from cellDeps or inputs.Accumulator-based Data Cell
, confirmed by its type, stores required data and proofs in the Witness for Actor Cell access. Due to the fixed length of on-chain accumulator-based data, this functionality can usually be embedded within the Actor Cell’s TypeScript for convenience.
-
Application Examples:
-
Rollup:
- Rollup will place configurations in the Configs Cell.
- Uses Asset Cell to store assets for Rollup.
- Processes Rollup data using an accumulator.
- Users deposit assets into Rollup via intent-cell or OTX.
- Rollup TypeScript is responsible for verifying the correctness of off-chain state transitions through fraud proofs or validity proofs.
- Uses a consensus algorithm’s verification lock to determine Rollup Cell unlockability.
-
Sidechain:
- Places configurations in the Configs Cell.
- Uses Asset Cell to store assets for sidechain.
- Stores the state root of the sidechain in the Cell.
- Users transfer assets across chains via intent-cell or OTX.
- Sidechain TypeScript is responsible for verifying the correctness of cross-chain transactions.
- Uses a consensus algorithm’s verification lock to determine sidechain Cell unlockability.
On-Chain Verification Process
The previous sections discussed constructing the basic structure based on the Actor model within the Cell model, but the implementation of nested Actor inter-calls remains unaddressed.
Data Structures
table CrossCellInvocation {
from_script_hash: Byte32, // Caller’s script hash
from_location: byte, // Caller’s location, e.g., input_lock, input_type, or output_type
target_script_hash: Byte32, // Called party's script hash
target_location: byte, // Target location, e.g., input_lock, input_type, or output_type
target_index: u32, // Index in the target script processing sequence
success: byte, // Indicates if the call was successful; 0 for success, non-0 for failure (error code)
signature: Byte8, // Method signature
inputs: Bytes, // Input parameters
outputs: Bytes, // Output results
}
vector CrossCellInvocationVec <CrossCellInvocation>;
table IntentProcessTrace {
intent_index: u32, // Index of the Intent
success: byte, // Indicates if Intent processing was successful
invocations: CrossCellInvocationVec, // Sequence of calls
}
vector IntentProcessTraceVec <IntentProcessTrace>;
table IntentProcessTraces {
receiver_script_hash: Byte32, // Receiver's script hash
traces: IntentProcessTraceVec, // Intent processing trace list
}
Modifications:
-
To save space, all
script_hash
fields in the data structure can be converted intoscript_index
. For example,from_script_hash
becomesfrom_script_index
, andreceiver_script_hash
becomesreceiver_script_index
. Through the combination of index and script_location, the correspondingscript_hash
can be retrieved when needed viaload_lock_hash
andlock_type_hash
. This will greatly reduce space consumption. -
Regarding the modification of
target_index
, to simplify the structure, the following changes are made to the data structure:struct TargetIndex { target_script_hash: Byte32, // The script hash of the callee target_location: byte, // The target location, such as input_lock, input_type, or output_type target_index: u32, // The index of the target in the processing sequence } vector TargetIndexVec <TargetIndex>; table IntentProcessTrace { intent_index: u32, // The index of the Intent target_indexes: TargetIndexVec // Indexes of all involved targets success: byte, // Whether the Intent was processed successfully invocations: CrossCellInvocationVec, // Invocation sequence }
This means that the
target_index
field is removed from eachCrossCellInvocation
, and instead, atarget_indexes
field is added toIntentProcessTrace
to record the indexes of all targets.
The following discussions in this document have not been modified according to the above changes, as these changes aim to reduce space consumption and make the structure clearer, but they do not affect the core design logic.
State Transition Process of Actor Cell Type Script
1. Initialize Memory State
-
Read Assets and Data:
- The Actor first reads its
asset Cells
anddata
from thetransaction inputs (Inputs)
and caches the current state in memory.
- The Actor first reads its
-
Maintain Processing Counter:
- Maintain a counter
processed_intent_count
, initialized to 0. - Increment this counter by one each time the Actor processes an intent, whether it processes its own intent directly or an intent called by other Actors.
- This counter is used to sort on-chain processing, ensuring order and consistency.
- Maintain a counter
2. Scan Witness and Inputs
-
Scan Witness:
-
Find Relevant
IntentProcessTraces
:- Locate all
IntentProcessTraces
targeting its ownreceiver_script_hash
. - The
intent_index
inIntentProcessTrace
represents the correspondence between that trace and which intent it corresponds to. Since an Actor may participate as a target in other intents, theintent_index
may not be continuous.
- Locate all
-
Identify
IntentProcessTraces
Targeting Itself:- In all
IntentProcessTraces
, findCrossCellInvocations
wheretarget_script_hash
is itself andreceiver_script_hash
is not. - These represent calls from other Actors to itself, with
target_index
indicating the position of this intent in its processing sequence.
- In all
-
Consistency Check:
- Within the same
IntentProcessTrace
, the target_index should be entirely consistent for the same Actor as the target. target_index
indicates the index of that intent in the Actor’s processing sequence.- Example: If
target_index
is 2, this indicates this is the third intent being processed by the Actor.
- Within the same
-
-
Scan Inputs:
- Process Intent Cell and Open Transaction Inputs:
- Scan all Intent Cells or Open Transactions (OTX) calling its Actor Cell.
- This processing involves changes to the memory state, such as updating assets or modifying data.
- Process Intent Cell and Open Transaction Inputs:
3. Processing Modes
Each Actor has two modes for entering the intent processing trace:
-
Directly as Intent Receiver:
- The Actor directly processes intents targeting itself.
- Processing Steps:
- Execute in the order defined by invocations in the
IntentProcessTrace
- Verify that each invocation aligns with its expected operations; otherwise, report an error.
- Execute in the order defined by invocations in the
-
Indirectly Called by Other Actors:
- The Actor is indirectly called by other Actors.
- When calling other Actors, it needs to check if the corresponding Actor Cell exists in the transaction inputs (UTXO).
- Processing Order:
- For the called Actor, the
target_index
in the correspondingIntentProcessTrace
indicates the position of this intent in its intent queue. - For instance, if
target_index
is 0, this intent is the first one the Actor should process, regardless of whether the call is direct or indirect.
- For the called Actor, the
-
Process invocations in order:
- For each
IntentProcessTrace
, process the entire intent call sequence in the order of invocations. - Identify
CrossCellInvocations
where it is the caller (from_script_hash
is itself) and the called party (target_script_hash
is itself). - All calls must align with
- For each
its processing flow to ensure consistency.
4. Actor Processes a Trace
(1) As Caller
-
Find Matching Invocation:
- When requesting other Actors, find the corresponding invocations in the IntentProcessTrace and execute them in order.
- Find CrossCellInvocations that meet the following criteria:
from_script_hash
is its script hash.target_script_hash
is the called Actor’s script hash.signature
matches the called method’ssignature
.inputs
match the calling parameters.
- Then read
outputs
from the invocation for further processing. - Error Handling: If no matching invocation is found, report an error and terminate processing.
-
Support for Recursive Calls:
- If it encounters other Actors calling its invocation before finding a match, a recursive call is indicated.
- It can execute its invocation as a called party first, then continue processing.
- Error Handling: If a matching invocation is encountered but cannot be matched, report an error.
(2) As Called Party
-
Check Invocation:
- Check the method corresponding to the signature, using
invocation.inputs as input
, and execute the method logic. - Verify that the execution result’s
success
andoutputs
match those in the invocation. - Error Handling: If they do not match, report an error and terminate processing.
- Check the method corresponding to the signature, using
-
Handle Recursive Calls:
- If no other Actors are calling during processing but it encounters its own invocation to other Actors, this indicates recursion.
- Push these self-calling invocations onto a stack until the first invocation where it is the called party is read.
- Then begin processing the stack until the entire cached stack is processed.
- Error Handling: If any mismatched situations arise during processing, immediately report an error.
5. Process Intent and Update Counter
-
Complete Intent Processing:
- After successfully processing an intent, the Actor increments its processed_intent_count by one.
-
Continue to Next Intent:
- Based on the intent order in the IntentProcessTrace and the Actor’s recorded target_index, determine the next intent to process.
- Note: Ensure that all pending intents are processed in the correct order to avoid omissions or duplicate processing.
6. Handle Failed Intents
-
Detect Failure State:
- If success in the IntentProcessTrace is not equal to 0, it indicates that the corresponding intent processing has failed.
-
State Rollback:
- After processing the corresponding IntentProcessTrace, all involved Actors should restore the memory state to its state prior to processing that intent.
- Implementation: A state snapshot can be employed, saving state before processing and rolling back upon failure.
7. Serialization and Deserialization
- Method Definition Based:
- The serialization and deserialization methods for inputs and outputs are determined by the methods corresponding to the Actor’s signature.
- Molecule can be used as a unified ABI format to ensure that data between different Actors can be correctly parsed and processed.
8. Final State Check
-
Complete All Processing:
- Once the Actor Cell Type Script has processed all intents and messages, the memory state achieves the final state.
-
Verify Outputs:
- Check if the outputs in the transaction align with the final state in memory, completing the processing.
- Error Handling: If outputs are inconsistent, report an error and terminate the transaction.
Difference in Handling Intent and Invocation
It’s important to note that the Actor Cell Type Script handles Intent and Invocation differently.
-
Handling Intent:
- When processing Intent, the Actor needs to modify memory state and verify whether the corresponding intent received satisfactory outputs.
- This includes validating that the outputs’ assets and capacities meet the criteria and satisfy user expectations.
- Reason: Actors directly interacting with users must ensure their intents meet expectations, as no other scripts will perform this check, and outputs directly reflect in transaction output Cells.
-
Handling Invocation:
- When processing Invocation, the Actor merely modifies memory state according to inputs and outputs from the invocation.
- There’s no need to check specific results, as these processes involve intermediate states that are not reflected in transaction outputs.
- Example: When Actor A communicates with Actor B, exchanging 30 USD for 40 CKB, Actor A records a decrease of 30 USD and an increase of 40 CKB; Actor B records the opposite.
- Reason: Ultimately, if the assets and data controlled by Actors A and B conform to processing results in transaction outputs, it indicates correct intermediate message communication and that assets are implicitly transferred between Actors.
Additional Notes
-
Role of the Counter:
- processed_intent_count is used for sorting on-chain processing, ensuring order and consistency.
- intent_index and target_index are based on processed_intent_count to determine their processing positions, forming a continuous sequence representing the intents the Actor has directly or indirectly participated in.
-
Handling Recursive Calls:
- Allow execution of other Actors’ calls before finding matching invocations to support recursive calls.
- Cache self-calling invocations in a stack to support recursive calls.
-
Error Handling:
- If during invocation scanning, a from_script_hash is found that does not match the execution flow, immediately report an error to prevent inconsistencies.
-
State Consistency:
- Upon handling failed intents, the memory state must be rolled back to maintain the overall consistency of the system.
Location of IntentProcessTraces
- Special Witness Layout:
- Establish a special Witness layout for placing IntentProcessTraces to facilitate script scanning.
- These traces should ideally correspond to the order of intent processing for easier on-chain scanning.
- Recommendation: Clearly define the format and parsing methods for the WitnessLayout, ensuring all participants adhere to the same standards.
Off-chain Generator Process
We previously discussed how to verify the correctness of nested calls on-chain. Now, how do we piece together a complete UTXO transaction off-chain?
-
Consensus Constraints:
- All Actor-based dApps that need to be synchronously combined should operate under the constraints of the same consensus system to achieve synchronized composition.
-
Sorting and Processing of Intents:
- The starting point for all calls is the Intent Cell or OTX. We only need to sort these and process all intents in order.
-
Example Process:
- For each Actor Cell on-chain, assume there is a corresponding Generator off-chain.
- For Actor A, there is an intent queue; for Actor B, there is also an intent queue.
-
Processing Steps:
- When the Generator for Actor A processes an intent that requires calling Actor B, it sends a message to Actor B’s Generator.
- Upon receiving the message, Actor B processes it and returns the corresponding output, which may involve accessing other Actors, recording all messages as traces.
- Note that if there is a recursive call, the invocation for that recursive call should appear at the beginning of the trace.
- At this point, Actor B cannot directly start processing the next intent but is locked, waiting for new messages from Actor A until the current intent for Actor A is fully processed.
- Once an intent directly or indirectly accesses an Actor, that Actor must wait for the intent to complete before it can release and process the next intent.
- After processing an intent, outputs and all intermediate message calls can be obtained to construct an IntentProcessTrace.
-
Transaction Assembly:
- After processing a sequence of intents, the intent cell, OTX, and all IntentProcessTraces are assembled into a complete transaction according to their call order.
- This way, when the on-chain Actor Cell Type Script scans all intents, OTXs, and IntentProcessTraces, the order will be consistent with the off-chain assembly order.
Example
Using the above diagram as an example, there are three Actors, and for each intent, assume the call order is as follows:
- The entry point for Intent-1 is Actor-1. At this moment, Actor-1’s processed_intent_count is 0, creating a new trace with intent_index 0.
- Actor-1 first calls Actor-3, which calls back Actor-1, and Actor-1 receives a response from Actor-3.
- Actor-1 then calls Actor-2, which calls Actor-3, and Actor-3 calls Actor-1. After receiving a response, Actor-2 calls Actor-1 again, ultimately getting a return.
- Actor-1 then calls Actor-3 again, receiving a response directly.
- Actor-1 checks the outputs for the intent to ensure correctness.
- For Intent-2, the same process is executed.
- After processing Intent-2, Actors 1, 2, and 3 each check their Asset outputs and Data outputs in the transaction for consistency with the processing results.
The sequence diagram is as follows:
sequenceDiagram
participant A1 as Actor-1
participant A2 as Actor-2
participant A3 as Actor-3
Note over A1,A3: Intent-1 Processing
A1->>A1: Create trace (intent_index = 0)
A1->>A3: Call(1->3)
A3->>A1: Call(3->1)
A1-->>A3: Return
A3-->>A1: Return
A1->>A2: Call(1->2)
A2->>A3: Call(2->3)
A3->>A1: Call(3->1)
A1-->>A3: Return
A3-->>A2: Return
A2->>A1: Call(2->1)
A1-->>A2: Return
A2-->>A1: Return
A1->>A3: Call(1->3)
A3-->>A1: Return
A1->>A1: Check intent output
Note over A1,A3: Intent-2 Processing
A1->>A1: Create trace (intent_index = 1)
A1->>A3: Call(1->3)
A3->>A1: Call(3->1)
A1-->>A3: Return
A3-->>A1: Return
A1->>A2: Call(1->2)
A2->>A3: Call(2->3)
A3->>A1: Call(3->1)
A1-->>A3: Return
A3-->>A2: Return
A2->>A1: Call(2->1)
A1-->>A2: Return
A2-->>A1: Return
A1->>A3: Call(1->3)
A3-->>A1: Return
A1->>A1: Check intent output
Note over A1,A3: Final Checks
A1->>A1: Check Asset and Data output
A2->>A2: Check Asset and Data output
A3->>A3: Check Asset and Data output
The resulting traces are as follows:
IntentProcessTraces {
receiver: Actor-1
traces: {
{
intent_index: 0,
invocations: {
{
from: Actor-3,
target: Actor-1,
target_index: 0, // Matches intent_index
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-3,
target_index: 0,
signature
inputs
outputs
},
{
from: Actor-3,
target: Actor-1,
target_index: 0,
signature
inputs
outputs
},
{
from: Actor-2,
target: Actor-3,
target_index: 0,
signature
inputs
outputs
},
{
from: Actor-2,
target: Actor-1,
target_index: 0,
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-2,
target_index: 0,
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-3,
target_index: 0,
signature
inputs
outputs
}
}
},
{
intent_index: 1,
invocations: {
{
from: Actor-3,
target: Actor-1,
target_index: 1, // Matches intent_index
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-3,
target_index: 1,
signature
inputs
outputs
},
{
from: Actor-3,
target: Actor-1,
target_index: 1,
signature
inputs
outputs
},
{
from: Actor-2,
target: Actor-3,
target_index: 1,
signature
inputs
outputs
},
{
from: Actor-2,
target: Actor-1,
target_index: 1,
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-2,
target_index: 1,
signature
inputs
outputs
},
{
from: Actor-1,
target: Actor-3,
target_index: 1,
signature
inputs
outputs
}
}
}
}
}
As can be seen, the order of invocations in the Invocations section matches the order of returns in the sequence diagram, enabling recursive calls through this arrangement.
Conclusion
By completing the design for on-chain checks and off-chain assembly of UTXO nested call intermediate expressions, combining the Cell model with previously designed intent-script or OTX-based solutions allows for synchronous nested calls, even though it requires some effort.
Based on this plan:
-
Step 1: Each application can implement nested calls for their ordered Actors.
-
Further Steps: A universal ordering service can be created to implement an Overlapped Layer on CKB or a Rollup/Sidechain based on the Cell model.
- This ordering service can use PoS or pooled mining as an admission mechanism.
- On this Overlapped Layer, consensus ordering can resolve contention over shared mutable states.
-
Design Corresponding DSL:
-
Contracts written in DSL will be compiled into two parts:
- On-chain scripts deployed on CKB.
- Off-chain Generators deployed on the Overlapped Layer.
-
Users can initiate calls to contracts deployed
-
on the Overlapped Layer using intent-cells.
- The Overlapped Layer will sort intents upon receipt and then use the Generator to assemble a complete batch, sending it to the CKB chain for verification.