Introducing `cobuild-otx-contracts`: A Reference Implementation for CoBuild OTX Contracts

This repository( xcshuan/cobuild-otx-contracts) is a working reference implementation and testbed for building
CKB contracts around CoBuild and Open Transaction (OTX) workflows.

The design is based on the ideas described in:

It also incorporates the later discussions around fine-grained signing control and dynamic OTX/PSBT-style construction:

For comprehensive spec, read: cobuild-core-community-redraft-design.md

The goal of this repository is not to define the final protocol standard. The goal is to make the protocol ideas concrete: parse real CoBuild witnesses, derive signing requirements, verify OTX signatures on-chain, and test realistic application contracts that depend on those mechanics.

Why This Project Exists

CKB applications are usually built off-chain and verified on-chain. A wallet, dapp, matcher, market maker, relayer, or another participant may each contribute part of a transaction before it is signed and submitted. CoBuild provides a common witness and message model for this collaborative construction process.

OTX extends this idea to partially constructed transactions. A participant can sign an intent over a local transaction fragment, while allowing later parties to append inputs, outputs, cell deps, header deps, or other OTX fragments. The final transaction can then be assembled from multiple independently authorized pieces.

This repository explores the contract-side part of that model:

  • how a lock script verifies signatures for transaction-level CoBuild messages;
  • how a lock script verifies signatures for OTX base and append segments;
  • how a type script finds the CoBuild action relevant to itself;
  • how fine-grained masks allow specific fields to remain flexible;
  • how multiple OTXs can be composed into one final transaction;
  • how real application contracts can use these tools without duplicating
    witness scanning, OTX layout parsing, or signing hash logic.

Core Design

The repository is organized around three concepts: CoBuild messages, OTX layout, and signing requirements.

CoBuild Messages and Actions

In CoBuild, a transaction can carry a Message. The message contains one or more Actions. An action identifies:

  • the script role it targets: input lock, input type, or output type;
  • the target script hash;
  • application-specific action data.

This allows a contract to ask a higher-level question: “Is there an action for me, and does its data describe a valid state transition?” Instead of each contract manually scanning raw witness bytes, cobuild-core exposes helpers for finding actions by role and script hash.

This is used by the test contracts in this repository. For example, the limit order contracts parse fill-order actions, and the NFT minter contracts parse create/mint actions. The application contracts focus on their own state rules, while the shared CoBuild machinery handles witness layout and action lookup.

OTX Base and Append Segments

An OTX is split into one base scope and zero or more ordered append segments:

  • the base scope, signed by the original OTX creator;
  • append segments, each with its own counts, flags, and seals. A participant
    signs an append segment only when that segment contains an input owned by the participant’s lock.

The base scope can include:

  • base inputs;
  • base outputs;
  • base cell deps;
  • base header deps;
  • a CoBuild message;
  • append permissions.

Append segments can include:

  • append inputs;
  • append outputs;
  • append cell deps;
  • append header deps.

The final transaction can contain multiple OTXs. OtxStart anchors where the OTX section begins, and each Otx witness provides base counts plus ordered append segment counts for the entities it owns. cobuild-core scans the witnesses and derives both per-segment ranges and aggregate OTX ranges.

This lets a script answer questions such as:

  • Is my input in the base scope of this OTX?
  • Is my input in a specific append segment?
  • Is the output referenced by this action inside this OTX?
  • Is an action merely targeting my script, or is it also in my OTX scope?

Concrete OTX Examples

The easiest way to read an OTX is to start from a normal CKB transaction. A CKB transaction consumes old cells through inputs, creates new cells through outputs and outputs_data, loads code or read-only data through cell_deps, and carries signatures or CoBuild witnesses in witnesses.

OTX does not replace this transaction model. It describes which parts of the final transaction belong to a participant’s signed fragment, and which parts may be appended later.

Fine-Grained Base Scope Control

The base scope supports fine-grained masks. Instead of signing every field of every base entity, the OTX creator can choose which fields are committed.

The current implementation supports masks for:

  • base input fields: since, previous_output;
  • base output fields: capacity, lock, type, data;
  • base cell deps;
  • base header deps.

