iCKB journey into CoBuild

I have a feeling we might be talking non-sense here without a clear picture of iCKB structure, so I will cease from commenting on the following assertions:

Deposit would be doable without limit orders even if it’s a two step process.
Withdrawals is non doable without supporting partial fulfill-ability.

Maybe they are true, maybe not. There is just not enough information to judge. I will leave that topic till another discussion.

Instead, I will refocus on the main point I want to make here:

  • Cobuild, in a sense, already provides P2SH by lock and P2SH by type for free now.
  • I would not recommend building a CKB app against a particular P2SH by type feature implemented in just omnilock here. It would be better to build apps using conventions from cobuild, so iCKB can ideally work with any potential cobuild-enabled locks. We’ve learned enough lessons, this is an example: Kollect.me 这个项目没了?都不知道有没有NFT?

Let me explain one thing first: to me, the administrator mode in omnilock was designed at an early time, from a wrong set of assumptions, with a poorly design. I would not encourage anyone to build upon administrator mode anymore, it is there for compatibility reasons, but if you really ask me, we should simply ditch it.

The core idea behind P2SH by type auth, correct me if I am wrong, is to defer all the validation logic of the omnilock to another script in the current transaction. Even signature validation part in the omnilock is completely skipped, omnilock will have to trust the deferred type script to perform all necessary security checks.

My question here is: is this really something we want?

In a cobuild-compatible world, my personal vision, is that iCKB users will submit deposit & withdraw requests in the form of cobuild OTXs. Ideally iCKB users can use any cobuild-compatible locks in the input cells of those OTXs but let’s make it simple for now: assume those iCKB OTXs all come with input cells using omnilock as lock scripts. Let’s also assume that P2SH by type auth is implemented, all the input cells in an iCKB OTX, will defer the actual validation work to the iCKB type script included in the full CKB transaction as a type script.

I now have a question to ask: how can the user be sure here, that his / her submitted iCKB OTX will not be tampered with?

The lock scripts of input cells from the iCKB OTX, will simply be omnilocks that defer the actual execution to an external type script. No signature validation is performed here. Any part of the OTX, including input cells, output cells, cobuild messages can be tampered by anyone in anyway.

Yes it can be argued that it is still possible to build a secure implementation here:

  • An iCKB Action object addressed to the iCKB type script is included in the cobuild message of the submitted iCKB OTX
  • iCKB parameters, such as deposit & withdraw details are included in the iCKB Action object
  • A user signed signature covering the whole OTX is included in the iCKB Action as well

The iCKB type script then loops against each OTX and does signature validation.

This solution works, but you are essentially coding against cobuild. There is nothing forbidding you from doing this, the best I can say to you here is, cobuild is not designed to be used this way.

I do want to stress this point one more time: this albeit-working solution is also manually coded against one particular feature implemented by one particular lock, P2SH by type auth is not a commonly-defined feature implemented by all cobuild-compatible locks. We’ve learned enough lessons, I plead you and every CKB developers that are reading this to never design your app against a particular lock or type ever again.

What would be cobuild’s recommended way?

  1. A user creates an iCKB OTX covering his / her deposit or withdraw intentions.
  2. The input cells in this iCKB OTX are free to use any cobuild-compatible locks, a proper signature is generated by the user guarding the whole OTX, and be put in the seals part of the OTX witness data structure
  3. The deposit / withdraw parameters used by iCKB can also be put in an Action object in the Message field of this particular OTX. This very Action object shall use iCKB type script’s hash as script_hash value. By definition of cobuild protocols, a cobuild-compatible lock will ensure that iCKB type script is included the full CKB transaction.
  4. The iCKB type script can also loop through all OTXs in current CKB transaction, extracting Action objects for iCKB, does necessary validations. The only difference, is that iCKB type script will not perform signature validation for each individual OTX. And this is one of the major point of OTX: locks should handle ownership, prevent malleability, whlie types does the real data checking.
2 Likes

I fully agree, but it has its own limitations too :raised_hands:

It came to my mind too but since it’s Omnilock and so it supports most form of locking and it will be maintained, I brushed it off. Then again fair point, thanks!! :pray:

Yup yup, this workflow works well for most protocols where user fully controls the underlying. Here it’s shared. Still let’s assume we use the CoBuild recommended way:

  • Both Deposits and Withdrawals are a two (or more) steps process.
  • Deposit have a per deposit and per transaction limits, so user with big amounts have to split before into smaller cells to avoid the partial fulfillment issue (and limit of 64 output cells) and later on authorize otx for each cell (~100K CKB per standard deposit).
  • User with small amounts have to group together to come close to a standard deposit size. They just authorize otx, aggregator does the grouping.
  • Let’s say we can manage Deposit step in your way, which is tricky for the second step, but maybe doable.

How to implement withdrawals?

  • Let’s assume exists an entity that is willing to lend some capital and take the role of aggregator.
  • All existing deposits are of very similar size, but not exactly same size.
  • Users who want to withdraw small amounts have to group together their funds to come close to withdraw a standard deposit size.
  • We are able to do the first withdrawal step with otx thanks to the aggregator who provides capital to avoid the otx partial fulfillment issue.
  • Now a single withdrawal request has possibly many users claiming a piece of it, possibly including the aggregator.
  • The owners of the Withdrawal Requests are stored in possibly many WR Receipts, one per users (or one per each user & Withdrawal Requests pair).
  • Each user take turns into claiming his capital from the WR cells using the WR receipt(s).

This show that going with CoBuild design is maybe possible (devil is in the details tho), but the number of user interactions is a little too high (especially for users with bigger capital who need to split their capital beforehand and authorize many otx) and the logic for grouping/splitting the deposits/withdrawals is a little too complex (consider final amounts depends on Header of the first transaction). Additionally now this logic lives in the main iCKB script.

(Keep in mind iCKB will adopt xUDT to make the script code super simple, so to cut the attack surface for possible exploits :sweat_smile:)

This CoBuild OTX logic could live instead in a second contract, which handles all grouping and splitting. This means one additional transaction. Then again this would likely defeat the CoBuild otx main point: using user partial signatures to assure the safety of transactions.

On the other side, we have a super simple limit order lock that is completely separated from iCKB and Omnilock, where the user can fully specify his intentions. Once the logic is sound it cannot be tampered with and and it’s partially fulfillable by design. Sure, this approach has its own limitations as it’s capital intensive for the aggregator in the iCKB to CKB step.

I have a proposal for iCKB, but it’s slightly outdated… Then again if you’d like to bless me with your valued opinion, I’ll update it just for you!! :hugs:

I fully agree, but it has its own limitations too :raised_hands:

True but every solution, including P2SH by type auth introduced here has its own limitation.

it’s Omnilock and so it supports most form of locking and it will be maintained

Personally I consider this is the reason for me to use omnilock, not the reason my app should target only omnilock. For future CKB apps, I would discourage designing upon one particular lock or type, no matter what quality or support one can get from that particular lock or type.

Regarding general deposit & withdraw designs, I’m afraid we are mixing different issues together:

  1. How to move from a custom-designed pattern to cobuild design.
  2. NervosDAO’s current 64-DAO-output-cell limitation adds trouble when splitting big deposit into smaller one, and when merging smaller withdraws into bigger one.

To me the 2nd issue still exists and requires solving even if we use a P2SH by type auth design. The issue is still there. A cobuild-compatible design, in my opinion, merely requires transforming certain data to a different location, it really does not affect the overall workflow.

Let me just say: I don’t really see how P2SH by type auth solves the 2nd issue, while cobuild can’t.

It might be the case that we are still talking in general texts that are quite misleading. If you want, there is no need for a detailed spec, but might be better to add a tx example or two showcasing how splitting deposits and merging withdrawals can be handled in a P2SH by type auth design, we can then see how it can be transformed to a cobuild design.

1 Like

Actually it is pretty complex problem, independently of Cobuild OTX Actions, as there are even more subtle complications like:

  • Exchange rate between CKB and iCKB UDT is dynamic, depending on Header.
  • Amount of iCKB minted for a CKB deposit OTX depends on the aggregated deposit size, chosen by the aggregator…
  • NervosDao implementation is very much positional based.

The only way I ever saw to abstract over NervosDao and iCKB protocol limitations by using a cell lock that implements a limit order (abstracting the user intention) and that anyone can (partially) fulfill, similarly to an ACP. If you are interested, I can detail this one instead, so you can give me your valued opinion on its security.

So we create a general interface abstracting over the protocol details and a DApp will be easy to build on top of it.Then again now we need a bot with capital to do the actual interactions with the underlying iCKB protocol itself and fulfilling the orders. This bot usually requires no user interactions and for now it uses an unencrypted private key on .env file for facilitating operator management.

In the past I also wanted to design a separate direct iCKB DApp to bypass the limit orders and interact directly with the iCKB deposit pool in case of extreme scenarios. Then I realized that this would be a duplicate of the bot functionalities as the bot already has to handle operator ingress and egress. By turning the bot interface into a Wallet-enabled locally run DApp we can achieve the following:

  • Depending on the signature method it can be fully autonomous or require Wallet authorization.
  • Advanced users and operators are served by the same single entry-point.
  • Users can choose to become advanced users and bypass fees.
  • Users can choose to become operators after seeing how easy it is to use.
1 Like

Please do provide a detailed design however it might work, we can then work from there for a cobuild-enabled variation

2 Likes

I’d like to thank you again for your time and support, I appreciate a lot!! Usually I discuss these topics with @jm9k and we agree on most decisions, so hearing your perspective (which usually differs from mine) would be beneficial for both iCKB and me :pray:

Current design

Let’s start with Limit Order then. After talking with @doitian, I adopted the following molecule template schema:

array Uint64 <byte; 8>;
array Hash32 <byte; 32>;

