UTXO-Based Actor Model Design and Nested Invocation Intermediate Representation


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:

  1. Information Processing (Computation).
  2. Storage (State).
  3. 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:

  1. Configs Cell:

    • Stores configuration dependencies for the entire application.
    • Type Script conforms to type_id rules to ensure global uniqueness.
    • Lock determines who can modify the configs, which can also be set to always-success, controlling modifications within the configs cell’s Type Script.
  2. 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 to always-success, controlling modifications within the Type Script.
  3. 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, typically input_type_proxy, which determines unlockability based on whether the corresponding Type Script exists in the inputs.
  4. 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 to always-success to integrate permission management into the type.

    • 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 into script_index. For example, from_script_hash becomes from_script_index, and receiver_script_hash becomes receiver_script_index. Through the combination of index and script_location, the corresponding script_hash can be retrieved when needed via load_lock_hash and lock_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 each CrossCellInvocation, and instead, a target_indexes field is added to IntentProcessTrace 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 and data from the transaction inputs (Inputs) and caches the current state in memory.
  • 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.

2. Scan Witness and Inputs

  • Scan Witness:

    • Find Relevant IntentProcessTraces:

      • Locate all IntentProcessTraces targeting its own receiver_script_hash.
      • Theintent_index in IntentProcessTrace represents the correspondence between that trace and which intent it corresponds to. Since an Actor may participate as a target in other intents, the intent_index may not be continuous.
    • Identify IntentProcessTraces Targeting Itself:

      • In all IntentProcessTraces, find CrossCellInvocations where target_script_hash is itself and receiver_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.
    • 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.
  • 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.

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.
  • 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 corresponding IntentProcessTrace 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.
  • 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

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’s signature.
      • 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 and outputs match those in the invocation.
    • Error Handling: If they do not match, report an error and terminate processing.
  • 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.
5 Likes

基于UTXO的Actor模型设计与嵌套调用中间表达


前面,我论述了两种在UTXO模型上实现可变状态访问的方案,即基于 Intent-Script ( General Composable Intent Script(Covenant) and Asynchronous Invocation Paradigm - English / CKB Development & Technical Discussion - Nervos Talk),或者基于Open Transaction ( Dynamic Fine-Grained Cobuild OTX(PSBT) Design - English / CKB Development & Technical Discussion - Nervos Talk),用BTC的术语来说,可以称为基于Covenant,或者基于PSBT的方案。

使用这两个方案,可以让用户的意图串行被dApp所处理,但是在之前的方案中,我只论述了用户如何将意图发送给dApp,以及dApp之间如何利用intent cell异步通信,而没有论述dApp之间应该如何同步通信,乃至于串联多个dApp的同步调用。

Actor模型是一种计算的数学理论,基于 Actors 的概念。 Actor是计算的基本单元,它体现为三件事:

  1. 信息处理(计算)。
  2. 存储(状态)
  3. 通信

本方案将借助 Actor模型的思路来论述,每个 dApp 都接收来自用户的意图,以及来自其他 dApp 的请求,同时也可以给其他 dApp 来发送请求,但是由于 UTXO 并非链上计算系统,而是链下确定性的,亦即链上的 inputs 和 outputs 只有最初和最终的状态,而没有中间状态。

本方案将试图解决这个问题,通过在Witness 内固化整个Actor通信流程。

