A CKB Programming Paradigm Proposal: Simulate Account Model on Cell Model

The cell model brings a new programming paradigm on blockchain. Users compose a transaction including input cells, output cells and deps, then the chain validates the transaction. The state transition is offered in the transaction, instead of being driven by execution of transactions, like in ethereum.

Here we propose a solution to simulate the account-model-like programming paradigm on cell model. Why do we need this?

  • Sometimes users do not care about the whole state transition process, they only care about their actions. For example, in a vote contract, we may only care about that our vote is processed successfully, not the state before and after our action. It is a big overhead to get the input cell, process the action to get the output cell, and compose the transaction.
  • The data associated to a contract is distributed in many cells. It will be hard to do statistics, e.g., count the votes of every candidate.

The details of the solution:

  • A contract is a cell
    • Lock script of contract cell is an always true script, anyone can refer the cell as input
    • Type script interprets the data of the cell. It contains functions below:
      • init
        • Used when “deploying the contract”. When we create the contract cell from arbitrary cell, the data should be under restraint of the init function.
        • Use the hash of first input cell as “contract id”, and record it in data.
      • execute actions
        • The input cells of a transaction should be one contract cell and many action cells. The output cell is a new contract cell, whose typescript remains the same, and data changes by the actions.
        • The execute_actions function shows how to handle action with the data.
      • verify actions. The default behavior of verify is execute the actions with original data, and compare the result data with output cell data. Sometimes there may be more efficient method, e.g., for a sort action, verify function can validate the result instead of executing sort again.
    • Data. Arbitrary data, interpreted by type script, similar to the storage in ethereum contract.
  • Action cell
    • action
      • An action is used to describe the user behavior, e.g., vote, deposit, withdraw, and so on.
      • An action contains some common fields, like to_contract_id, action_name, from_address, nonce, signature. Other fields are contract specific. For example, a vote action may contains who the user votes for.
      • An action already contains all information which can be used to compose a transaction action. It can be packaged into an action cell by users themselves, or a 3rd party relayer, like what people does in 0x protocol.
    • One action cell contains one or more actions. For example, the simplest implementation is use a json list in data field, every element inside the json list is an action object which contains the needed fields.
    • Action cell is container of actions, similar to a transaction in 0x protocol. It contains the action data, and may offer the cell capacity for the contract data increase and package fee.
    • The state transition of contract cell is only up to the actions inside the action cells. It doesn’t matter who offers the action cell.
    • Action cell may have an optional fee field in the data. Sometimes the action cell data may be big but the contract cell data changes little. For example, in a vote contract, the action cell may contains thousands of votes, but the contract cell data size remains the same after counting the votes. In this case, user who offers the action cell may lose much capacity. We can use an optional fee field to make sure that when compose the transaction, there must be an output cell whose lock is the same with the action cell with capacity which is more than the amount specified in the action cell. In this way, who offers the action cell may get some capacity back.
  • Compose transaction with contract cell and action cells
    • Anyone can collect action cells, find the associated contract cell, execute the actions using the execute function in contract cell type script, get new contract cell data, then compose a new transaction and broadcast it.
    • Miners may have strong wills to do the transaction composition work.
      • They can get extra capacity from the action cell.
      • They need to execute all the actions to verify the transaction. They can skip this if they compose the transaction themselves.

We use a vote contract as an example to show the solution.

There is a fixed candidate list in the contract, any address can vote, one address have only one vote.

The vote contract type script pseudocode:

'''
data format:
{
    "contract_id": "",
    "votes": {
        "candidate1": 123,
        "candidate2": 456
    },
    "voted": ["addr1", "addr2"]
}
'''

def init(contract_id):
    data = {
        "contract_id": "",
        "votes": {
            "candidate1": 0,
            "candidate2": 0
        },
        "voted": []
    }
    return data


def verify_action(action):
    # check to_contract_id match data['contract_id']
    # check action signature

def vote(candidate, from_addr, data):
    if from_addr not in data["voted"]:
        data["votes"] += 1
        data["voted"].push(from_addr)

def execute(action, data):
    verify_action(action)
    if action["action"] == "vote":
        vote(action["candidate"], action["from_addr"])
    else:
        throw Exception("action not support")