array Bytes<T> <byte; T>

struct lock<T> {
    codeHash: Hash32,     // 32 bytes
    hashType: byte,       // 1 byte
    args: Bytes<T>        // T bytes
}

struct OrderArgs<T> {
    userLock: lock<T>           // 33 + T bytes
    sudtHash: Hash32,           // 32 bytes
    isSudtToCkb: byte,          // 1 byte
    ckbMultiplier: Uint64,      // 8 bytes
    sudtMultiplier: Uint64,     // 8 bytes
    logMinFulfillment: number,  // 1 byte
}

union OrderArgs {
    OrderArgs<0>,
    OrderArgs<1>,
    ...
    OrderArgs<255>,
}

Currently the limit order script memorize all these parameters in its args, no data memorized anywhere else. The script validates that each interaction with a limit order cell is either a valid (partial) fulfillment or a cancel action.

The contract is positional based, so if at input index i there is a cell with limit order lock, the output cell at index i is either a limit order (similarly to NervosDao withdrawal request) or the complete fulfilled cell with user lock.

Also there is the possibility to cancel a limit order (at index i) by signature delegation (in input must exist a cell with user lock) and the output cell at index i is a cell with user lock.

Pitfall of transition from limit order to user lock without a signature

My constant fear with this design is that it may exists another script, let’s call it cooler limit order, with slightly different validation rules. A user may use both at the same time, place a order in both and unknowingly both orders have compatible completely fulfilled cells. (These cells have user lock for improved user experience as they effectively remove one transaction for the user.) The attack vector is the following: an attacker can completely fulfill both limit order and cooler limit order with the same output cell (so both scripts validate) and be able to steal the rest. This is a remote possibility, still it is troubling.

Ideas for a CoBuild OTX design

  1. Due to the possible attack, we could ask that the completely fulfilled cell step happens only with user signature delegation, same as with cancelling. If necessary we could also memorize user lock as Hash32 and this would also avoid having a dynamic length field.

  2. Since CoBuild OTX is definitely not positional friendly (or better its positional to the OTX), the output cell must be able to reference the input cell in other ways. For example the action could store both the input outpoint of the cell is matching against and a reference to the output cell. An alternative is to define a synthetic ID that identify that particular limit order. Then again I see no way to guarantee its unicity. It could be unique in the OTX, but it may still create issues. Another possibility could be to keep the positional approach but relative to the OTX.

  3. The signature delegation in a CoBuild OTX Order would need to check that input user cell exists in the very same OTX. Same tx is not enough to assure safety as it could be attacked easily.

  4. We don’t need to require for the corresponding output of fulfilled cell or cancelled cell to have user lock. Same as when completing the withdrawal from a NervosDao, it can be right away used for anything the user decides. This should be safe since the OTX contains user signature.

  5. Since now the action Cancel and action Withdraw Fulfilled have the very same logic, they can be defined as Melt, so there are only three CoBuild Actions: Mint, Fulfill and Melt. Only Melt requires delegated user signature, while Fulfill is exclusively validated by Order logic.

  6. Limit Order will support both sUDT and xUDT.

  7. All data could be memorized into the CoBuild witnesses’s action and lock script may only need to memorize the hash of this data as script args or XudtData lock for next fulfillment round. In case of sUDT the very same XudtData convention can be used.

  8. If we impose the rule for all limit orders to have already the correct UDT Type (so not undefined) at Mint time, then it could be possible to drop the sudtHash field from Args. Then again if we follow the design of 7 it could still be required in Action.

  9. Another idea concurrent to 7 (memorize only hash of data) is to store limit order data into a secondary cell, with limit order type and user lock. This cell is then referenced as CellDep and its outpoint is the synthetic ID needed to identify the order. The limit order lock script could contain either directly the outpoint (as args or XudtData) or just referece its position in the otx CellDeps (so the weaker unique ID in OTX). This design also improves on the security of Melt as it requires a specific user cell (this secondary cell) to be consumed in the same OTX of the limit order Melt. Then again this solution would require an additional disbursing of CKB for the secondary cell and for referencing it from the limit order cell.

All in All, probably a variation of 9 would be the most future proof design I can think of, then again 7 would have a smaller CKB footprint.

1 Like

I must say that I still don’t see a detailed design here. We don’t need a fully compete proposal to start talking, but I would personally expect something like the spore examples put together, so you can see what a complete CKB transaction with iCKB interactions might be. Unfortunately what we have here is just a snippet of a small data structure, I must say I still don’t have a picture on the full iCKB design to comment if it is viable.

Instead, I’m gonna do a slightly different way: I will put together a small design if I’m doing a liquidable Nervos DAO UDT in the context of cobuild. I will first list a few assumptions I will use in my UDT, then come up with a design from the assumptions. It’s possible that my assumptions here will be different from iCKB’s assumptions, hence leading to different designs. But with the information I have at hand, this is the best I can do.

So without further ado, here are the assumptions I will use:

  • A special UDT token will be introduced. I’m gonna name it xDAOCKB. xDAOCKB can be minted when a Nervos DAO deposit into a special lock address is included in the same CKB transaction. One can only mint as many as xDAOCKB as the DAO deposit.
  • Smaller deposits and bigger withdraws are supported by xDAOCKB, but as we shall see later, they will be implemented via Cobuild OTXs, not as Nervos DAO deposits / withdraws directly.
  • xDAOCKB can be freely transferred to any cobuild-compatible accounts(this has a potential BIG issue we will address later).
  • Any xDAOCKB owners can put up limit orders selling xDAOCKB in exchange for (could be slightly smaller than typical DAO withdrawals to give liquidity provider incentives) corresponding CKBs with some interests. A limit order will also be in the form of Cobuild OTX.
  • A liquidity providers can fully / partially fulfill limit orders in the form of Cobuild OTX, buying and burning xDAOCKBs from limit orders, providing limit order creators with specified CKBs, and initialize CKB withdraw requests in the same CKB transaction.

I personally believe the above assumptions already make a decent liquidable Nervos DAO app but if you have a specific requirement you want in mind, feel free to add via more comments.

And there is also one big issue with xDAOCKB that could potential lead to drastically different design: not all xDAOCKB tokens are created equal. Assuming that 1000 xDAOCKBs are issued at block 10000 with DAO deposit cell A, but 2000 xDAOCKBs are then issued at block 20000 with DAO deposit cell B, what if people combine the xDAOCKBs issued at block 20000 with DAO deposit cell A for withdrawals? This would create an unfair situation. Here I’m gonna talk about 2 different designs, they all have their own pros & cons. Different people would favor different one, but as protocol designers, I would simply list both designs without preferences.

  1. We add a validation rule to xDAOCKB: the creation order of Nervos DAO deposit cells will be maintained by xDAOCKB, so that a liquidity provider can only provide the earliest Nervos DAO deposit cell to withdraw from. The market will then adjust the limit order price, so a reasonable balance between Nervos DAO interest and the buy-out price can be reached.
  2. Take inspiration from Bitcoin Inscription and Spore: what if we treat each issuance of xDAOCKB as a NFT, not UDTs? Different xDAOCKB are issued at different time for different Nervos DAO deposits. What if we treat the whole issuance of xDAOCKB as an NFT, and only allowing the transfer of a batch of xDAOCKBs as a group? That will essentially simply withdrawing by a large margin.

Again, let me stress the point that it is not my position nor my interest to debate which of the above 2 designs are a better one, I’m just saying I see 2 solutions here, both work from a technical perspective, and I will explain both here.

Ordering withdrawals

Let’s first talk about the case where we can treat xDAOCKB as true UDTs and can be freely transferred in any amount.

For xDAOCKB to work on chain, there are 3 different types of cells involved:

  • xDAOCKB entity cell: a unique(could be ensured via type id) entity cell exists on chain for a particular deployment of the xDAOCKB app. It contains certain metadata information(such as the minimal CKBytes per Nervos CKB deposit batch), or bookkeeping information(all Nervos DAO deposits into xDAOCKB app). This cell will use an always-success script as the lock script, and a xDAOCKB app script as the type script. Note that xDAOCKB app script does the majority of the validation work of xDAOCKB.
  • xDAOCKB asset cell: a series of cells used to hold CKBytes deposited into xDAOCKB app. A xDAOCKB app might have a series of xDAOCKB asset cells depending on actual usage. A xDAOCKB asset cell will use a xDAOCKB ensuring script as the lock script, and the Nervos DAO type script as the type script. The xDAOCKB ensuring script only does one thing: it validates that current CKB transaction has a cell using xDAOCKB app script as the type script
  • xDAOCKB cell: this just refers to cells containing xDAOCKB UDTs. Per UDT’s design, a xDAOCKB cell can use any lock script as the lock, it uses xDAOCKB UDT script as the type script. There can be as many xDAOCKB cells as possible on chain, one is also free to split xDAOCKB cell into multiple xDAOCKB cells each holding a portion of xDAOCKB UDTs. I believe we can simply use xDAOCKB app script as the owner lock for xDAOCKB UDT script.

Deposit OTX

Assuming Alice wants to deposit 1100 CKB into xDAOCKB app, Alice can create an OTX like the following:

inputs:
  input 0:
    capacity: 5000 CKB
    lock: Any OTX-compatible lock of Alice's (let's call it lock A)
    type: <EMPTY>
    data: <EMPTY>
outputs:
  output 0:
    capacity: 3499.99 CKB
    lock: Any lock of Alice's
    type: <EMPTY>
    data: <EMPTY>
  output 1:
    capacity: 400 CKB
    lock: Any lock of Alice's
    type: xDAOCKB UDT script
    data: 1100 xDAOCKB