首先对本方案的应用进行定义,每个应用大体上会包含以下Cell:

  1. Configs Cell:
    • data内放置整个应用依赖的配置
    • typescript 符合 type_id 规则,以保证全局唯一。
    • Lock 决定了谁能修改configs,也可以将lock设置为always-success,在 configs cell 的 typescript 里控制修改。
  2. Actor Cell:
    • data内会放置应用的全局状态数据
    • type script 符合type_id 规则,以保证全局唯一,同时 type script 通过扫描所有的输入 (intent-script or OTX),并处理意图,同时 type script 还应该扫描所有来自其他 Actor Cell 的请求,以及扫描自己对其他 Actor Cell 请求的返回。type script 还需要检查自己控制的 Asset Cell 和 Data Cell 经过处理完所有的意图和请求后的输出,与交易实际的输出一致。
    • lock决定了谁能解锁Actor Cell,也可以将lock设置为always-success,在 typescript 里控制修改。
  3. Asset Cell:如 UDT Cell,DOB Cell,Capacity Cell
    1. Data 一般由资产 type 所决定。
    2. type script:定义资产类型
    3. lock:proxy lock,一般为 input_type_proxy,即通过判断 inputs 中是否存在对应的 type script 来判定是否解锁,而对应 Actor Cell 的 type script 则负责检查 Asset Cell 的前后变化是否符合规则。
  4. Data Cell:用于存储某些长度不定的数据, 如类似可追加数组,key-value字典等。
    1. Data 一般存储需要的data。
    2. type script:定义数据类型
    3. lock:可以用lock控制数据修改,也可以将lock设置为alway-success,把权限管理整合进唐type。
  5. 示例:
    1. 基于链表的 Data Cell,通过其type判定其确实是Actor Cell所存储的数据,直接从 cellDeps,或者inputs中读取数据使用。
    2. 基于累加器的Data Cell,通过其 type 判定其确实是Actor Cell所存储的数据,将需要访问的数据以及数据的证明放置在 Witness内,供 Actor Cell 读取使用。由于基于累加器的链上Data长度固定,所以通常可以把这一功能内置在 Actor Cell 的type script内更方便。

应用举例:

  • Rollup:
    • Rollup将在Configs Cell内放置配置
    • 使用Asset Cell存放跨进Rollup中的资产
    • 同时使用累加器处理 Rollup 的数据
    • 用户通过 intent-cell 或者 OTX 来向 Rollup Deposit 资产
    • Rollup type script 将负责检验链下状态转换的正确性,通过欺诈证明或者有效性证明。
    • 使用某种共识算法的检验lock来决定 Rollup Cell的解锁。
  • 侧链:
    • 在Configs Cell内放置配置
    • 使用Asset Cell存放跨进侧链中的资产
    • 在 Cell 中存储侧链的状态根
    • 用户通过 intent-cell 或者 OTX 来进行资产跨链
    • 侧链 type script将负责检验跨链进出的正确性
    • 使用某种共识算法的检验lock来决定侧链 Cell的解锁。

链上验证流程

前面论述了在 Cell 模型上构造基于Actor构造的基本结构,但仍未论述如何实现嵌套 Actor 的互相调用。

数据结构

table CrossCellInvocation {
    from_script_hash: Byte32,      // 调用方的脚本哈希
    from_location: byte,           // 调用方位置,例如 input_lock、input_type 或 output_type
    target_script_hash: Byte32,    // 被调用方的脚本哈希
    target_location: byte,         // 目标位置,例如 input_lock、input_type 或 output_type
    target_index: u32,             // 目标在处理序列中的索引
    success: byte,                 // 调用是否成功,0 表示成功,非 0 表示失败,可使用错误码
    signature: Byte8,              // 方法签名
    inputs: Bytes,                 // 输入参数
    outputs: Bytes,                // 输出结果
}

vector CrossCellInvocationVec <CrossCellInvocation>;

table IntentProcessTrace {
    intent_index: u32,                     // Intent 的索引
    success: byte,                         // Intent 处理是否成功
    invocations: CrossCellInvocationVec,   // 调用序列
}

vector IntentProcessTraceVec <IntentProcessTrace>;

table IntentProcessTraces {
    receiver_script_hash: Byte32,          // 接收者的脚本哈希
    traces: IntentProcessTraceVec,         // Intent 处理跟踪列表
}

