Design of Approve Cell

Background

In the development of SUDT, we have encountered a problem that is closely related to the user experience which is how to receive a SUDT when there is no ACP cell has been created for that SUDT?

One solution is that the receiver needs to create an ACP cell (anyone-can-pay cell) for the SUDT, and the receiver cannot actually know in advance that someone is going to send a new SUDT to him/her, for now the receiver can only be notified via a off-chain notification that an ACP cell is needed. The other solution is that the sender needs to give the receiver an ACP cell of the SUDT when sending the SUDT. These two solutions will leads to the problem that the receiver cannot receive the token conveniently or the sender needs to pay additional costs.

To solve this problem, we design an Approve Cell approach. When the sender sends a SUDT to a receiver who does not have an ACP cell for the SUDT, it can transfer the SUDT to an Approve Cell and then wait for the receiver to claim it. The receiver can get transfer information from the chain and then decide whether to create an ACP cell to receive the asset, so that the receiver can receive the SUDT without creating the ACP cell in advance and the sender don’t need to pay the additional costs.

Solutions

The sender transfers the SUDT that needs to be transferred to an Approve Cell, adds the sender’s and receiver’s information to the Approve Cell’s lock args, and the receiver can follow the rules to unlock it once read the relevant authorization information on the chain.

// Structure of Approve Cell 

capacity: uint128 // 8 bytes
lock script: 
	code_hash: <approve_cell_lock_script>  // 32 bytes
	hash_type: type // 1 byte
	args: <receiver_lock_hash>, <sender_lock_hash>  // 20 bytes + 20 bytes
type script: <sudt_type_script>  // 65 bytes
data:{
	sudt_amount  // 16 bytes
}

// Total: 162 bytes
  • Approve Cell has a approve period (restrict the sender from undoing the Approve Cell within the approve period, this would written in the approve_cell_lock_script).

Unlock Logic of Approve Cell

Approve Cell’s lock script has two methods of unlocking:

  • Receiver claimed. The receiver unlocks the Approve Cell according to the rules of the Approve Cell, which can be separated into two ways according to the signature rules.

    • The receiver provides a lock args and uses the default secp256k1_blake160 as lock code_hash to assemble and sign the lock script, which is identical to the receiver_lock_hash in the Approve Cell.
    • The receiver directly provides a input cell that is identical to the receiver_lock_hash and signs it.
  • Sender withdrew. If the Approve Cell created by the sender has been on the chain for longer than the approve period and has not been claimed by the receiver, the sender can withdraw by providing a cell identical to sender_lock_hash as a input cell and signing it to withdraw the Approve Cell.

Examples of transaction structures

Create an Approve Cell

When the sender sends a SUDT to a receiver who does not have this SUDT’s ACP Cell, an Approve Cell will be created.

Inputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data: 
			sudt_amount: 1000 UDT

	CKB Normal Cell:
		Capacity: 1000 CKBytes
		Lock: <sender_lock_script>

Outputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args:  <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT  // approve amount of UDT

	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 900 UDT

	CKB Normal Cell:
		Capacity: (838-fee) CKBytes
		Lock: <sender_lock_script>

Witnesses
	<valid signature for sender_sudt_cell_lock_hash>
	<valid signature for sender_ckb_normal_cell_lock_hash>

Approve Cell Unlock Logic 1: Receiver claimed

The receiver uses the official secp256k1_blake160 lock to claim (suitable for short CKB address)

The receiver provides the lock args and assembles the lock script with the default secp256k1_blake160 as the code_hash and signs it. The assembled lock script is the same as the receiver_lock_hash in the Approve Cell.

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	Anyone SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <anyone_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 200 UDT

Outputs:
	Anyone SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <anyone_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 300 UDT

	CKB Normal Cell:
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<valid signature for secp256k1_blake160 + receiver lock script>  // need a identifier, marked as receiver
	<valid signature for anyone_lock_script>

The receiver uses other lock script to claim

The receiver directly provides a cell identical to receiver_lock_hash in the Approve Cell as an input cell and signs it.

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <receiver_lock_script>  // equals to the receiver_lock_hash in the args part of Approve Cell
		Type: <sudt_type_script>
		Data:
			sudt_amount: 200 UDT

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <receiver_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 300 UDT

	CKB Normal Cell:  // send CKB back to the sender
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for receiver_lock_hash> need a identifier, marked as receiver

Approve Cell Unlock Logic 2: Sender Withdrew

The sender withdraws the Approve Cell if the cell created by the sender has been on chain for longer than the approval period and has not been claimed by the receiver.