For base inputs, resolved input cell output and input data are still covered by the base signing hash. This keeps input substitution constrained: omitting the exact previous outpoint does not mean the signer stops committing to the consumed state.

For base outputs, the mask can be used to support flexible construction. For example, an NFT mint OTX can bind the buyer lock and/or capacity while leaving the NFT type script and data to be filled later, or it can bind additional fields when the application needs stronger guarantees.

The mask bytes themselves are included in the signing hash. This prevents two different coverage policies from sharing the same signature.

Uncovered fields are not removed from the base signing preimage. Instead, the implementation writes canonical default values for those slots. This keeps the preimage shape stable while preserving the intended flexibility: uncovered fields can still be changed by later builders, but the signer commits to the exact mask policy and field positions.

Example 1: NFT-for-UDT Swap from Two OTXs

The nft_for_udt_swap_otxs_case fixture demonstrates the smallest useful composition pattern: two independently signed OTXs are combined into one final transaction.

Participant A owns an NFT and wants 1000 units of a UDT. Participant B owns 1000 units of that UDT and wants the NFT. Each participant signs their own OTX base fragment. Neither participant needs to sign the other participant’s input.

The final transaction can be read as:

Transaction
  cell_deps:
    [0] cobuild-otx-lock code
    [1] test-udt type code
    [2] test-nft type code

  inputs:
    [0] A's NFT cell                -> swap OTX #0 base input
    [1] B's UDT cell                -> swap OTX #1 base input

  outputs:
    [0] UDT payment to A            -> swap OTX #0 base output
    [1] NFT delivery to B           -> swap OTX #1 base output

  outputs_data:
    [0] UDT amount: 1000
    [1] NFT metadata

  witnesses:
    [0] OtxStart
    [1] swap OTX #0 witness with A's base seal
    [2] swap OTX #1 witness with B's base seal

The two OTXs describe complementary sides of the swap:

swap OTX #0, signed by A
  base_inputs:
    [0] inputs[0]  A's NFT cell

  base_outputs:
    [0] outputs[0] UDT payment to A

  base input mask:
    since:           covered
    previous_output: covered

  base output mask:
    capacity: covered
    lock:     covered
    type:     covered
    data:     covered

swap OTX #1, signed by B
  base_inputs:
    [0] inputs[1]  B's UDT cell

  base_outputs:
    [0] outputs[1] NFT delivery to B

  base input mask:
    since:           covered
    previous_output: covered

  base output mask:
    capacity: covered
    lock:     covered
    type:     covered
    data:     covered

This example uses full base masks. A signs the exact NFT input and the exact UDT payment output they expect to receive. B signs the exact UDT input and the exact NFT output they expect to receive. When the two OTXs are placed into the same transaction, the lock script verifies each base seal against the final transaction ranges derived from OtxStart and the two Otx witnesses.

If a builder changes A’s UDT payment amount, A’s base signature fails. If the builder changes the delivered NFT output, B’s base signature fails. The useful property is composability: each participant signs only their local fragment, but the final transaction settles both fragments atomically.

Example 2: Using Base Masks

Consider an NFT minting workflow involving three buyers, one minter, and a transaction builder. Each buyer contributes one mint OTX. The minter cell is not part of any buyer’s OTX append input. Instead, it is handled by a transaction-level SighashAllOnly signature, because the minter/admin state cell is outside the OTXs and must authorize the final aggregate transition.

The final transaction might look like this:

Transaction
  cell_deps:
    [0] cobuild-otx-lock code
    [1] nft-minter-type code
    [2] minted-nft-type code

  inputs:
    [0] buyer A payment cell        -> mint OTX #0 base input
    [1] buyer B payment cell        -> mint OTX #1 base input
    [2] buyer C payment cell        -> mint OTX #2 base input
    [3] minter cell                 -> tx-level input, SighashAllOnly signed
    [4] fee cell                    -> fee payer input

  outputs:
    [0] minted NFT for buyer A      -> mint OTX #0 base output
    [1] minted NFT for buyer B      -> mint OTX #1 base output
    [2] minted NFT for buyer C      -> mint OTX #2 base output
    [3] updated minter cell         -> tx-level output, SighashAllOnly signed
    [4] fee payer change cell       -> tx-level or remainder output

  outputs_data:
    [0] NFT A metadata
    [1] NFT B metadata
    [2] NFT C metadata
    [3] updated minter state
    [4] empty fee-change data

  witnesses:
    [0] OtxStart
    [1] mint OTX #0 witness with buyer A base seal
    [2] mint OTX #1 witness with buyer B base seal
    [3] mint OTX #2 witness with buyer C base seal
    [4] SighashAllOnly witness for the minter cell and tx-level flow