修改:

  • 为了节省空间,可以将数据结构里所有的 script_hash 变成 script_index,如 from_script_hash 变成 script_index,receiver_script_hash 变成 receiver_script_index,通过index和script_location,需要使用的时候可以通过 load_lock_hash 和 lock_type_hash 获取到对应的script_hash使用,这将大大减少空间占用。、

  • 关于 target_index 的修改,为了简化,将数据结构做如下的修改:

    struct TargetIndex {
        target_script_hash: Byte32,    // 被调用方的脚本哈希
        target_location: byte,         // 目标位置,例如 input_lock、input_type 或 output_type
        target_index: u32,             // 目标在处理序列中的索引
    }
    
    vector TargetIndexVec <TargetIndex>;
    
    table IntentProcessTrace {
        intent_index: u32,                     // Intent 的索引
        target_indexs: TargetIndexVec          // 所有涉及到的target的索引
        success: byte,                         // Intent 处理是否成功
        invocations: CrossCellInvocationVec,   // 调用序列
    }
    

    即从每个 CrossCellInvocation 中删除 target_index 这一项,而在 IntentProcessTrace 里增加一项 target_indexs 记录所有 target 的 index。

本文后面的论述并未按照上述改动修改,因为上述改动旨在减少空间占用,以及让结构显得更清晰,但不影响具体的设计逻辑。

Actor Cell Type Script 的状态转换流程

1. 初始化内存状态

  • 读取资产和数据:
    • Actor 首先从交易的输入(Inputs)中,读取属于自身的资产 Cell 和数据,将当前状态缓存到内存中。
  • 维护处理计数器:
    • 维护一个计数器 processed_intent_count,初始值为 0。
    • 每当 Actor 处理完一个 intent 后,该计数器加一,无论是直接处理自己的 intent,还是在处理过程中被其他 Actor 调用的 intent。
    • 该计数器用于对链上处理进行排序,确保处理的顺序性和一致性。

2. 扫描 Witness 和 Inputs

  • 扫描 Witness:

    • 找到相关的 IntentProcessTraces:
      • 找到所有以自身 receiver_script_hash 为目标的 IntentProcessTraces
      • IntentProcessTraceintent_index 代表了该 trace 与第几个 intent 的处理对应。由于 Actor 可能作为被调方参与其他 intent 的处理,intent_index 可能不连续。
    • 找到针对自身的 IntentProcessTraces:
      • 在所有的 IntentProcessTrace 中,找到 invocations 列表中 target_script_hash 为自身且 receiver_script_hash 非自身的 CrossCellInvocation
      • 这些代表了其他 Actor 对自身的调用,其中target_index代表这个intent在自己处理序列中位置。
    • 一致性检查:
      • 在同一个 IntentProcessTrace 中,对于同一个 Actor 作为 targettarget_index 应该完全一致。
      • target_index 代表该 intent 在 Actor 处理序列中的 intent 索引。
      • 示例: 如果 target_index 为 2,表示这是 Actor 处理的第三个 intent。
  • 扫描 Inputs:

    • 处理 Intent Cell 和 Open Transaction Inputs:
      • 扫描所有对自身 Actor Cell 进行调用的 Intent Cell 或 Open Transaction(OTX)。
      • 处理过程中涉及对内存状态的改变,例如更新资产、修改数据等。

3. 处理模式

每个 Actor 有两种进入 intent 处理 trace 的模式:

  • 直接作为 Intent 的接收者(receiver):

    • Actor 直接处理针对自身的 intent。
    • 处理步骤:
      • 根据 IntentProcessTrace 中的 invocations,按照顺序执行。
      • 检查每个 invocation 是否与自身的预期操作一致,如果不一致则报错。
  • 被其他 Actor 间接调用:

    • Actor 被其他 Actor 间接调用。
    • 在调用其他 Actor 时,需要检查对应的 Actor Cell 是否在交易的输入(UTXO)中存在。
    • 处理顺序:
      • 对于被调用的 Actor,其在对应 IntentProcessTrace 中的 target_index,代表了该 intent 在自身处理 intent 队列中的位置。
      • 例如,target_index 为 0,表示该 intent 是 Actor 应该首先处理的 intent,无论是直接还是间接调用。
    • 按照顺序处理 invocations:
      • 对于每个 IntentProcessTrace,按照 invocations 的顺序处理整个 intent 的调用序列。
      • 找到自身作为调用方(from_script_hash 为自身)和被调方(target_script_hash 为自身)的 CrossCellInvocation
      • 所有的调用都需要与自身的处理流程匹配,确保一致性。