When withdrawing, the sender needs to add a lock script to the input cells that is identical to the sender_lock_hash in the Approve Cell lock args, and depending on the different situations in which the user has different cells, there are various ways to assemble the transaction, and in order to facilitate the front end developer to filter the appropriate cell, we can give priority to the following two situations:

  • If the sender has this SUDT cell (we assume all SUDT cells using the ACP lock), it will be used as an input cell for signing.

  • If the sender does not have this SUDT cell but has a CKB Normal Cell whose lock script corresponds to the sender_lock_hash in Approve Cell, it will be used as an input cell for signing.

Sender has this SUDT cell

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args:  <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	USDT Cell:  // sender can use a SUDT Cell to unlock the Approve Cell
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>  // equals to the sender_lock_hash in the args part of Approve Cell
		Type: <sudt_type_script>  // equals to the type script of Apporve Cell
		Data:
			sudt_amount: 900 UDT

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 1000 UDT

	CKB Normal Cell:
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for sender_lock_hash> need a identifier, marked as sender

Sender does not have this SUDT cell, but has a suitable CKB Normal Cell

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
		sudt_amount: 100 UDT

	CKB Normal Cell:  // sender can use a CKB Normal Cell to unlock the Approve Cell
		Capacity: XX CKBytes
		Lock: <sender_lock_script>  // equals to the sender_lock_hash in the args part of Approve Cell

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	Sender CKB Normal Cell:
		Capacity: XX+20 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for sender_lock_hash> need a identifier, marked as sender
6 Likes

Design of Approve Cell

背景

在 SUDT 的相关开发中,我们遇到了一个与用户体验紧密相关的问题。那就是,用户如何在没有某 SUDT 的 ACP cell 的时候接收该 SUDT?

一种解决方案是,用户需要自行创建一个该 SUDT 的 ACP cell(anyone-can-pay cell),而用户实际上无法在链上提前得知有人将要给 ta 发送一个新的 SUDT,目前只能通过链下通知的方式,告知用户创建需要 ACP cell。另外一种解决方案是,发送方需要在发送 SUDT 时赠送给对方一个该 SUDT 的 ACP cell。这两种解决方案在一定程度上会造成接收方无法便捷地接收代币,发送方需要额外付出转账成本的问题。

为了解决这一问题,我们设计了一个 Approve Cell 的方式,当发送方给一个没有该 SUDT ACP cell 的用户发送 SUDT 时,可以将 SUDT 转至一个 Approve Cell,然后等待接收方前来认领。这样就可以实现接收方在没有提前创建该 SUDT 的 ACP cell 的情况下,从链上获取到发送方的代币发送信息,进而再决定要不要创建一个 ACP cell 来接收该部分资产。

解决方案

发送方将需要转账的 SUDT 转移至一个 Approve Cell,在 Approve Cell 的 lock args 中添加发送方和接收方的信息,接收方在链上读取到相关的授权信息后,即可按照规则进行解锁。

// Structure of Approve Cell 

capacity: uint128 // 8 bytes
lock script: 
	code_hash: <approve_cell_lock_script>  // 32 bytes
	hash_type: type // 1 byte
	args: <receiver_lock_hash>, <sender_lock_hash>  // 20 bytes + 20 bytes
type script: <sudt_type_script>  // 65 bytes
data:{
	sudt_amount  // 16 bytes
}

// Total: 162 bytes
  • Apprvoe cell 设置有 approval 周期(限制发送方无法在 approval 周期内撤销 Approve Cell,这个会写死在approve_cell_lock_script 内)

Approve Cell 的解锁逻辑

Approve Cell 的 lock script 有两种解锁方式:

  • 接收方接收。接收方按照 Approve Cell 的规则解锁 Approve Cell,解锁方式根据签名规则分成两种:
    • 接收方提供 lock args,并使用默认的 secp256k1_blake160 作为 code_hash,组装成 lock script,并进行签名,且组装成的 lock script 经对比与 Approve Cell 中的 receiver_lock_hash 一致。
    • 接收方直接提供与 receiver_lock_hash 一致的 cell 作为 input cell,并进行签名。
  • 发送方撤回。发送方创建的 Approve Cell 在链上存在时间超过 approve 周期未被接收方领取时,发送方可以通过提供与 sender_lock_hash 一致的 cell 作为 input cell,并进行签名,实现撤回。

交易结构示例

创建 Approve Cell

1. 发送方给没有该 UDT ACP Cell 的用户发送 UDT 时,将会创建一个 Approve Cell

Inputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data: 
			sudt_amount: 1000 UDT

	CKB Normal Cell:
		Capacity: 1000 CKBytes
		Lock: <sender_lock_script>

Outputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args:  <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT  // approve amount of UDT

	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 900 UDT

	CKB Normal Cell:
		Capacity: (838-fee) CKBytes
		Lock: <sender_lock_script>

Witnesses
	<valid signature for sender_sudt_cell_lock_hash>
	<valid signature for sender_ckb_normal_cell_lock_hash>

Approve Cell 解锁逻辑1: 接收方申领