The fee cell and change cells play different roles. A fee cell is an input: it adds capacity to the transaction so the builder can pay the implicit CKB fee. There is no special “fee output”; the fee is the difference between total input capacity and total output capacity. A change cell is an output that returns unused capacity to a buyer, minter, or fee payer.

Each buyer can sign a base OTX before the full transaction is known:

mint OTX #0 base
  base_inputs:
    [0] inputs[0]  buyer A payment cell

  base_outputs:
    [0] outputs[0] intended NFT A cell

  base output mask for outputs[0]:
    capacity: covered
    lock:     covered
    type:     uncovered
    data:     uncovered

mint OTX #1 base
  base_inputs:
    [0] inputs[1]  buyer B payment cell

  base_outputs:
    [0] outputs[1] intended NFT B cell

  base output mask for outputs[1]:
    capacity: covered
    lock:     covered
    type:     uncovered
    data:     uncovered

mint OTX #2 base
  base_inputs:
    [0] inputs[2]  buyer C payment cell

  base_outputs:
    [0] outputs[2] intended NFT C cell

  base output mask for outputs[2]:
    capacity: covered
    lock:     covered
    type:     uncovered
    data:     uncovered

This means each buyer commits to spending their selected payment cell, and commits that their NFT output will have the expected capacity and buyer lock. The buyers do not commit to the final NFT type scripts or NFT metadata. Those fields can be filled in later by the minter or transaction builder.

The base signing hash still has a stable structure. For covered fields, the real final transaction value is written into the preimage. For uncovered fields, the implementation writes canonical default slots. For each of the three mint OTXs, the NFT output fields are handled like this:

outputs[i].capacity  -> real capacity bytes
outputs[i].lock      -> real buyer lock script bytes
outputs[i].type      -> default ScriptOpt bytes
outputs_data[i]      -> default empty data bytes

where i is 0, 1, or 2 for mint OTX #0, #1, or #2.

So each buyer’s signature says: “I authorize this base OTX shape, this payment input state, this NFT output capacity, this NFT output owner, and this exact mask policy. I do not authorize a specific NFT type/data value.”

During verification, the OTX lock recomputes the base hash from the final transaction. If a builder changes outputs[1].lock to another owner, or lowers the covered capacity for buyer B’s NFT, buyer B’s base signature fails. If the builder fills in outputs[0..2].type and outputs_data[0..2], the three base signatures can still remain valid, but the NFT minter/type contracts must still verify that all three final type/data values are valid mints. The minter cell at inputs[3] and outputs[3] is checked by the transaction-level SighashAllOnly flow, so it can validate the aggregate transition for all three mint requests.

Example 3: Using Append Segments

Append segments are for contributions added after the base OTX exists. Continuing the NFT example, each buyer’s base OTX might allow the builder to append the actual minted NFT output as a segment output. The minter cell still does not appear in any append input. It remains a transaction-level input so the minter can approve the whole batch with one SighashAllOnly flow.

An append-oriented complete transaction can be read like this:

Transaction
  cell_deps:
    [0] cobuild-otx-lock code       -> prefix dep
    [1] nft-minter-type code        -> prefix dep
    [2] minted-nft-type code        -> prefix dep

  inputs:
    [0] buyer A payment cell        -> mint OTX #0 base input
    [1] buyer B payment cell        -> mint OTX #1 base input
    [2] buyer C payment cell        -> mint OTX #2 base input
    [3] minter cell                 -> tx-level input, SighashAllOnly signed
    [4] fee cell                    -> tx-level fee payer input

  outputs:
    [0] minted NFT for buyer A      -> mint OTX #0 append output
    [1] minted NFT for buyer B      -> mint OTX #1 append output
    [2] minted NFT for buyer C      -> mint OTX #2 append output
    [3] updated minter cell         -> tx-level output, SighashAllOnly signed
    [4] fee payer change cell       -> tx-level or remainder output

  outputs_data:
    [0] NFT A metadata
    [1] NFT B metadata
    [2] NFT C metadata
    [3] updated minter state
    [4] empty fee-change data

  witnesses:
    [0] OtxStart
    [1] mint OTX #0 witness with buyer A base seal
    [2] mint OTX #1 witness with buyer B base seal
    [3] mint OTX #2 witness with buyer C base seal
    [4] SighashAllOnly witness for the minter cell and tx-level flow