witnesses:
  witness 0: WitnessLayout format, Otx variant
    seals:
      0: Seal format
        script_hash: lock A's hash
        seal: signature for lock A
    input_cells: 1
    output_cells: 2
    message: Message format
      actions:
        0: Action format
          script_info_hash: xDAOCKB UDT ScriptInfo's hash
          script_hash: xDAOCKB UDT script's hash
          data: cobuild message for issuing 1100 xDAOCKB
        1: Action format
          script_info_hash: xDAOCKB app ScriptInfo's hash
          script_hash: xDAOCKB app script's hash
          data: cobuild message for depositing 1100 CKB

Several interesting points of this OTX:

  • It does not use Nervos DAO script at all. There is no deposit cells. There is only a cell for issuing 1100 xDAOCKB. So there is no concern about Nervos DAO’s script limits, a user can deposit any amount of CKBytes as he / she wants.
  • The user is free to use any lock as he / she wants. The input cell must use an OTX-compatible lock since this is an otx, the output cells are free to use any lock, even non-OTX-compatible ones.
  • The user is in charge of providing capacity cost for the xDAOCKB cells(in reality this can change), the user also provides 0.01 CKBytes as incentives for OTX processors to process this OTX
  • A signature is included in seals to ensure no one can alter this OTX
  • The cobuild message part has 2 Action objects: one for issuing UDT script, one containing the action for xDAOCKB app

Merging Deposit OTXs into CKB Tx

With a few gathered deposit OTXs, an OTX processor can then merge then into one CKB transaction, creating a single Nervos DAO deposit request:

inputs:
  input 0(otx #0):
    capacity: 5000 CKB
    lock: Any OTX-compatible lock of Alice's (let's call it lock A)
    type: <EMPTY>
    data: <EMPTY>
  input 1(otx #1):
    capacity: 3000 CKB
    lock: Any OTX-compatible lock of Bob's (let's call it lock B)
    type: <EMPTY>
    data: <EMPTY>
  input 2:
    capacity: xDAOCKB app capacity
    lock: always-success lock
    type: xDAOCKB app script
    data: xDAOCKB app data