接收方使用官方 secp256k1_blake160 lock 进行接收(适用于 CKB 短地址)

接收方提供 lock args,并使用默认的 secp256k1_blake160 作为 code_hash,组装成 lock script,并进行签名,且组装成的 lock script 经对比与 Approve Cell 中的 receiver_lock_hash 一致。

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	Anyone SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <anyone_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 200 UDT

Outputs:
	Anyone SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <anyone_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 300 UDT

	CKB Normal Cell:
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<valid signature for secp256k1_blake160 + receiver lock script>  // need a identifier, marked as receiver
	<valid signature for anyone_lock_script>

接收方使用其他 lock script 进行接收

接收方直接提供与 receiver_lock_hash 一致的 cell 作为 input cell,并进行签名。

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <receiver_lock_script>  // equals to the receiver_lock_hash in the args part of Approve Cell
		Type: <sudt_type_script>
		Data:
			sudt_amount: 200 UDT

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <receiver_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 300 UDT

	CKB Normal Cell:  // send CKB back to the sender
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for receiver_lock_hash> // need a identifier, marked as receiver

Approve Cell 解锁逻辑2: 发送方撤回

发送方创建的 Approve Cell 在链上存在时间超过 approval 周期未被接收方领取时,发送方撤回 Approve Cell。

撤回时发送方需要在 input cells 中加入一个 lock script 与 Approve Cell args 中的 sender_lock_hash 一致的 cell,根据用户持有各种 cells 的不同情况,会衍生出多种多样的交易组装方式,为了方便前端筛选合适 cell 组装交易,我们可以优先考虑以下两种情况:

  • 如果发送方还有该 SUDT 的 cell (我们假设 SUDT cell 通常采用的都是 ACP lock),则将其作为 input cell,进行签名
  • 如果发送方没有该 SUDT 的 cell,而有与 sender_lock_hash 一致的 CKB Normal Cell,则将其作为 input cell,进行签名

发送方还有该 SUDT 的 cell

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args:  <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	USDT Cell:  // sender can use a SUDT Cell to unlock the Approve Cell
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>  // equals to the sender_lock_hash in the args part of Approve Cell
		Type: <sudt_type_script>  // equals to the type script of Apporve Cell
		Data:
			sudt_amount: 900 UDT

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 1000 UDT

	CKB Normal Cell:
		Capacity: 162 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for sender_lock_hash> // need a identifier, marked as sender

发送方没有该 SUDT 的 cell,但还有 CKB Normal Cell

Inputs:
	Approve Cell:
		Capacity: 162 CKBytes
		Lock: 
			code_hash: <approve_cell_lock_script>
			hash_type: type
			args: <receiver_lock_hash>, <sender_lock_hash> 
		Type: <sudt_type_script>
		Data:
		sudt_amount: 100 UDT

	CKB Normal Cell:  // sender can use a CKB Normal Cell to unlock the Approve Cell
		Capacity: XX CKBytes
		Lock: <sender_lock_script>  // equals to the sender_lock_hash in the args part of Approve Cell

Outputs:
	SUDT Cell:
		Capacity: 142 CKBytes
		Lock: <sender_lock_script>
		Type: <sudt_type_script>
		Data:
			sudt_amount: 100 UDT

	Sender CKB Normal Cell:
		Capacity: XX+20 CKBytes
		Lock: <sender_lock_script>

Witnesses:
	<000...000>
	<valid signature for sender_lock_hash> // need a identifier, marked as sender
2 Likes

What is the scenario that the approve period exists to prevent?

To prevent the receiver from not claiming the Approve Cell for a very long time. If the receiver don’t claim the Approve Cell, those CKB in the Approve Cell would be premanently occupied.

Why [quote=“stwith, post:4, topic:5029”]
To prevent the receiver from not claiming the Approve Cell for a very long time. If the receiver don’t claim the Approve Cell, those CKB in the Approve Cell would be premanently occupied.
[/quote]

But why wouldn’t you allow the sender to cancel at any time?

We consider the usual transfer format that Exchange typically use. Exchange would collect Approve Cells as many as possible and to claim them all in one transaction. If someone cancel the Approve Cell, it will cause the whole transaction to fail.

Considering the possibility that claim transactions could be aggregated, if there is a approve period, it would be much easier to assembly several claim transcations to one.

I would suggest making the approve_period an argument on the approve cell lock script. That makes the period configurable by the sender.

Yes, of course, adding the approve period to lcok script would own a lot of flexibility, but it may:

  • increase the sender’s cognitive threshold
  • increase the sender’s operational complexity
  • larger the capacity of the Approve Cell
  • increase the parsing cost on the wallet or exchange side
    So, it needs some discussion about whether we need to add the approve period into the approve cell lock script.

