Open Tx Protocol Brainstorm: (2) Design a Practical Protocol on CKB


To design a protocol for Open Tx, we need to follow some basic principles, such as signature rules, combination rules and security concerns. In this post, I wanna talk about these issues.

Signature rules

In general, every meaningful lock script needs an asymmetric cryptographical signature to unlock. For default SIGHASH_ALL scheme, it first calculates the full TX’s hash, then sign the hash result. For OTX, to confirm the ownership, it’s signature must cover every OTX’s inputs and the fields it concerns.

As demostrated in the above image, the full TX is combined by two open TXs, OTX#0 and OTX#1. If this full TX is a legacy one, and suppose all the inputs share the same lock script, it’s hash should cover all the data of the TX. But for open TX scheme, these two OTXs have their own signatures to cover their constrains about the full TX.

Let’s take a simplest scenario as an example. Alice has 100 USDT stored in input#0, and she sends 20 USDT to Bob with 80 USDT change saved in output #1. This OTX#0 is invalid because 1) it generates an extra output cell, which requires extra CKBytes to create (say 100 CKBytes) 2) it doesn’t provide any mining fee. Someone else combines OTX#1 with OTX#0 to firm a valid TX, it contains 1000 CKBytes as inputs, and 890 CKBytes as output. So OTX#1 provides 100 CKBytes for output#1 and 10 CKBytes for mining fee.

In this demo, the signaure of OTX#0 should cover input#0, output#0 and output#1, and that of OTX#1 should cover input#1 and output#2. The signature here has two purposes, 1) assert the ownership of inputs, and 2) ensure the integrity of a TX/OTX. To achieve these, signatures should covers the consuming inputs and the components of the TX that the signer concerns. There are four different fineness levels for components’ scope definition.

Scope Level 0, TX

It means the signature covers all data of a TX. In other words, this level of signature is the same as SIGHASH_ALL in Bitcoin. It loads all data from the TX, hash it and sign. No one can modify the TX, and thus it cannot support open transaction scheme.

Scope Level 1, Cells

On this level, OTX’s signature covers specific cells. Like the demostration above, the first OTX signs some inputs and some outputs, the second OTX signs others, and they can finally be combined with each other to firm a valid TX.

This mode requires a well definition method for an OTX to identify the cells it concerns. For example, each signature follows a cell index bitmap as parameters (in witnesses fields) to describe the cells it covers.

Scope Level 2, Fields

On this level, an OTX defines which fields in TX’ cells it concerns. For example, it constrains output#0's type script, data, and code hash of lock script field, and leave the args of lock script alone.

Scope Level 3, Logics

This level gives more flexible constrains to an OTX. Such as marking the capacity of output#0 must greater than 1000, or requesting the data field of an output must equals to the hash of its lock script.


There are two composability requirements in OTX protocol, the first one is combination between different OTXs, the second one is combination between OTX protocol scripts and user defined scripts.

Multiple OTXs

When initializing an OTX, one does not known how many other OTXs will be attached to this OTX, and what lock scripts / type scripts of OTX they have. So different OTXs should keep composable under the same protocol.

The key infomation underlying here is that the signature rules that refer to target cells must use some relative index, or they will be mess up in different stack sequence.

OTX feature as an extention

To support OTX features, input cells’ lock script should follow OTX rules. To make things easy and different implementation compatible with each other, it’s better to create a universal OTX lib, and every lock script invoke it.

/* pseudo-code: OTX ready SECP256K1 lock script,
   note that the demo code cannot achieve scope level 3 (logic) constrans.

int main() {
    // load lock script args
    lock_args = load_script_args(CKB_SOURCE_GROUP_INPUT);
    // load OTX lib
    otx_handle = load_lib(OTX_LIB_HASH);
    // load OTX parameters in witness, which defines the signature coverage
    otx_parameters = load_witness(0, CKB_SOURCE_GROUP_INPUT);
    // hash the items defined in otx_parameters, 
    // including all inputs with the same lock_script
    while (message = load_tx_input(CKB_SOURCE_GROUP_INPUT)) {
        hash_update(digest, message);
    // extract items in TX
    TX = load_tx();
    items = otx_handle.invoke(TX, otx_parameters);
    // hash the items
    for (item in items) {
         message = load_tx_item(item);
         hash_update(digest, message);
    // load signature
    signature = load_witness(1, CKB_SOURCE_GROUP_INPUT);
    // recover pk
    pubkey = secp256k1_ecdsa_recover(digest, signature);
    // calculate pkhash
    pk_hash = hash(pubkey);
    // return 0 if pkhash matches lock_args
    return match(pk_hash, lock_args);


An OTX must identify the constrains, and there are 4 levels of constrain details. To make the constrains immutable, we use signatures cover them. We also need OTXs composable and code reusable, so we’d better provide a shared library to help lock scripts aquire OTX features.