4. Actor 处理一个 Trace

(1)作为调用方

  • 查找匹配的 Invocation:

    • 在需要请求其他 Actor 时,找到对应的 IntentProcessTraceinvocations,按照顺序执行。
    • 找到满足以下条件的 CrossCellInvocation
      • from_script_hash 为自身的脚本哈希。
      • target_script_hash 为被调用 Actor 的脚本哈希。
      • signature 与调用的方法签名一致。
      • inputs 与调用的输入参数一致。
    • 然后读取 invocation 中的 outputs,继续处理。
    • 错误处理: 如果找不到匹配的 invocation,则报错并终止处理。
  • 支持循环调用:

    • 如果在找到匹配自身作为调用方的 invocation 之前,遇到其他 Actor 调用自己的 invocation,说明存在循环调用。
    • 可以先执行自身作为被调用方的 invocation,然后继续处理。
    • 错误处理: 如果扫描到一个 invocation,其 from_script_hash 为自身但无法匹配,则直接报错。

(2)作为被调用方

  • 检查 Invocation:

    • 检查 signature 对应的方法,使用 invocation.inputs 作为输入,执行方法逻辑。
    • 验证执行结果的 successoutputs 是否与 invocation 中的 successoutputs 一致。
    • 错误处理: 如果不一致,则报错并终止处理。
  • 处理循环调用:

    • 如果在处理过程中,没有其他 Actor 调用自身,但出现了自身调用其他 Actor 的 invocation,说明是循环调用。
    • 将这些自身作为调用方的 invocation 压入栈中,直到读取到第一个自身被调用的 invocation
    • 然后开始处理栈中的 invocation,直到处理完整个缓存栈。
    • 错误处理: 在处理过程中,如果遇到任何不匹配的情况,立即报错。

5. 处理 Intent 和更新计数器

  • 完成 intent 处理:

    • 在成功处理完一个 intent 之后,Actor 将自身的 processed_intent_count 加一。
  • 继续处理下一个 intent:

    • 根据 IntentProcessTrace 中的 intent 顺序,以及 Actor 记录的自身 target_index,决定下一个要处理的 intent。
    • 注意: 确保按照正确的顺序处理所有待处理的 intent,避免遗漏或重复处理。

6. 处理失败的 Intent

  • 检测失败状态:

    • 如果 IntentProcessTrace 中的 success 不等于 0,表示对应的 intent 整体处理失败。
  • 状态回滚:

    • 在处理完对应的 IntentProcessTrace 后,所有涉及的 Actor 应该将内存状态恢复至处理该 intent 之前的状态。
    • 实现方式: 可以采用状态快照,在处理前保存状态,处理失败时进行回滚。

7. 序列化和反序列化

  • 依据方法定义:

    • inputsoutputs 的序列化和反序列化方式,由 Actor 中 signature 对应的方法决定。
    • 可以使用 Molecule 作为统一的 ABI 格式,确保不同 Actor 之间的数据可以正确解析和处理。

8. 最终状态检查

  • 完成所有处理:

    • 当 Actor Cell Type Script 处理完所有 intent 和 message 后,内存状态达到最终状态。
  • 验证输出:

    • 检查交易 outputs 中的输出是否符合内存中的最终状态,完成处理。
    • 错误处理: 如果输出不一致,则报错并终止交易。

Intent 和 Invocation 的处理差别