I don’t think that the cycle cost will be substantial, and the capacity cost would likely be only 16 bytes more (to specify a block height). To a sender, the 16 byte cost is going to be much less than having to deploy a custom code cell with a static value.

Approve period could also be made an optional parameter. An absent or zero value meaning it can be claimed by the sender at any time.

It’s more flexible, but more complicated to end users. I believe this solution is to solve the sUDT deposit problem. The user has to append 142 or more ckbytes to the sUDT to deposit to exchanges, which is very bad experience. With this proposal, user just locks some ckbytes, send the sudt to the exchange, and get back the locked ckbytes after the exchange collecting the sudt. A fixed maximum lock period makes it easy to understand, standard across different exchanges, and less interactions with users.

What is going to be the user flow for creating the approval cell when making an exchange deposit? Just a long address?

I think it’s a standard short address, exactly the same as the CKB deposit address. When the wallet initializes a sUDT deposit action, it will generate a approve cell with the target address as the beneficiary.

Hi @stwith

Would you please elaborate on what you mean by need an identifier in this one <valid signature for secp256k1_blake160 + receiver lock script> // need a identifier?

我在其他几处 witness 做了一些更改,加入了标识符的设置。加入标识符主要是用来区别这里提供的签名是去验证 sender 还是 receiver 的。

目前文中涉及到的 Approve Cell 主要有三种解锁方式:

  1. Receiver 为 CKB 短地址进行解锁(主要服务交易所),在 Approve Cell 对应的 witness 位置放入签名
  2. Receiver 为其他 CKB 地址进行解锁(主要 PW-lock 等 new lock),在 input cell 对应的 witness 位置放入签名
  3. Sender 进行解锁(主要 PW-lock 等 new lock),在 input cell 对应的 witness 位置放入签名

对于第二、第三种情况,我们需要在 witness 中加入标识符,来标记这是 receiver 解锁还是 sender 解锁(当然应该也可以通过别的方式来进行实现)

至于为什么要在第一种情况下,在 witness 中加入标识符,是因为 sender 其实也可以采用和第一种类似的方式,只提供一个 CKB 短地址对应的签名(签名算法采用 secp256k1-blake160)进行解锁,这个时候就需要加入标记符进行区分。

这个标示符号如果是直接加在 WitnessArgs.lock 中的话,这样是不是意味着需要去修改 ckb 默认验签方式 secp256k1 的 lock 脚本? 还是说有其它更合理的方法?

我没理解错的话,当前 secp256k1 lock 脚本中的方法 verify_secp256k1_blake160_sighash_all(unsigned char pubkey_hash[BLAKE160_SIZE]) 是直接拿取 WitnessArgs.lock 当作签名进行验签,而不是通过指定传入方法参数的形式。如果是把包含 标示符WitnessArgs.lock 进行验签,似乎无法用当前官方的 secp256k1 来进行。

这个 anyone 指得就是接收方对吗?

不需要去修改 CKB 的默认验签方式,标识符是用来给 approve_cell_lock_script 进行识别,然后走哪一种解锁逻辑来解锁脚本的。

Approve Cell 这个名词看起来有点奇怪,感觉这个 Cell 和支票有点类似:开票人指定了收款人和金额,收款人拿着支票去银行,执行付款操作,当然开票人也可以选择在收款人收款之前废弃这张支票。

在 CKB 里面这个 Cell 和支票不同的一点在于它本身还有价值(本身所占用的 CKBytes),所以需要在操作之后,相当于把支票上的填写内容清空,返回给开票人。

是否可以考虑直接叫支票Cell: Check Cell

2 Likes

Implementation update:

repo: GitHub - duanyytop/ckb-cheque-script: The lock script of cheque cell on Nervos CKB

  • Change the name from “Approvel cell” to “Cheque cell”
  • Add default secp256k1 lockscript unlock logics

The lockscript of a Cheque cell is:

Lock: 
	code_hash: <cheque_cell_lock_script>
	hash_type: type
	args:  <receiver_lock_hash[:20]> | <sender_lock_hash[:20]> 

There are two methods to unlock the Cheque cell for both sender and receiver:

Input cell match method

  • The witness field of the Cheque cell is empty
  • There is at least one input cell’s lock script matchs the lock_hash declared in the Cheque cell’s lock args
    • The conresponding input cell’s witness is not empty

Signature match method

  • The witness field of the Cheque cell is an valid secp256k1 signature
  • Generate a lockscript by the following rules:
    • Recover the public key from the signature, get the first 20 bytes of its blake2b hash as lock args
    • Use the default secp256k1_signhash_all script as code_hash
    • Set hash_type as type
  • Ensure the hash of the generated lockscript matchs the lock_hash declared in the Cheque cell’s lock args

And additionally, the transaction must follow other business logics defined by the spec, including the withdrawal time limit and so on.

5 Likes