outputs:
  output 0(otx #0):
    capacity: 3499.99 CKB
    lock: Any lock of Alice's
    type: <EMPTY>
    data: <EMPTY>
  output 1(otx #0):
    capacity: 400 CKB
    lock: Any lock of Alice's
    type: xDAOCKB UDT script
    data: 1100 xDAOCKB
  output 2(otx #1):
    capacity: 499.99 CKB
    lock: Any lock of Bob's
    type: xDAOCKB UDT script
    data: 2500 xDAOCKB
  output 3:
    capacity: 3600 CKB
    lock: xDAOCKB ensuring script
    type: Nervos DAO script
    data: Nervos DAO data
  output 4:
    capacity: updated xDAOCKB app capacity
    lock: always-success lock
    type: xDAOCKB app script
    data: updated xDAOCKB app data
witnesses:
  witness 0: Witness for Otx #0
  witness 1: Witness for Otx #1

This CKB transaction merges 2 OTXs together: Alice deposits 1100 CKB to xDAOCKB via OTX #0, Bob deposits 2500 CKB to xDAOCKB via OTX #1. But as the CKB Tx shows, no matter how many deposit OTXs there are, there is only one Nervos DAO deposit cell created(output 3 above), it combines all the individual deposits from each OTX together into a single Nervos DAO deposits. xDAOCKB entity cell(input 2 & output 4) is also included in the CKB transaction, so xDAOCKB app script can perform all the checking needed, and update all the bookkeeping information(current Nervos DAO deposit information must also be included in xDAOCKB app data for maintaining deposit ordering). Also sice xDAOCKB app script is included in current Tx, xDAOCKB ensuring script can succeed in the execution. xDAOCKB UDT script can also successfully issuing xDAOCKBs to Alice and Bob.

Withdraw OTX via a limit order

With xDAOCKB, there is no direct withdrawing from Nervos DAO, instead a withdraw request is handled as a limit order:

inputs:
  input 0:
    capacity: 400 CKB
    lock: Any OTX-compatible lock of Alice's (let's call it lock A)
    type: xDAOCKB UDT script
    data: 1100 xDAOCKB
outputs:
witnesses:
  witness 0: WitnessLayout format, Otx variant
    seals:
      0: Seal format
        script_hash: lock A's hash
        seal: signature for lock A
    input_cells: 1
    output_cells: 0
    message: Message format
      actions:
        0: Action format
          script_info_hash: xDAOCKB UDT ScriptInfo's hash
          script_hash: xDAOCKB UDT script's hash
          data: cobuild message for burning (at most) 1100 xDAOCKB
        1: Action format
          script_info_hash: xDAOCKB app ScriptInfo's hash
          script_hash: xDAOCKB app script's hash
          data: cobuild message for withdrawing limit order
            ask_interest: 0.00005 CKB
            partial_fulfill_enabled: true

Like the deposit OTX, the withdraw OTX does not contain any Nervos DAO cells. It merely provides an input cell providing xDAOCKB, and an Action object(at action #1) containing limit order information. Of course different app would want to use different parameter formats as limit order information, here I merely include the absolutely necessary part:

  • ask_interest: for each xDAOCKB sold by the limit order, we ask for 1.00005 CKB
  • partial_fulfill_enabled: whether you can partially fulfill the order, or you can only fulfill the complete order.

The total tradable xDAOCKB, is thus read from the OTX itself. In the above OTX, Alice wants to trade at most 1100 xDAOCKB for CKB. But if Alice only wants to trade less xDAOCKB, a slightly different OTX can be used:

inputs:
  input 0:
    capacity: 400 CKB
    lock: Any OTX-compatible lock of Alice's (let's call it lock A)
    type: xDAOCKB UDT script
    data: 1100 xDAOCKB
outputs:
    capacity: 400 CKB
    lock: Any lock of Alice's
    type: xDAOCKB UDT script
    data: 600 xDAOCKB
witnesses:
  witness 0: WitnessLayout format, Otx variant
    seals:
      0: Seal format
        script_hash: lock A's hash
        seal: signature for lock A
    input_cells: 1
    output_cells: 1
    message: Message format
      actions:
        0: Action format
          script_info_hash: xDAOCKB UDT ScriptInfo's hash
          script_hash: xDAOCKB UDT script's hash
          data: cobuild message for burning (at most) 1100 xDAOCKB
        1: Action format
          script_info_hash: xDAOCKB app ScriptInfo's hash
          script_hash: xDAOCKB app script's hash
          data: cobuild message for withdrawing limit order
            ask_interest: 0.00005 CKB
            partial_fulfill_enabled: true

In this OTX, Alice only wants to trade 500 xDAOCKB.

Merging Withdraw OTXs into CKB Tx

In xDAOCKB, Liquidity providers act as OTX processors, they monitor withdraw OTXs submitted by users, and when they see orders they want to fulfill, they can merge those withdraw OTXs into a proper CKB Tx, like the following:

inputs:
  input 0(otx #0):
    capacity: 400 CKB
    lock: Any OTX-compatible lock of Alice's (let's call it lock A)
    type: xDAOCKB UDT script
    data: 1100 xDAOCKB
  input 1(otx #1):
    capacity: 400 CKB
    lock: Any OTX-compatible lock of Bob's (let's call it lock B)
    type: xDAOCKB UDT script
    data: 3000 xDAOCKB
  input 2:
    capacity: 10000 CKB
    lock: xDAOCKB ensuring script
    type: Nervos DAO type script
    data: Nervos DAO data
  input 3:
    capacity: xDAOCKB app capacity
    lock: always-success lock
    type: xDAOCKB app script
    data: xDAOCKB app data
  any required cells from LP to make CKBytes balanced
outputs:
  output 0(otx #1):
    capacity: 400 CKB
    lock: Any lock of Bob's
    type: xDAOCKB UDT script
    data: 1000 xDAOCKB
  output 1(fulfilling otx #0):
    capacity: 1100.1 CKB
    lock: Recipient lock specified by Alice
    type: <EMPTY>
    data: <EMPTY>
  output 2(partially fulfilling otx #1):
    capacity: 1000.1 CKB
    lock: Recipient lock specified by Bob
    type: xDAOCKB UDT script
    data: 1000 xDAOCKB
  output 3:
    capacity: 7900 CKB
    lock: xDAOCKB ensuring script
    type: Nervos DAO type script
    data: Nervos DAO data
  output 4:
    capacity: updated xDAOCKB app capacity
    lock: always-success lock
    type: xDAOCKB app script
    data: updated xDAOCKB app data
  output 5:
    capacity: 10000 CKB
    lock: Any lock of LP's
    type: Nervos DAO type script
    data: Nervos DAO data for withdrawing cell
  any changed cells to LP

In the above transaction, we have completely fulfill OTX #0(1100.1 CKB for 1000 xDAOCKB), while partially fulfill OTX #1(1000.1 CKB for 1000 xDAOCKB, while Bob wants to trade for 2000 xDAOCKB). Even though there are multiple limit orders, we are only doing withdrawing from one Nervos DAO deposit cell(input 2). For security reason, if burned xDAOCKB is less than CKBytes in withdrawed Nervos DAO cell, a new Nervos DAO deposit cell must be created(output 4 here). xDAOCKB entity cell is also included(input 3 & output 4) which does the bulk validation and bookkeeping work.

Recall the previous dicussion that xDAOCKB issued at different time is not equal, some might be issued from DAO deposit cells that exist longer than others, hence bearing more interested. To prevent attackers from exploiting this behavior, xDAOCKB entity cell will validate that the withdrawed Nervos DAO cell(input 2) is the oldest DAO deposit cell for current xDAOCKB app.

Apart from displayed cells, above, an LP might need to include other cells in the transaction as well, so as to provide CKBytes for fulfilled limit orders, as well as L1 transaction fees. The LP can then claim the withdrawed Nervos DAO cell(output 5), and gather the locked CKBytes with interests after a certain lock period.

Till this point, we’ve designed a liquidable Nervos DAO app that supports small & large deposits / withdraws, is not affected by Nervos DAO’s script limitation, and also supports complete / partial fulfillment of limit orders. Such an app is also cobuild-compatible, and is designed to leverage cobuild’s protocol from ground up.

NFT-style xDAOCKB

NFT-style xDAOCKB is similar to the above, but would be different in certain points:

  • The issued xDAOCKB must keep the block number of the DAO deposit cell, in its cell’s data storage. This can be done in several ways:
    • A 2-phase design can be used: a first transaction deposits CKBytes into Nervos DAO, and a follow-up transaction issues the xDAOCKB tokens
    • A proximation can also be used: we can set the since value of one input in the transaction, then use block number / epoch number contained in since value as the block number used for issuing xDAOCKB tokens, this would ensure that the DAO deposit cell can only be committed at or after the since value. Also providing security guarentees.
  • A cell containing xDAOCKB can only be tranferred as a single unit, there is no way to break an xDAOCKB cell into multiple cells, each containing a part of xDAOCKB tokens
  • There is no need to maintain the order of deposited DAO cells in xDAOCKB entity cell
  • When building withdrawing CKB transaction, xDAOCKB app script must check that for each consumed xDAOCKB cell, the included block number in xDAOCKB cell, must be no bigger than the block number where withdrawing DAO cell is committed.

This way we are limiting xDAOCKB cells so they can only earn interests starting from the point they are first issued, but not before that particular time point.

Recap

Here we have shown that how to build a liquidable Nervos DAO app in the context of cobuild protocol. What’s best about this protocol, is that we are not hardcoding against any particular lock script, one is free to use any lock to interact with xDAOCKB, such as secp256k1-sighash-all included in genesis block, omnilock, Unipass, dotbit(I’m not 100% on the dotbit part, since last time I checked dotbit still uses a lot of type scripts, and this app requires a wallet to only use lock script), Joy ID, etc. Hopefully this can shed some light on future CKB app development.

4 Likes

You know @xxuejie, I feel that we are doing double work here!! :rofl::rofl::rofl:

Point taken, I updated the iCKB proposal to a xUDT design. Let me know if I should cross post it here! :hugs:

Hmmm, trouble ahead

Exactly!! This is the underlying issue, this design is more similar to dCKB than iCKB. It could easily be implemented by using a custom lock for new dCKB deposits receipts. On the other side, iCKB basic idea is to account all deposits in a fair way. It’s a bit more involved since it requires accessing the header, but it’s well worth it.

This requires a global state update to account every deposit. Also crucially increases illiquidity as increase the capital requirement for liquidity providers as the oldest deposit on average is ~90 epochs away from Claim time (assuming uniform probability distribution), while in the worst case it could be ~180 epochs away :exploding_head:

In iCKB every deposit can be freely withdrawn from, especially those whose Claim time is close.

This protocol is vulnerable to DoS. Let’s assume an attacker has a big enough capital (in the order of GB), he can withdraw from small deposits and redeposit in deposits as big as the entirety of his capital. Doing this for all xDAOCKB controlled deposits will hamper the fruition of xDAOCKB as it’s impossible to withdraw partially from a NervosDAO deposit.

To avoid this form of DoS it is necessary to put a cap on the deposit size.

This is also the reason why iCKB poses a soft limit on the deposit size. Exceeding this limit can cause up to 10% loss. So this attack is prevented by slashing the attacker capital.

My approach instead obtains a similar results with a value high enough of logMinFulfillment also allows higher expressiveness.

About partial fulfillment, our partial fulfillment interpretations have crucial differences.

In my interpretation user funds are locked with a custom lock that validates if a partial fulfillment is valid, no user signature involved.

In your example every time the order is partially fulfilled, the user must sign another OTX. This could improve security (to prove this point you should provide an example where my implementation would inevitably fail) at the expenses of User Experience. Then again you also must include additional logic for validating a partial fulfillment, which would have pitfalls similar to mine.

I’d say that if an order is small enough to be fully fulfilled (no partial fulfillment), your interpretation for limit orders has better user experience, while for amounts too big mine is better.

Now I’m thinking, since your limit order interpretation works really well for small amounts, I could make the user interact directly with the protocol until their remaining assets to convert are less than one standard deposit (or few standard deposits). At that point I could use your limit order approach (without partial fulfillment). This approach has a user experience similar to your design after the necessary Dos mitigation measures.

By the way, this flow is very similar to the bot use-case I was proposing before, see:

Except this time a CoBuild limit order is used.

This could have some state contention issues at withdrawal time, then again I could let the user make an informed choice between creating a limit order and interacting directly with the protocol, so it should be fine.

By the way, how does the aggregator/bot collect these otx? Is there any standardized way?

Not really my cup of tea, but it could be promising. In Spore you can add additional CKB to a NFT for increasing its intrinsic value and it can be redeemed with Melt. It could be possible to integrate a deposit within Spore itself as a secondary cell controlled by the Spore NFT.

Hmmm, I see your point now. I’d say it kind of defeat the purpose of creating this protocol. It has much better value in my erroneous interpretation as Spore + NervosDAO!! :sweat_smile:

1 Like

You know @xxuejie, I feel that we are doing double work here!! :rofl::rofl::rofl:

Rest assured, I have no interest in building a similar interest. It’s just that communication has been so inefficient that I can only squeeze the design of iCKB from your reply bit by bit. So I figured I wanted to do it a different way: I can spend a couple of hours piecing an alternate design. Maybe you have other concerns putting together a complete design, I get it, but by presenting an alternative design, it will be easier for me to learn how iCKB plans to design things. And it really seems my goal is achieved here.

This requires a global state update to account every deposit.

Global state contention is a known artifact in Cobuild OTX. All OTX processors, regardless of the actual use case of OTXs, will have a level of shared state contention involved. There are 2 points here:

  • At user level, constructing OTXs is free from shared state contention. Anyone is free to construct as many OTXs as he / she wishes without worrying about shared state. And that is really the point.
  • OTX processors will indeed compete for shared cells, but OTX processors are really programs, in case of contention, they can simply rebuild a new CKB tx containing updated shared cells. There is no real issue here. In addition, it has been in the discussion that miners are particularly suitable to act as OTX processors as well, since they will know the details of the blocks they are producing, and are free from shared cell contentions.

Also crucially increases illiquidity as increase the capital requirement for liquidity providers

This is really just a balance between the the liquidity required for LP, and the minimum CKBytes required by a deposit batch so as Nervos DAO’s 64 cell limit ceases to become a problem.To me it is nothing related to cobuild, but one parameter we can tune for better usability. You can use a smaller limit for batch deposit, in exchange for limited number of deposits processed per CKB tx(but you always have the option to use more CKB txes, again it’s just how you want to balance things).

This protocol is vulnerable to DoS. Let’s assume an attacker has a big enough capital (in the order of GB), he can withdraw from small deposits and redeposit in deposits as big as the entirety of his capital. Doing this for all xDAOCKB controlled deposits will hamper the fruition of xDAOCKB as it’s impossible to withdraw partially from a NervosDAO deposit.

To avoid this form of DoS it is necessary to put a cap on the deposit size.

This is also the reason why iCKB poses a soft limit on the deposit size. Exceeding this limit can cause up to 10% loss. So this attack is prevented by slashing the attacker capital.

Yes by all means, a limit on deposit size can be used indeed. And this is why I listed the assumptions above, we can add a new assumption here like a cap on deposit size, and it’s easy to adjust the design.

In your example every time the order is partially fulfilled, the user must sign another OTX.

Actually both is doable. A user just signs an OTX saying that he / she wants to sell, say 2000 xDAOCKB, an LP in the system can also issue an OTX saying I will pay for 1500 xDAOCKB for 1500.1 CKB, and yet another LP issues a second OTX saying he / she will pay 500 xDAOCKB for 500.05 CKB. An OTX processor can then pack all 3 OTXs together achieving the same goal. And that is really how Cobulid OTX is designed in mind.

Now I’m thinking, since your limit order interpretation works really well for small amounts, I could make the user interact directly with the protocol until their remaining assets to convert are less than one standard deposit (or few standard deposits). At that point I could use your limit order approach (without partial fulfillment). This approach has a user experience similar to your design after the necessary Dos mitigation measures.

That is the goal. My hope is that by comparing designs (well this has to be done in your mind) and absorbing ideas, an improved one with cobuild in mind can be achieved.

Basically I now get that iCKB uses a third solution, which ties the exchange rate to CKB’s secondary issuance rate, but I feel like it is just a third solution to solving the different deposit time issue. It is possible to combine the cobuild-OTX-based design here with iCKB’s solution to exchange rate. To me that will be an even better solution. And if I’m designing iCKB, I will personally go with this way.

2 Likes

Hey @xxuejie, hope everything is going well, I noticed that you are reviewing xUDT RFC PR and fighting to improve its readability. That’s very appreciated, keep it up!! :muscle:

This is an advanced POC, I was wondering how much time could it take to have a production ready CoBuild Rust Library?

CoBuild OTX Collector, Missing

In these days I started to appreciate more and more CoBuild OTX, so I started looking how to integrate CoBuild OTX-style limit orders into the iCKB flow. Then again, I realized that a key Service is missing for integrating CoBuild OTX into iCKB, let’s call it OTX Collector.

A CoBuild OTX Collector would be defined by the following functionalities:

  • Buffer the received messages, while being resistant to DoS.

  • Check that these messages can be parsed as CoBuild OTX, so shielding aggregators against malicious XSS messages.

  • Organize the CoBuild OTX in Topics.

  • As noted by @xxuejie, avoid validating the received CoBuild OTX as it’s a hard problem. (Validation could be achieved by the specific aggregator retrying enough times aggregated transactions, excluding the non validating OTX little by little.)

  • Make publicly available for aggregators the received CoBuild OTX.

  • Possibly be organized as a P2P network for high availability and for resisting DDoS attacks.

No Cobuild OTX Collector is currently available and I can’t afford to create, deploy and maintain it as I don’t have the required mental power.

iCKB Staggered Adoption of CoBuild

At this point, while in my opinion CoBuild OTX is solid, its integration in iCKB is not 100% workable right away. That’s why we are taking a staggered approach for iCKB. In V1:

  1. I’ll complete the proposal update the definition for all scripts in a way that is both L1 Classic and CoBuild OTX compatible. Then I’ll kindly ask @xxuejie for a review.

  2. I’ll implement these script in L1 Classic and, if CoBuild Rust Libraries are production ready, possibly CoBuild (OTX).

  3. Deploy them on Testnet.

  4. Finishing implementing the DApp as L1 Classic.

  5. Launch on Mainnet.

When CoBuild back-end and front-end libraries are production ready, in V2:

  1. Update the scripts to validate Cobuild (OTX) messages and re-deploy, if not already fully CoBuild OTX.

  2. Update the DApp to support CoBuild and possibly CoBuild OTX-style limit orders if a CoBuild OTX Collector-alike service is available by then.

1 Like

This is an advanced POC, I was wondering how much time could it take to have a production ready CoBuild Rust Library?

This is in fact the CoBuild Rust library that is expected to be used in Rust contracts. The repo name is slightly misleading. See ckb-transaction-cobuild-poc/contracts at main · cryptape/ckb-transaction-cobuild-poc · GitHub for actual contracts using the library.

Then again, I realized that a key Service is missing for integrating CoBuild OTX into iCKB

Yes I do agree that OTX still requires an off-chain part so as to function propertly. Unfortunately this is still in planning phase, I will see how I can help here.

avoid validating the received CoBuild OTX as it’s a hard problem.

Clarification: I never say that we should not validate a received OTX, I just say it is a hard problem to validate any generic OTX. It is more feasible for an OTX collector/processor/agent(depending what wording you use here, they really all refer to the off-chain module for collecting and packing OTXs into L1 TXs) to deal with a certain type of OTX, fulfilling a certain type of app on CKB. In the process of working with OTXs, a collector of course can perform certain validation work.

And the validation rule, at least in the beginning, does not have to be very complicated. For a similar work: Swap Transfer Rules | Atomicals Guidebook this can be an example here. We can have an OTX collector to only collect OTXs that have a certain structure(e.g., the first input cell should have no type script, the second output cell should use a particular type script, etc.). And it could already provide enough logic to piece together an OTX collector for, say, iCKB. Gradually as more requirements come we can then add more power to the collector, but my point is, you don’t really need a beefy, full featured OTX collector to get started.

And the OTX collector can also started as a single node without any P2P functionality, it could also say like I will keep at most 20000 OTXs, any more OTXs than that will be discarded. Such an OTX collector will already provide necessary functionality, while being somewhat DDoS resistant. Of course a P2P network will be nice to have later, but to get started, we don’t really need something huge.

In an ideal world where OTXs are fully utilized on CKB, we can have more sophisticated OTX collectors/processors/agents with advanced P2P network as well as all other fancy features, but my point really is we can do this in incremental steps, we don’t need to have everything ready at day one.

3 Likes

That’s good to hear, I didn’t really like the idea of having to update the contracts right after deploy! Would it be possible to export ckb-transaction-cobuild as a crate?

(In a way that it can be installed with something like cargo add ckb-transaction-cobuild without specifying repo and folder)

Perfect!! This is super important for CoBuild OTX adoption :+1::+1:

This makes more sense!! Also since the Collector is already at this level, maybe it’s doable to also filter out OTX that are already spent by keeping an eye on L1 spent cells.

2 Likes

I will leave this question to @quake and @xjd for more insights.

1 Like

Yes, a proper OTX collector/processor can and should indeed do this.

1 Like

Yes, this PR is in progress. We will publish it soon when it’s ready.

2 Likes

Took a while, but I was able to bring the iCKB proposal up to date with designs are both L1 Classic and L1 CoBuild OTX compatible. CoBuild Actions are missing at the moment.

Highlights

Deposit receipt is now positional independent and it allows for multiple receipts in output.

Note: The receipt only memorizes deposit amount and quantity, but it doesn’t specify which NervosDAO deposits that is accounting. A possible future OTX design may be: the user unlocks the necessary funds & create a receipt in output, while the aggregator transforms these funds into the intended NervosDAO deposits. Then again, currently I see no benefit in this possible OTX design.

Withdrawal requests are now handled by the script Owned Owner. While iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script which split the ownership of the withdrawal request into two cells:

  1. The owned cell with this script as lock and NervosDAO withdrawal request as type.
  2. The owner cell with this script as type and a lock that identifies the user.

To unlock both cells the input must contain both the owned cell and the owner cell.

Similarly to Owned Owner, a Limit Order is now a two cell design:

  1. The limit order cell with this script as lock and UDT as type.
  2. The master cell with this script as type and a lock that identifies the user.

While the limit order cell can be updated following its logic, to effectively consume both cells the input must contain both the owned cell and the owner cell.

Follows the updated proposal sections.

Deposit

In NervosDAO, a deposit is a single transaction in which a CKB holder locks his CKB in exchange for a NervosDAO receipt of that specific deposit.

In the proposed protocol, a deposit is the process in which a CKB holder locks his CKB in exchange for iCKB tokens.

This process can’t happen in a single transaction due to a Nervos L1 technical choice: as seen from the previous section, to mint the iCKB equivalent for a deposit the protocol needs to access the current accumulated rate, which is defined in the deposit’s block header, then again Nervos L1 is off-chain deterministic, so the current block header cannot be accessed while validating a transaction.

Thus the protocol is forced to split a deposit in two phases:

  1. In the first phase, the CKB holder locks his CKB in exchange for a protocol receipt of the specific amount deposited.
  2. In the second phase, the deposit’s header block is available, so the protocol receipt can be transformed into iCKB tokens.

Deposit Phase 1

In this first phase the protocol:

  • Transforms input CKB into NervosDAO deposit cells locked by iCKB Logic Script, in short a deposit.
  • Awards to the user a protocol receipt of the deposits, effectively wrapping them.

Given the impossibility to access the header in this phase, it cannot exist a strict requirement on deposits iCKB-equivalent size. On the other hand, to achieve higher deposits fungibility and to prevent a certain form of DoS, the protocol needs to incentivize standard deposits.

In particular, deposits bigger than the standard deposit size are actively disincentivized: the user will receive only 90% of the iCKB amount exceeding a standard deposit. The remaining 10% is offered as a discount to whoever is willing to withdraw from the oversized deposits.

On the other side, deposits smaller than the standard deposit size are intrinsically disincentivized by L1 dynamics. As deposits gets smaller they incur a bigger penalty in form of unaccounted occupied capacity, up to 10% of the deposit. This translates to a minimum deposit of 820 CKB:

  • 82 CKB of fixed occupied capacity, used for state rent of the deposit cell with iCKB Logic Script.
  • 738 CKB of minimum unoccupied capacity, to be accounted by the receipt and later on converted in iCKB.

Taking in consideration these incentives, at least 90% of the deposit amount is always converted, of course the optimal strategy for a depositor is to split his CKB into standard deposits.

Since having a separate receipt per deposit cell would be capital inefficient, the protocol allows to account multiple deposit with a single receipt. An iCKB receipt accounts for a group of deposits with the same size, it just contains the single deposit unoccupied CKB capacity and the quantity of the accounted deposits. In a transaction output there can be many receipt cells and possibly more than one receipt for the same deposit size.

In a receipt cell data:

  • The first 4 bytes, currently zeroed, are reserved as unionID in case of future iCKB updates.
  • The single deposit unoccupied CKB capacity is stored in 6 bytes, this poses an hard-cap of ~2.8M CKB per single deposit. This hard-cap prevents a certain form of DoS, while still leaving enough slack for the standard deposit CKB size to grow for well over a hundred of years.
  • The quantity of deposits is stored in 2 bytes, this poses an hard-cap of 65535 deposits per receipt. On the other side for simplicity a transaction containing NervosDAO script is currently limited to 64 output cells so that processing is simplified. This limitation may be relaxed later on in a future NervosDAO script update.

Summing up, in the first deposit phase, these rules must be followed:

  • A deposit is defined as Nervos DAO deposit with an iCKB Logic Lock {CodeHash: iCKB Logic Type ID, HashType: Type, Args: Empty}.
  • A group of same size deposits must be accounted by a receipt.
  • A receipt is defined as a cell with iCKB Logic Type {CodeHash: iCKB Logic Type ID, HashType: Type, Args: Empty}, the first 12 bytes of cell data are reserved for:
    • union_id all zero, it’s reserved for future updates to data encoding (4 bytes)
    • receipt_count keeps track of the quantity of deposits (2 bytes)
    • receipt_amount keeps track of the single deposit unoccupied capacity (6 bytes)
  • No more than 64 output cells are allowed, due to the current NervosDAO restriction.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script and Nervos DAO Script.

Receipt data molecule encoding:

array Uint16 <byte; 2>;
array Uint48 <byte; 6>;

struct ReceiptDataV0 {
    receipt_count: Uint16,
    receipt_amount: Uint48,
}

union ReceiptData {
    ReceiptDataV0,
}

Example of deposit phase 1:

CellDeps:
    - iCKB Dep Group cell
    - ...
Inputs:
    - ...
Outputs:
    - Nervos DAO deposit cell with iCKB Logic Lock:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
    - ...
    - Receipt:
        Data: ReceiptData
            union_id: All zero, reserved for future updates (4 bytes)
            receipt_count: Quantity of deposits (2 bytes)
            receipt_amount: Single deposit unoccupied capacity (6 bytes)
        Type:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user

Deposit Phase 2

A receipt accrues interests and it can be used to withdraw, but it’s not liquid nor transferrable.

The second phase of the deposit transforms a receipt into its equivalent amount of iCKB tokens, which in turn is both liquid and transferrable. This conversion is now possible thanks to the header of the deposit block now available.

As seen in iCKB/CKB Exchange Rate Calculation for each receipt the equivalent amount of iCKB is well defined. The only difference being the incentivization: oversized receipts are subject to a 10% fee on the amount exceeding a standard deposit.

In the second deposit phase, these rules must be followed:

  • The iCKB value of a receipt is calculated as
iCKB_value(unoccupied_capacity, AR_m) {
    let s = unoccupied_capacity * AR_0 / AR_m;
    if s > standard_deposit_size {
        s = s - (s - standard_deposit_size) / 10
    }
    return s;
}

receipt_iCKB_value(receipt_count, receipt_amount, AR_m) {
    return receipt_count * iCKB_value(receipt_amount, AR_m);
}
  • The total iCKB value of input tokens and input receipts must be bigger or equal to the total iCKB value of output tokens.
  • iCKB xUDT flags are set to 0x80000000 to enable xUDT owner mode by input type.
  • HeaderDeps must contain the transaction hash of the deposit block for each receipt.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of deposit phase 2:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Receipt:
        Data: ReceiptData
        Type:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Type ID, 0x80000000]
        Lock: A lock that identifies the user

Withdrawal

In NervosDAO time is slotted in batches of 180 epochs depending on the initial deposit timing, a withdrawal is split in two steps:

  1. In the first transaction the user requests the withdrawal.
  2. In the second transaction the user withdraws the deposit plus interests. Must be after the end of the 180 epoch batch in which the first transaction happened.

As seen in NervosDAO RFC Calculation section the actual withdrawn CKB amount depends on the deposit block and on the withdrawal request block.

The proposed protocol instead proceed by un-wrapping iCKB tokens into NervosDAO withdrawal cells:

  1. In the first transaction the user:
    • Requests the withdrawal from some protocol controlled deposits.
    • Respectfully to that quantity, burns a bigger or equal amount of iCKB tokens and/or receipts.
  2. The second transaction is a Nervos DAO second withdrawal step.

As seen in iCKB/CKB Exchange Rate Calculation for each deposit and receipt the equivalent amount of iCKB is well defined. The only difference being the incentivization: requesting the withdrawal from an oversized deposit is incentivized by a 10% discount on the amount exceeding a standard deposit.

An additional NervosDAO constraint is that if deposit lock and withdrawal request lock differs, as in iCKB case, then NervosDAO requires the deposit lock and withdrawal request lock to have the same size. A non solution would be to use a lock with zero padded args in the deposit, then again different user locks would have different sizes, so it wouldn’t solve the problem at hand. While iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script.

Summing up, when withdrawing, these rules must be followed:

  • The iCKB value of receipts and deposits is calculated as
iCKB_value(unoccupied_capacity, AR_m) {
    let s = unoccupied_capacity * AR_0 / AR_m;
    if s > standard_deposit_size {
        s = s - (s - standard_deposit_size) / 10
    }
    return s;
}

receipt_iCKB_value(receipt_count, receipt_amount, AR_m) {
    return receipt_count * iCKB_value(receipt_amount, AR_m);
}

deposit_iCKB_value(capacity, occupied_capacity, AR_m) {
    return iCKB_value(capacity - occupied_capacity, AR_m);
}
  • The total iCKB value of input tokens and input receipts must be bigger or equal to the total iCKB value of output tokens and input deposits, the deposits being withdrawn.
  • Withdrawal Request lock must have zero args length and no information stored in the data cell.
  • No more than 64 output cells are allowed, due to the current NervosDAO restriction.
  • HeaderDeps must contain the transaction hash of the deposit block for each deposit being used to withdraw and each receipt cashed out.
  • CellDeps must contain iCKB Dep Group comprising of: iCKB Logic Script, Standard xUDT Script and Nervos DAO Script.

Example of withdrawal phase 1:

CellDeps:
    - iCKB Dep Group cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Nervos DAO deposit cell with iCKB Logic Script:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Type ID, 0x80000000]
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's including block number
        Type: Nervos DAO
        Lock: A lock that identifies the user
    - ...

Ancillary Scripts

The iCKB protocol without additional scripts would be difficult to use, this section describes the L1 scripts that have been developed to address iCKB user needs.

These scripts offers solutions to specific lock needs, while supporting all users locks. The natural choice to prove user ownership would be to use the delegated signature validation pattern, then again given the incumbent OTX era this pattern has some specific OTX pitfalls. Let’s assume that:

  • The user lock is OTX signature based.
  • The user unlocks some cells with signature in an OTX transaction, first OTX.

An attacker could do the following:

  • Attacker includes all other cells locked with delegated user signature in a second OTX.
  • Attacker packs this second OTX together with the first OTX in the same transaction.
  • These second cells will unlock thanks to delegated signature validation.
  • Attacker gains control of this second group of cells.

This is the reason why these scripts are instead designed around a similar but safer pattern:

  • A single transaction mint both a controlled cell and controller cell.
  • In a transaction there may be multiple controlled cell and controller cell.
  • At minting time one of the cells reference the other one using the signed relative index distance between each other.
  • The controlled cell satisfies specific user needs.
  • The controlled cell uses the new script as lock
  • The controlled cell may have an updating method using the new script logic.
  • The controller cell has ownership of the controlled cell.
  • The controller cell uses the new script as type.
  • The controller cell has a lock that identifies the user.
  • Melting both cells in the same transaction is the only way to consume both cells.

Owned Owner Script

While iCKB Logic script is independent to the withdrawal request lock choice, this lock has some pretty restrictive constraints, as no information can be stored in its lock args nor in its cell data. For this reason has been developed Owned Owner Script. In a transaction there may be multiple owned cells and owner cells. This script lifecycle consists of two transactions: Mint and Melt.

Owner data molecule encoding:

array Int32 <byte; 4>;

struct OwnerOwnedData {
    owned_distance : Int32,
}

Mint Owned Owner

In the Mint transaction, the output contains:

  1. The owned cell with this script as lock.
  2. The owner cell with this script as type and a lock that identifies the user. This cell memorizes in data the signed relative index distance between the owned cell and itself as a signed 32 bit integer encoded in little-endian.

Validation rule: owned_index == owner_index + signed_distance

Example of withdrawal phase 1 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - Owned Owner data cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Nervos DAO deposit cell with iCKB Logic Script:
        Data: 8 bytes filled with zeros
        Type: Nervos DAO
        Lock:
            CodeHash: iCKB Logic Type ID
            HashType: Type
            Args: Empty
    - Token:
        Data: amount (16 bytes)
        Type:
            CodeHash: Standard xUDT Script
            HashType: Data1
            Args: [iCKB Logic Type ID, 0x80000000]
        Lock: A lock that identifies the user
    - ...
Outputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's including block number
        Type: Nervos DAO
        Lock: Owned role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
    - Owner cell:
        Data: Signed distance from Owned cell (4 bytes)
        Type: Owner role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
    - ...

Melt Owned Owner

In the Melt transaction, the input contains both the owned cell and the owner cell. If one of the two is missing the script does’t validate.

Example of withdrawal phase 2 using Owned Owner:

CellDeps:
    - iCKB Dep Group cell
    - Owned Owner data cell
    - ...
HeaderDeps: 
    - Deposit block
    - ...
Inputs:
    - Nervos DAO phase 1 withdrawal cell:
        Data: Deposit cell's including block number
        Type: Nervos DAO
        Lock: Owned role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
    - Owner cell:
        Data: Signed distance from Owned cell (4 bytes)
        Type: Owner role
            CodeHash: Owned Owner Hash
            HashType: Data1
            Args: Empty
        Lock: A lock that identifies the user
    - ...
Outputs:
    - ...

Limit Order Script

Interacting directly with the iCKB protocol has some limitations:

  • In transactions containing NervosDAO script, no more than 64 output cells are allowed.
  • iCKB Logic discourages deposits bigger or smaller than the standard deposit size.
  • There may be a mismatch between the amount the user wants to withdraw and the deposits available in the iCKB pool.
  • NervosDAO doesn’t allow to partially withdraw from a deposit.
  • There is no easy way to merge multiple user intentions within a single deposit or withdrawal.

To abstract over NervosDAO and iCKB protocol limitations, it has been created a lock that implements limit order logic, abstracting user intentions, and that anyone can match partially or fulfill completely, similarly to an ACP lock. This lock aims to be compatible with all types that follows the sUDT convention of storing the amount in the first 16 bytes of cell data, at the moment sUDT and xUDT. In a transaction there may be multiple orders cells. This script lifecycle consists of three kind of transactions: Mint, Match and Melt.

Limit Order Args molecule encoding:

array Hash <byte; 32>;
array Uint64 <byte; 8>;
array Uint32 <byte; 4>;
array Int32 <byte; 4>;

struct OutPoint {
    tx_hash : Hash,
    index : Uint32,
}

struct OrderInfo {
    is_udt_to _ckb: byte,
    ckb_multiplier: Uint64,
    udt_multiplier: Uint64,
    log_min_match: byte,
}

struct MintOrderArgs {
    orderInfo: OrderInfo,
    master_distance: Int32,
}

struct MatchOrderArgs {
    order_info: OrderInfo,
    master_outpoint: OutPoint,
}

struct FulfillOrderArgs {
    master_outpoint: OutPoint,
}

union OrderArgs {
    MintOrderArgs,
    MatchOrderArgs,
    FulfillOrderArgs,
}

Mint Limit Order

In the Mint transaction, the output contains:

  1. The limit order cell itself with an UDT as type and this script as lock. This lock args memorizes:

    • is_udt_to _ckb expresses the order direction.
    • ckb_multiplier and udt_multiplier expresses the order exchange ratio.
    • log_min_match expresses the logarithm in base 2 of the minimum partial match of the wanted asset.
    • master_distance expresses the signed relative index distance between this cell and the master cell.
  2. The master cell with this script as type and a lock that identifies the user. This cell controls the limit order cell.

Validation rules:

  • orders_index + master_distance == master_index
  • Order cell data length must be at least 16 bytes.

Example of Limit Order mint:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - ...
Outputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MintOrderArgs variant of OrderArgs
    - Master cell:
        Data: ...
        Lock: Master role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user

Match and Fulfill Limit Order

In Match and Fulfill transactions the allowed input limit OrderArgs variants are MintOrderArgs and MatchOrdersArgs. While the allowed output limit OrderArgs variants are MatchOrdersArgs and FulfillOrdersArgs.

Validation rules:

  • in_ckb * ckb_multiplier + in_udt * udt_multiplier <= out_ckb * ckb_multiplier + out_udt * udt_multiplier
  • in_wanted_asset + 2^log_min_match <= out_wanted_asset
  • excluding the first 16 bytes that encode the amount, the rest of data bytes must be equal between input and output order.
  • FulfillOrdersArgs is not allowed as input order.
  • MintOrderArgs is not allowed as output order.

Example of Limit Order Match:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MintOrderArgs variant of OrderArgs
Outputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: MatchOrderArgs variant of OrderArgs

Melt Limit Order

In the Melt transaction, the input contains both the order cell and the master cell. If one of the two is missing the script does’t validate. Any limit OrderArgs variant is allowed as input.

Example of Limit Order melt:

CellDeps:
    - Limit Order data cell
    - ...
Inputs:
    - Limit Order cell:
        Data: [amount (16 bytes), ...]
        Type: UDT
        Lock: Limit Order role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: FulfillOrdersArgs variant of OrderArgs
    - Master cell:
        Data: ...
        Lock: Master role
            CodeHash: Limit Order Type ID
            HashType: Type
            Args: Empty
        Lock: A lock that identifies the user
Outputs:
    - ...
4 Likes

Thanks for putting together a complete design! There are parts where I would personally design differently, but that is really an irrelevant question. What matters here, is that a complete design allows us to derive certain assumptions that are mis-communicated before:

  • The iCKB issued from each deposit, is tied to the commit block header of the deposited cell, hence 2-phase deposit is required(I must say I missed this part earlier)
  • The core of iCKB is only about issuing iCKB together from a DAO deposit, and then requiring burning iCKB when deposit cells are withdrawed. The limit order of iCKB for CKB, is really an add-on part, that can be done separately, possibly via a standalone DEX.

Those assumptions would indeed affect the design, and I agree with you now that a 2-phase deposit might be needed, and that OTX is not needed at least at the core part of iCKB.

Though I still have some questions, and if I were doing this design, I might do something different, I will include them here just as suggestions. Feel free to think about it and absorb/ignore/criticize them as you want.

Questions

OTX

I don’t really understand this part. A second OTX would require a separate signature signed by the user, so as to unlock cells from the original user. An attacker cannot just add a second OTX with the first OTX in the same transaction, hoping that the second OTX would succeed in validation. Each OTX will have clear boundaries, can you explain more on how this attack is performed?

What Is Owned Distance

This part really confuses me, what is owned distance here? To me Mint is just withdraw phase 1, and Melt is just withdraw phase 2.

And what is Owner cell used in Melt transaction?

Suggestions

Automating the conversion from Receipt to iCKB tokens

This is one part that I believe could use some optimization: right now a receipt cell uses a lock from the original user making the deposit, and the user himself/herself has to sign a different transaction to issue the iCKB tokens. But if you think about it, the included iCKB Logic Type ID type script of the receipt cell, already handles all verification work. So if I were designing the workflow, I would use an always-success lock as the lock of receipt cell created from deposit phase 1. Then a bot can handle the conversion from a receipt cell, to the actual iCKB tokens. Of course 2 additional points are needed:

  • ReceiptData will need to hold a lock script used as the lock of the final iCKB token cell
  • An incentive in the form of CKB is probably needed to be taken from the receipt cell so some bot would want to do the conversion for the user

A user can still manually do the conversion from a receipt cell to the actual iCKB tokens, but in most cases, bot will help take care of this process without any potential issues.

Simplifying scripts

I’m sure you might also want to clean this up when the actual development work started, still I want to point out some inconsistencies:

  • The DAO deposit step creates a DAO deposit cell using iCKB Logic Type ID as the lock
  • Withdrawal phase 1 consumes a DAO deposit cell using iCKB Logic Type ID as the lock, then creates a DAO withdrawing cell using Owned Owner Hash as the lock

To me, it might simply be simpler if DAO deposit cell simply use Owned Owner Hash as the lock script here. Which will result in a cleaner(to me) design:

  • iCKB Logic Type ID is only used as the type script of receipt cell, it validates the legit creation of receipt cell from DAO deposits into Owned Owner Hash, then validates the issuance of iCKB tokens from the consumption of receipt cell
  • Owned Owner Hash is only used as the lock script of DAO deposit/withdrawing cells, it validates the withdrawing of DAO deposits, together with the burning of enough iCKB tokens

You might want to revisit the names used here for the scripts, but to me this way we have a pair of scripts, each with clearly defined responsibility and a cleaner design. In general, I would not personally recommend using a single script both as lock and type. In theory it works, but in practice it would probably messes your head so chances are you might miss something.

And one more question here: I noticed iCKB Logic Type ID uses type as hash type, but Owned Owner Hash uses Data1 as hash type. What’s the rationale behind this? I would personally recommend to stick to one hash type choice across iCKB. And really don’t make any design that fixates on a particular hash type, this is really one field that should stay flexible for future changes.

Don’t Over Simplify Data Structures

I personally would not recommend using Uint16 and Uint48 in ReceiptData. I get it you want to have smaller cell data, but my past experience is that those excessive saving of bytes, will only incur more problems in toolings. If I were you, I would simply go with Uint64, even if you are only using a small fraction of Uint64. Yes contracts can be upgraded but it really is all those off-chain SDKs & toolings that worry me.

Limit Order Can Be Implemented In Different Ways

In this design here, Limit Order is just really a separate thing from iCKB. It really depends on the lock of the iCKB token cells to say what limit order design can be used here. You are essentially designing a small DEX here, the proposed solution works, we can also use other solutions like OTX, or any other solution. I would personally not consider this a part of iCKB, in fact I would not consider locking to a particular design. The choices here are really numerous, feel free to look at other DEX solutions in the ecosystem.

2 Likes

Hey @xxuejie, thank you for taking your time to read the updated proposal, I appreciate a lot :hugs:

If we assume that every script is CoBuild OTX, no such attack can occur.

Then again this assumption may fail in the following scenarios:

  1. In the future exists a second form of signature OTX with different rules and developer/user unwittingly mix and match CoBuild OTX and non-CoBuild OTX.

  2. The script who performs delegated signature validation is not CoBuild OTX aware and developer/user unwittingly uses a CoBuild OTX signature lock.

While 1 is what worries me in the ancillary scripts section, 2 is more realistic than one may expect. In the past to perform delegated signature validation a script would only need to check that the input contains a specific lock. Crucially sUDT and xUDT adopt this patter and they are not CoBuild OTX aware. Now, in the unlucky scenario that a developer/user chooses a CoBuild OTX signature lock as owner lock of his sparkling new UDT, then the attack can occur.

Let’s start by clarifying that Mint and Melt are context dependent. There are at least four different kinds of Mints:

  1. CKB → Receipt, so a Receipt mint (not named mint in the proposal)

  2. Receipt → iCKB xUDT, so a iCKB xUDT mint (just mentioned as verb in the proposal)

  3. An Owned Owner mint (as per section title)

  4. A Limit Order mint (as per section title)

Same goes with Melt. Maybe using instead synonyms would help, do you have suggestions for the synonyms?

My typo, sorry, owned_distance and signed_distance are the same thing. Let’s say the outputs of a TX/OTX are arranged like this:

  • index 0: Cell

  • index 1: Owned Cell

  • index 2: Cell

  • index 3: Cell

  • index 4: Owner Cell of 2 Owned Cell

  • index 5: Cell

  • index 6: Cell

The validation rule: owned_index == owner_index + signed_distance

In this case:

  • owned_index = 2

  • owner_index = 4

  • signed_distance = -2

What you propose is indeed doable and in the past I consider it, then again:

I’d like to point out that:

  • The old design adopted the same lock for deposits and withdrawal requests, this brought unnecessary complications, that’s why in this design I switched to xUDT.

  • Not inconsistent, intentional. A deposit cell is owned by the protocol, a withdrawal request cell is owned by a specific user. A user may use other locks too.

  • Splitting deposit lock and iCKB Logic Type ID is troublesome as it’s a circular dependency between the two scripts. So the deposit lock now has to memorize as Args the hash of the script of the other one. Using a script as both lock and type solves elegantly the issue.

Actually it’s one of the designs that I like most as it’s inherently safe and it avoids circular dependencies.

Agreed, to be honest I don’t like the name Owned Owner either, but it’s short and gives the idea. Do you have any suggestion?

Well spotted! Limit Order and iCKB Logic are meant to be upgradable, while Owned Owner Hash has a really simple function, so it is not meant to be upgradable. Then again yeah, I’m pretty neutral on this, I wouldn’t mind to switch to type for Owner Owned script :+1:

I agree, the non standard Uint48 may bring future tooling headaches to third parties, so it’s a good idea to change it to Uint64 :+1: (and limit the max within the script itself). Then again, any tooling that doesn’t support the standard Uint8, Uint16 or Uint32 has a long way to go.

Exactly!! :raised_hands:

Could you point me to existing alternatives?

Personally I think this is being over paranoid, since the issue is actually much bigger if we follow down this path:

CKB is a permissionless blockchain, meaning anyone is free to deploy any script on chain, and use it as the lock / type script of a cell. Such a script used, can really execute any logic as one wishes. This brings a question: in such a chaos world of CKB, how can we be sure a cell is secure?

We first define a set of conventions, then we develop scripts using this set of conventions, those scripts are then used as locks and types of cells. Finally, when a wallet / dapp generates a new transaction for user to sign, the wallet / dapp also follows this very convention. To ensure his / her cells are secure, a user is also required to ONLY use scripts that conform to the above conventions.

We can now reach a conclusion: it’s not that any CKB cell using any script is secure, only when everyone follows a clearly defined set of conventions, will everyone be sure that his / her cells are secure and can work in ways he / she wants. This is the KEY for building protocols on CKB.

We don’t have to look at cobuild OTX now, we can look at the sighash-all script included in genesis cell. This script does not define a rule of CKB consensus. CKB nodes will perfectly run without this script. Instead, it merely defines the following set of conventions:

  • A lock script guards the tx hash derived from current transaction
  • A lock script guards witnesses of its own script group
  • A lock script guards those witnesses that do not have matching input cells

This is a set of rules we defined to make a cell secure, but it really is only one set of all possible rules. It’s totally fine someone is building a different set of rules(for example, we can have a lock script that only limits and uses one witness) and it could also work on CKB. It’s just that we as a community choose to build more scripts / wallets / tools following conventions used by scripts in genesis cells.

And really, every wallet, every tool, every user has to recognize and conform to the above set of conventions, so as to build secure cells and transactions. Just like your example, if someone uses a different script using a different signature that do not conform to the above rules, we might have cells / transactions that are insecure, and this is really by design. It is not a risk. Developers / users have to work under a single set of conventions, so as to securely move cells on CKB.

Cobuild, with and without OTX, is also designed following the same philosophy: nothing we defined in cobuild is consensus rules, and theoretically one is free to either obey/disobey/partially-obey rules defined in the cobuild protocol. This is just what a permissionless blockchain really mean. But fundamentally, we can only be sure that cells / transactions will be secure when all the defined rules in the cobuild protocol are obeyed by scripts, wallets, tools and users. It’s just like any good law(some would want to argue all laws are bad but we will not go that far today): with defined responsibilities you can have rights, if you choose to misbehave, some rights might then be deprived from you.

So I would not worry now about a second form of OTX signature, if the newer form of OTX is compatible with the current one, they will be happily used together. If there are some conflicting parts, then they are not designed to be used together, anyone forcing them together will risk their cells / transactions being insecure, and this is totally expected in the CKB world.

Cobuild is designed with a lot of considerations in backward compatibility, when I say this sentence repeatedly, I really mean it. Yes current sUDT and xUDT are not cobuild OTX compatible, but that does not prevent you from using current versions of sUDT and xUDT in a cobuild OTX. sUDT and xUDT will continue to validate its rules in the full CKB L1 transaction. No validation rules will change here. In other words, you can have a cobuild OTX that receives 1000 sUDT and it is perfectly fine. It is just required that in the final assembled L1 CKB transaction, there will be other cells providing this 1000 sUDT.

There is work needed to make sUDT / xUDT script cobuild-compatible, but we won’t touch current validation rules. I believe there will only be an additional logic, that validates the cobuild action for sUDT / xUDT in each individual cobuild OTX. The core validation logic of sUDT / xUDT, stays the same, and will still be performed in the full L1 transaction.

So I don’t really see a scenario for attacks. Feel free to follow-up if you think otherwise.

Personally I don’t get the reasoning here. It’s just one step depositing and in the same time creating a receipt. I do feel like we are manually adding a second step for no apparent gain.

And see above discussion, I think you don’t need to worry about script with slight different rules. Yes a user might use any different locks but really there is just one type script controlled by iCKB which is considered to be valid receipt.

I guess this is another thing that fundamentally comes down to personal preference. I do respect that you have your own likes and dislikes, but do want to point out one inconvenience: right now deposit cell and withdrawing cell must use lock of the same size, in your current design, iCKB Logic Type ID has no script args, but Owned Owner Hash do have script args so obviously they will have different script size. If I were doing this I would use the same script as lock for deposit cell and withdrawing cell, then I can always read the cell data to tell the different between 2 kinds of cells. But in the end the decision is yours.

Again this is depending on personal viewpoint, I will just point it out and leave the decision to you.

Personally I don’t really see the circular dependency, care to weigh in?

The only thing I would recommend, is sticking to one decision across your app. Yes Owned Owner Hash seems simple for now, but you might never now one year from now.

One more thing: I will need to read more before I can comment on the distance part, seems pretty odd to me.

2 Likes

I don’t mind being over-paranoid if it can even marginally avoid issues down the line. That’s also the reason why instead of rushing to complete iCKB and moving on, I’m here kindly asking your valued opinion :pray:

You know, a couple years ago, when I first heard about OTX I started worrying about how an sUDT OTX Owner Lock would be able to handle it safely. I’m reassured that (while this particular about UDT was not mentioned in the proposal) you have a plan for this! :+1::+1:

As for the technical implementation, if I have to guess, you’ll require for UDT owners at minting time to include the additional CoBuild OTX logic. Failing to do so may render the CoBuild OTX unsecure, but it will be their fault.

Same goes for any script that cannot be upgraded and that currently uses delegated validation, right?

Actually I have a question about Omnilock delegated signature handling. Maybe there was some misunderstanding, but from this Omnilock CoBuild OTX issue seems that only a single cell with matching signature script is required in the the fully assembled Transaction to validate all OTX using that delegated signature validation. Please, could you tell me how the attack proposed in the issue is handled?

I mean, it’s a valid idea, but this additional logic don’t have to reside in the main iCKB Logic, it can reside in a separate Lock (instead of Always Successful).

Yup yup, I considered this and I thought: Well, we can just deploy a V1 script, without touching V0 right away and eventually consuming the v0 cell when there are no on-chain cells referencing it. Then again, as you suggested, using a reference by Type ID is just more straight forward and it doesn’t compromise more from the security standpoint! :+1::+1:

Owned Owner has zero len args in the lock of the withdrawal request, otherwise as you pointed out, not only it would be inconvenient, it would not validate at all. As documented in the proposal, the trick is that it stores User lock information in a secondary cell.

Another completely different way to go around the zero length args issue is to create a brand new script that does exactly two things in its code:

  • Stores/hardcodes a particular user public key.

  • Invokes/delegates the verification (dynlib, exec or spawn).

Generally we can design a signature-based lock in a way that:

  • It hardcodes the information about the user public key in the script code itself (so every User has to first deploy this script hardcoding a public key that he controls)

  • It minimizes the code state rent by relying on already deployed library cells for signature validation logic.

  • It can be referenced in a lock with empty args, so it can be used instead of the Owned Owner Script.

  • It could be referenced by Type ID, so the code of the script itself could be updated. (This open interesting possibilities for automated Bots that are otherwise unable to store securely their private keys. This way they can use a session based private key)

Given all this reasoning, we could keep the zero length args, drop Owned Owner Lock and use this class of scripts, which also work nicely with automated Bots. In your opinion, would this improve the overall iCKB design?

Yes yes, of course!! :hugs: In iCKB protocol we have three entities: iCKB xUDT, Receipt Script and Deposit Script. In particular:

  • iCKB xUDT depends on Receipt Logic (by using xUDT args, ok)

  • Receipt Script depends on iCKB xUDT Hash (!) and Deposit Script Hash (!)

  • Deposit Script depends on iCKB xUDT Hash (!) and Receipt Script Hash (!)

Considerations:

  1. xUDT hash is fixed, so its hash can be hardcoded into the scripts.

  2. Given xUDT hash, if we know Receipt Script hash, then we know iCKB xUDT hash.

So it boils down to the following circular dependency:

  • Receipt Script depends on Deposit Script Hash (!)

  • Deposit Script depends on Receipt Script Hash (!)

We can break this circular dependency by either:

  1. Using the proposed design (so a single script used both as lock and type).

  2. Memorizing one script hash as args (so same approach as xUDT).

  3. Deploy both scripts as type ID and update them until they reference each other. (This option didn’t existed when I first considered this design as then I was fully committed to reference by data).

  4. Other more convoluted ways like using external type ID cells with this data…

If I understand correctly, the design you propose follows 2, right? Could you explain again the advantages it has over 1?

If it’s about fixing the size of withdrawal receipt args, even with 1 we could just pad the deposit lock args with zeros…

Which brings the question, do we know the exact size of the withdrawal request lock args length?

Not really, but we could fix it to a reasonable non-zero value, just we must keep in mind that:

  • We are basically wasting some CKB for state rent in Deposits (around 20%-30% increase in state rent)

  • We are giving a preferential treatment to some user locks respectfully to others (still different from the protocol being tied up to a singular external lock)

Personally I don’t consider either of them deal breakers, so let’s examine this path!

For example, we could target base Omnilock args length, so args length could be 21 bytes. Omnilock is a nice choice as it offers both direct signature unlocking and delegated signature unlocking (in case both user wallet does not support directly Omnilock and user wallet lock args length is not 21). Also it would be CoBuild ready for free.

Given all this reasoning, we could pad deposit args with 21 bytes, so that withdrawal requests can directly use Omnilock. In your opinion, would this improve the overall iCKB design?