需要注意的是,Actor Cell Type Script 针对 IntentInvocation 的处理方式不同。

  • 处理 Intent:

    • 当处理 Intent 时,Actor 不仅需要对输入的资产和数据进行处理,修改内存状态,还需要检查对应 intent 是否得到了满意的输出。
    • 这包括验证输出的资产和容量(Capacity)是否符合规则,以及是否满足用户的期望。
    • 理由: 直接面向用户的 Actor 必须检查其 intent 是否满足,因为没有其他脚本会执行这个检查,并且该输出直接反映在交易的输出 Cells 中。
  • 处理 Invocation:

    • 当处理 Invocation 时,Actor 只需要根据 invocation 中的 inputsoutputs 修改内存状态即可。
    • 无需检查具体结果,因为这些处理涉及的是中间状态,并未反映在交易输出中。
    • 示例: 当 Actor A 与 Actor B 通信,使用 30 USD 换取 40 CKB,Actor A 记录自身 USD 减少 30,CKB 增加 40;Actor B 反之。
    • 理由: 最终,如果在交易输出中,Actor A 和 Actor B 控制的资产和数据符合处理结果,说明中间的消息通信是正确的,资产是隐式在 Actor 间传递。

附加说明

  • 计数器的作用:

    • processed_intent_count 用于对链上处理进行排序,确保处理的顺序性和一致性。
    • intent_index 以及 target_index 都是根据 processed_intent_count 确定其处理位置的,即 intent_indextarget_index 一起组成了一个连续的序列,分别是该 Actor 作为直接被调方或者间接被调方参与的 intent。
  • 循环调用处理:

    • 通过允许在找到匹配的 invocation 之前,执行其他 Actor 对自身的调用,以支持循环调用。
    • 通过将自身作为调用方的 invocation 压栈缓存,以支持循环调用。
  • 错误处理:

    • 如果在扫描 invocations 时,发现 from_script_hash 为自身但与执行流程无法匹配的情况,立即报错,防止不一致的发生。
  • 状态一致性:

    • 在处理失败的 intent 时,必须将内存状态回滚,以保持整个系统状态的一致性。

IntentProcessTraces 的位置

  • 特殊的 WitnessLayout:

    • 设定一个特殊的 Witness 布局,用于放置 IntentProcessTraces,以方便 Script 的扫描处理。
    • 这些 traces 最好与 Intent 的处理顺序对应,以简化链上扫描。
    • 建议: 明确 WitnessLayout 的格式和解析方法,确保所有参与者遵循相同的标准。

链下 Generator 流程

之前讨论了链上如何检查嵌套调用中间表达的正确性,那么链下如何拼接完成 UTXO 交易呢?

  • 共识约束:

    • 所有需要同步组合的 Actor-Based dApp 应该在同一个共识系统的约束下,才有可能做到同步组合。
  • Intent 的排序和处理:

    • 所有调用的开端都是 Intent Cell 或 OTX,只需要针对这些进行排序,并按顺序处理所有的 intent。
  • 示例流程:

    • 假设对于每一个链上的 Actor Cell,链下都存在一个对应的 Generator。
  • 对于 Actor A,存在一个 intent 队列;对于 Actor B,也存在一个 intent 队列。

    • 处理过程:

      • 当 Actor A 的 Generator 处理到某个 intent 需要调用 Actor B 时,其向 Actor B 的 Generator 发送一条消息。
    • 当 Actor B 接收到消息时,处理并返回对应的输出,期间可能涉及到对其他 Actor 的访问,将所有的消息记录成trace。

      • 注意,如果出现循环调用,则循环调用的 invocation 要出现在 trace 的前面。
      • 此时,Actor B 不能直接开始处理下一个 intent,而是处于锁定状态,等待来自 Actor A 的新消息,直到 Actor A 当前 intent 处理结束。
      • 即一旦一个 intent 直接或间接地访问了某个 Actor,该 Actor 必须等待该 intent 完全执行完毕,才能释放并处理下一个 intent。
      • 当一个 intent 处理完毕后,可以得到 intent 的 outputs 和处理过程中所有中间的消息调用,并构造 IntentProcessTrace
    • 交易拼接:

      • 在处理完一个 intent 序列后,将 intent cell、OTX,以及所有的 IntentProcessTraces,根据其调用顺序,拼接成一个完整的交易。
    • 这样,链上的 Actor Cell Type Script 在扫描处理所有的 intent 和 OTX,以及 IntentProcessTraces 时,其顺序与链下拼接的顺序一致。