In this example, the final transaction has the same three mint requests as the mask example, but the NFT cells are appended outputs rather than base outputs. Each buyer’s base OTX commits to the payment input and permits later append content. A one-output append segment records the final NFT output range. There are no buyer change outputs in this simplified transaction; the only change output shown is the fee payer’s tx-level change cell.

The OtxStart witness sets where OTX-owned ranges begin. In this transaction, all cell_deps are prefix deps. No OTX owns cell deps in this example:

OtxStart
  start_input_cell:  0
  start_output_cell: 0
  start_cell_deps:   3
  start_header_deps: 0

The three OTX witnesses then consume ranges sequentially from those starts. Each base OTX only needs append-output permission in this example:

append permissions
  append_inputs:      unused
  append_outputs:     allowed
  append_cell_deps:   unused
  append_header_deps: unused

Later, the builder constructs one append segment for each mint OTX:

mint OTX #0 append segment 0
  inputs:
    []

  outputs:
    [0] outputs[0] minted NFT for buyer A

mint OTX #1 append segment 0
  inputs:
    []

  outputs:
    [0] outputs[1] minted NFT for buyer B

mint OTX #2 append segment 0
  inputs:
    []

  outputs:
    [0] outputs[2] minted NFT for buyer C

After applying OtxStart, the append segment output ranges are therefore:

mint OTX #0 segment 0 outputs:   outputs[0..1]
mint OTX #1 segment 0 outputs:   outputs[1..2]
mint OTX #2 segment 0 outputs:   outputs[2..3]

This example has no append input, so there is no append signer and no append seal. The appended NFT outputs are allowed by the base OTX append permissions. They are still part of the final transaction and still checked by application contracts, but they are not independently signed by an append participant. Here the append segment is a layout and permission boundary, not an additional signature boundary.

For example, if the builder removes buyer A’s appended NFT output, the mint transition should fail in the NFT minter/type checks because the requested NFT was not created. If the builder changes buyer A’s base payment input, buyer A’s base signature no longer matches. The same reasoning applies independently to mint OTX #1 and mint OTX #2.

The fee cell in inputs[4] can be handled by the transaction-level flow in this example. It pays the transaction fee by contributing input capacity, and any unused capacity can return through outputs[4] as the fee payer’s change cell. That fee input and fee-change output are separate from the three mint OTXs in this simplified example.

The final transaction is valid only when all layers agree:

  • the buyers’ base signatures authorize each base payment input and append
    policy;
  • append permissions allow the appended NFT outputs in this example;
  • CoBuild actions describe the intended application transition;
  • lock scripts verify ownership and required signatures;
  • the minter cell’s SighashAllOnly signature authorizes the aggregate minter state
    transition;
  • type scripts verify the NFT minting and minter state rules.

Signing Hash Domains

The implementation separates signing domains:

  • transaction-level CoBuild with a message;
  • transaction-level CoBuild without a message;
  • OTX base scope;
  • OTX append segment.

Each domain uses its own BLAKE2b personalization string. The signing preimages are structured and length-prefixed where needed, so different logical field tuples cannot collapse into the same byte sequence.

For append segment signatures, the segment hash binds to the base hash. This means an append signer signs not only that segment’s appended entities, but also the specific base OTX being extended. A segment can also opt into previous coverage, in which case its hash binds all previous segments in order.

How Contracts Check a Transaction

The main reusable component is crates/cobuild-core.

At runtime, a contract builds a CobuildContext for its current script role. The context:

  1. reads transaction structure through syscalls;
  2. scans witnesses for CoBuild layouts;
  3. validates OTX sequence shape and counts;
  4. computes OTX ranges;
  5. exposes action lookup and validation plans.