def main():
    # if there is no input cell and only one output cell whose type script is this, it can be treated as a deploy transaction
    if IS_INIT():
        contract_id = INPUT_CELLS[0].hash
        data = init(contract_id)
        assert json.dumps(data) == OUTPUT_CELLS[0].data     # json is just an example here, we can ignore the uncertain serialization problem here
    # if there is only one input cell and only one output cell whose type script is this, it can be treated as a call transaction
    if IS_CALL():
        in_data = json.loads(INPUT_CELLS[0].data)
        for cell in INPUT_CELLS[1:]:
            for action in json.loads(cell.data):
                execute(action, in_data)
        out_data = OUTPUT_CELLS[0].data
        assert json.dumps(in_data) == out_data

Compose a transaction to deploy the contract. We can use an arbitrary cell as input, an output cell with the above script as type script and initial data.

When we want to vote, we can compose an action:

{
    "from_addr": "",
    "to_contract_id": "",
    "action": "vote",
    "candidate": "candidate1",
    "signature": "xxx"
}

How the signature is created depends on the verify_action in the contract. We can propose a protocol here.

User can compose the action cell themselves, or send it to a 3rd party relayer to delegate them to compose the cell.

  • Type script of the action cell makes sure that the cell can only be consumed with the associated contract cell.
  • The content of data is an action list, like [{ "from_addr": "addr1", "to_contract_id": "", "action": "vote", "candidate": "candidate1", "signature": "xxx" }, { "from_addr": "addr2", "to_contract_id": "", "action": "vote", "candidate": "candidate1", "signature": "xxx" }].

When the miners creates the block, they can collect the unspent action cells into groups, execute them with the associated contract, get the new data and compose the transaction, append the transaction to the block.

Cross contract call pattern

  • Send message by action cell among contracts, to simulate a call in ehtereum. For example:
    • In ethereum, contract A calls B, B calls C
    • In this pattern, we send a action cell to A, A execute it, produce a new action cell whose to_contract_id is B, then someone use it to call B, then get a new action cell to call C
  • Problems
    • How to ensure that the action cell produced by A can not be faked, and can only be used as B’s input cell.
    • Every ‘call’ needs to wait a block.
    • There is no way to rollback here.

FAQ

Why use action cell to hold actions, not witness?

  • Actions are self-contained messages, they can be put anywhere as long as users can get the data and compose transaction.
  • Cells can be tranfer by existing infrastructure. If we use witness, we may need to add features for p2p layer to support this.
  • Cells may only live a very short time. In ideal situation, they will be consumed just one block after they are produced. It will take much space for the chain node maintainers.
  • Cells have capacity while witnesses have not.
    • This can be both advantage and disadvantage.
    • disadvantage
      • Users who want to send transaction have to put a lot of capacity into the action cell, though we can design a mechanism for users to get the capacity back, it is still a high barrier for users.
    • advantage
      • The action cell can offer the capacity for the data size increase after executing actions.
      • The capacity offers incencive. If we use witness to hold actions data, the nodes may have no motivation to spread the data, compose the transaction.

A MVP for this design

https://github.com/huwenchao/ckb-standalone-debugger/blob/duktape/tests/test_udt.rs

The contract is written in JavaSrcipt with the help of on-chain duktape interpreter.

Quick Start:

git clone https://github.com/huwenchao/ckb-standalone-debugger.git
cd ckb-standalone-debugger
git checkout duktape

RUST_BACKTRACE=1 cargo test -- test_udt_init --nocapture

RUST_BACKTRACE=1 cargo test -- test_udt_transfer --nocapture

descriptions:

  • There are 2 functions in test_udt.rs, test_udt_init simulates a transaction which deploys the contract, and test_udt_transfer simulates a transaction which call the transfer function in the contract.
  • udt.js is the contract cell type script, action.js is the action cell type script.

todo:

  • Implement a relayer, listen the chain events, collect action cells, compose transactions and broadcast them to the chain. In this way, anyone can be a relayer.
  • Implement functions to specify max fee and get capacity back in the action.js.
  • JSON can be replaced by other serialization method which is more compact and efficient.
  • Validating signature is not implemented yet.
3 Likes

This is cool, I enjoyed reading through it. One question: why make it part of the CKB-debugger? So that it can be demoed without going through the regular process with a node?

Yes. This is a simple POC demo, I just want to prove the possibility of this programming paradigm. I can test easily with a simple command, instead of starting a node, sending transactions, mined and checking the result.