示例

以上面的图示为例,存在三个Actor,并且对于每一个intent,假设调用顺序如下:

  • Intent-1 调用的入口都是 Actor-1,此时 Actor-1 的 processed_intent_count 为 0,则此时新创建一个intent_index为0的trace。
  • Actor-1 首先调用 Actor-3,Actor-3 调用 Actor-1,Actor-1得到来自Actor-3的返回。
  • Actor-1 再去调用 Actor-2,Actor-2 会调用 Actor-3,Actor-3再调用Actor-1,得到返回后Actor-2再调用Actor-1,最终 Actor-1 得到返回。
  • Actor-1 再去调用Actor-3,直接得到返回。
  • Actor-1 得到对intent的输出,检查intent输出是否正确。
  • 对于 Intent-2,执行一样的流程。
  • 执行完对 intent-2 的处理后,Actor-1,Actor-2,Actor-3各自检查自己在交易中的Asset输出和Data输出是否与处理结果一致。

其时序图如下:

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

得到的traces如下:

IntentProcessTraces {
	receiver: Actor-1
	traces: {
		{
			intent_index: 0,
			invocations: {
				{
					from: Acrot-3,
					target: Actor-1,
					target_index: 0,  // 与 intent_index 一致
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-3,
					target_index: 0,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-3,
					target: Actor-1,
					target_index: 0,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-2,
					target: Actor-3,
					target_index: 0,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-2,
					target: Actor-1,
					target_index: 0,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-2,
					target_index: 0,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-3,
					target_index: 0,
					signature
					inputs
					outputs
				}
			}
		},
		{
			intent_index: 1,
			invocations: {
				{
					from: Acrot-3,
					target: Actor-1,
					target_index: 1,  // 与 intent_index 一致
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-3,
					target_index: 1,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-3,
					target: Actor-1,
					target_index: 1,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-2,
					target: Actor-3,
					target_index: 1,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-2,
					target: Actor-1,
					target_index: 1,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-2,
					target_index: 1,
					signature
					inputs
					outputs
				},
				{
					from: Acrot-1,
					target: Actor-3,
					target_index: 1,
					signature
					inputs
					outputs
				}
			}
		}
	}
}

可以看出,Invocations中invocation的顺序与时序图中 return 的顺序一模一样,通过这种排列,即可实现循环调用。

结语

通过完成关于 UTXO 嵌套调用中间表达的链上检查与链下拼接的方案设计,对于 Cell 模型,结合之前设计的基于 intent-script 或者基于 OTX 的方案,实现同步嵌套调用并不是一件很困难的事,尽管还需要一定的工作量。

基于这个方案:

  • 第一步: 各家应用可以对自己定序的 Actor 实现嵌套调用。

  • 进一步: 可以实现通用的定序服务,在 CKB 上实现一个 Overlapped Layer,或者基于 Cell 模型的 Rollup/Sidechain。

    • 该定序服务可以使用 PoS 或联合挖矿作为准入机制。
    • 在这个 Overlapped Layer 上,使用共识排序解决对共享可变状态的争用问题。
  • 设计对应的 DSL:

    • 使用 DSL 编写的合约将被编译成两部分:

      • 部署在 CKB 上的链上脚本。
      • 部署在 Overlapped Layer 的链下 Generator。
    • 用户可以使用 intent-cell 对部署在 Overlapped Layer 的合约发起调用。

    • Overlapped Layer 在收到 intent 后,进行排序,然后使用 Generator 拼接一个完整的批次(batch),发送到 CKB 链上验证。