For lock scripts, plan_lock_validation() returns:

  • the current lock script hash;
  • related actions;
  • required signatures;
  • the exact signing message hashes that must be verified.

For type scripts, plan_type_validation() returns:

  • related actions targeting the current type script;
  • whether an action is only targeted at the script or also lies inside the
    current OTX scope.

The example lock contract, contracts/cobuild-otx-lock, is intentionally thin. It:

  1. reads its args as a 20-byte public key hash identity;
  2. asks cobuild-core for lock validation requirements;
  3. rejects the transaction if no relevant signature is required;
  4. verifies every required seal using a local k256 secp256k1 recoverable
    signature verifier.

This keeps the lock script as a verifier boundary. It does not duplicate OTX layout logic or message parsing logic.

What This Enables

The repository demonstrates several useful application patterns.

Transaction-Level CoBuild Signing

A transaction can carry a SighashAll witness with a CoBuild message. Lock scripts can verify signatures that bind both the transaction hash and the message. Other lock groups can use SighashAllOnly while still signing the same transaction-level message hash.

OTX Base Signing

An OTX creator can sign a base scope. The final transaction may later include additional append segment outputs, inputs, deps, or other OTXs, as long as those additions are allowed by append permissions and do not alter fields covered by the base signature.

OTX Append Signing

Append participants sign append segments. Their signatures bind to the base commitment, so they cannot be replayed against a different base intent.

Mixed Transaction-Level and OTX Flows

The same final transaction can contain transaction-level CoBuild actions, OTX actions, and legacy-compatible scripts. The tests cover mixed sighash-all plus OTX signature requests.

Composed OTXs

Multiple OTXs can be combined into one final transaction. The tests include two UDT transfer OTXs in one transaction, an NFT-for-UDT swap built from multiple OTX pieces, and NFT minting flows involving multiple OTX actions.

Fine-Grained NFT Minting

The NFT minter tests demonstrate base output control. A minter can authorize an NFT output in the base scope while controlling only selected fields such as lock and capacity. This is useful for workflows where the buyer lock must be fixed early, while type/data can be completed by the minter or by later transaction assembly.

Limit Orders

The repository includes test-only limit order contracts for both type-script orders and lock-script orders. These contracts show how a real application can consume CoBuild actions while relying on cobuild-core for action discovery and OTX relation checks.

The limit order fixtures cover:

  • OTX-only fill flows;
  • transaction-level fill flows;
  • duplicate tx-level and OTX actions;
  • unrelated tx-level action noise;
  • wrong action targets;
  • payment outputs in another OTX or in the remainder region;
  • reused payment outputs;
  • real OTX lock signed base flows.

Repository Layout

The repository is split into reusable protocol crates, one example lock contract, and several test-only application contracts.

crates/cobuild-types

This crate contains Molecule schemas and generated readers/entities for CoBuild data structures. On-chain code uses lazy-reader views rather than owned entity objects where possible.

crates/cobuild-core

This crate contains the shared no_std protocol logic:

  • witness scanning;
  • CoBuild message views;
  • action lookup;
  • OTX layout validation;
  • OTX range computation;
  • signing hash construction;
  • lock/type validation planning;
  • syscall-backed transaction readers.

It is the main contract-side library used by application scripts.

contracts/cobuild-otx-lock

This is the reference CoBuild OTX lock. It is deliberately small: the contract delegates layout and signing-hash construction to cobuild-core, then verifies the resulting signature requirements against a 20-byte identity stored in the lock args.

The current local verifier uses k256 for secp256k1 recoverable signatures.

tests/contracts

These are test-only application contracts used to exercise realistic flows:

  • limit-order-type;
  • limit-order-lock;
  • nft-minter-type;
  • minted-nft-type;
  • test-udt;
  • test-nft.

They are not intended as production applications. They exist to prove that the CoBuild/OTX machinery can support real contract patterns.

tests/src/fixtures

The fixtures build CKB transactions with real witnesses, resolved inputs, cell deps, OTX layouts, action data, signatures, and expected outcomes. They are the main evidence that the protocol logic works across positive and negative cases.

Test Coverage

The integration tests are intentionally broad. They cover both protocol-level and application-level behavior.

CoBuild OTX Lock Coverage

The cobuild_otx_lock fixtures currently cover 31 scenarios, including:

  • invalid lock args;
  • missing relevant signature requests;
  • transaction-level CoBuild signatures;
  • current lock groups that start after input zero;
  • OTX base and append signatures;
  • full preimage signing;
  • append output mutation after signing;
  • same-lock inputs outside OTX scope;
  • other-lock inputs outside OTX scope;
  • missing, duplicate, wrong-scope, wrong-script, and malformed seals;
  • malformed OTX layout;
  • partial input/output/cell-dep/header-dep masks;
  • two UDT transfer OTXs in one transaction;
  • OTXs with a sighash-all fee input;
  • NFT-for-UDT swap OTX composition;
  • mixed sighash-all and OTX signature requests.

Limit Order Coverage

The limit order type-script fixtures cover 33 scenarios. The limit order lock-script fixtures cover 30 scenarios.

Together they test:

  • valid OTX fills;
  • transaction-level and OTX action interaction;
  • duplicate actions;
  • unrelated action noise;
  • missing or wrong action targets;
  • malformed actions;
  • wrong NFT or UDT outputs;
  • insufficient payment;
  • wrong owner;
  • reused payment outputs;
  • mixed type/lock order flows;
  • real OTX lock signatures.

Some scenarios allow more than one valid failure source. For example, if a transaction contains both an invalid application transition and an invalid NFT output type, either script may be the first failing script depending on VM execution order. The test framework supports this with an AnyOf expected outcome.

NFT Minter and Minted NFT Coverage

The NFT minter fixtures cover 46 scenarios, including:

  • creating a minter;
  • minting serials from different counters;
  • supply cap checks;
  • wrong counter, serial, rarity, minter hash, and attributes;
  • missing or duplicate NFT outputs;
  • forged NFT creation;
  • NFT transfer and burn;
  • minter burn;
  • multiple input/output group shape checks;
  • tx-level mint actions;
  • mixed tx-level and OTX minting;
  • mint outputs in base scope, append segment outputs, other OTXs, and remainder outputs;
  • real OTX lock signed-base minting;
  • base output masks for NFT lock/capacity;
  • tampering with covered versus uncovered fields;
  • missing or bad OTX base seals;
  • multiple OTX actions feeding one minter transition.

These tests are important because they exercise the fine-grained base output masking model in a realistic minting workflow.

What Is Being Validated

The repository validates three layers of behavior.

First, it validates protocol mechanics:

  • CoBuild witness encoding;
  • OTX sequence layout;
  • OTX entity ranges;
  • mask length and padding rules;
  • signing hash domain separation;
  • action references and action hashes.

Second, it validates lock-script behavior:

  • transaction-level signature requirements;
  • OTX base signature requirements;
  • OTX append signature requirements;
  • no silent success when a CoBuild-aware lock has no relevant signature.

Third, it validates application behavior:

  • type and lock scripts can consume CoBuild actions;
  • OTX scope information is sufficient for application checks;
  • composed OTX transactions can express swaps, payments, and NFT minting;
  • invalid transaction assembly is caught either by the protocol layer or by the
    application contracts.

Current Status

This repository should be read as a concrete engineering exploration of CoBuild OTX rather than a final specification. The important result is that the design is executable: it builds real CKB RISC-V contracts, signs real OTX witnesses, and verifies complete transactions with CKB testtool.

The implementation is also intentionally modular. Application contracts do not need to reimplement witness scanning or signing hash construction. They can ask cobuild-core for the relevant actions and signature requirements, then focus on their own state transition rules.

This makes the repository useful as:

  • a reference implementation for contract authors;
  • a regression test suite for CoBuild OTX protocol ideas;
  • a place to evaluate fine-grained authorization semantics;
  • a basis for discussing wallet, builder, matcher, and app integration flows.

The next useful direction is to connect this contract-side implementation with off-chain builder and wallet tooling, so the same CoBuild/OTX data model can be used from user intent, to partial transaction construction, to final on-chain verification.

10 Likes

虽然我看不懂,但你真是人类之光。

1 Like