Table of contents
- Why xUDT?
- XUDT data structures
- XUDT extension scripts
- XUDT cell
- XUDT Args
- XUDT data
- Run extension scripts
- Invoke extension scripts
- A. Built-in extension script
- B. When the extension script is exactly the lock script of the cell
- C. Load external extension script with dynamic library
- Enable owner mode
- Via an owner cell
- Via a script
- Complete workflow
- Example extension script
- Example transactions
- Conclusion
Why xUDT?
Simple UDT (also known as sUDT, simple User Defined Token), by definition, is simple in design, and thus may not be enough for many use cases. With xUDT (Extensible UDT), we extended sUDT's programmability to support a wider range of scenarios. We designed xUDT with backward compatibility with sUDT in mind. When xUDT-specific fields are missing the usual sUDT behaviors apply.
XUDT can be used in these cases which sUDT cannot cover, for example:
Check the total supply of the token: we can use an extension script to ensure the total number of tokens in the output is below some limit.
Enforce more restrictions on token transfer: for example, we may implement a time-lock in an extension script to allow token transfer after some specified time.
Compactly represent users’ accounts and balances of an exchange: we can save the account and balance information in a Sparse Merkle Tree and then use an extension script to make sure that users’ updated token amount (i.e., updates to the Merkle Tree) is valid.
Programmatically mint tokens with contracts: we could mint sUDT tokens with the owner account, but it requires an extra cell which is problematic in some cases. Programmatically minting tokens with xUDT is much easier. We illustrate this in the "Enable owner mode via a script" section below, where we check the validity of a secp256k1 public key with the hash identical to the xUDT owner hash to unlock owner mode. See this PR for details.
In this article, we first look at the standard data structure of extension scripts contained in an xUDT cell, then explain how to run these scripts. Following by a simple extension script to limit the amount of transfer and two example transactions that leverage the advanced features of xUDT. We conclude with some complicated real-world use cases of xUDT at the end of the article.
XUDT data structures
Just like normal scripts, we can locate extension scripts by their hash and hash type, pass arguments to them and store data in the cell for further usage.
The following section explains the standard data structure of extension scripts contained in an xUDT cell. The next section will cover how to actually run those scripts.
XUDT extension scripts
Every extension script is defined as a Script
. An xUDT cell can contain multiple extension scripts. Script
's molecule definition is as below:
table Script {
code_hash: Byte32,
hash_type: byte,
args: Bytes,
}
vector ScriptVec <Script>
Each (code_hash, hash_type)
tuple here represents an extension script, while the corresponding args
represents the arguments to be passed to the extension script.
XUDT cell
On top of sUDT, xUDT extends a cell as follows:
data:
<amount: uint128> <xudt data>
type:
code_hash: extensible_udt type script
args: <owner lock script hash> <xudt args>
lock:
<user_defined>
The added xudt args
and xudt data
provide all the new functions needed by xUDT, where xudt args
is used to locate the extension script and xudt data
is the extension script's counterpart of cell data.
XUDT Args
xUDT args has the following structure:
<4-byte xUDT flags (optional)>, <extension data (variable length)>
Where 4-byte xUDT flags
are flags to further tweak the behavior of extension scripts, and the extension data
is a pointer to the extension scripts data. Its actual meaning depends on the flags:
If
flags
is missing orflags & 0x1FFFFFFF
is 0, noextension data
is needed. xUDT behaves exactly as sUDT.If
flags & 0x1FFFFFFF
is 0x1,extension data
is the molecule-serializedScriptVec
.If
flags & 0x1FFFFFFF
is 0x2,extension data
contains the blake160 hash of theScriptVec
. The actualScriptVec
structure data will be included in a witness contained in the current transaction.
XUDT data
xUDT data has a molecule-serialized XUDTData
structure as follows:
vector Bytes <byte>
vector BytesVec <Bytes>
table XudtData {
lock: Bytes,
data: BytesVec,
}
The
lock
field ofXUDTData
will not be used by the xUDT script. It is reserved for lock script specific data for the current cell.The
data
field ofXUDTData
, which can be thought as cell data's counterpart for extension scripts, must be of the same length asScriptVec
structure included in xUDT args. This is useful for scripts that require some user-specific data storage. An extension script first needs to locate the index it resides in xUDT args, then look for the data in the current extension script at the same index indata
field ofXUDTData
structure.
Run extension scripts
This section explains the technical details on how to run xUDT extension scripts. Those who want to write their own extension scripts are expected to understand this section thoroughly. We first illustrate how the extension scripts are located and invoked, and then explain how the owner mode is set and passed to the extension scripts.
Invoke extension scripts
XUDT loads the extension script function validate
from the extension script dynamic library (Case C below). There are a few special cases: when the hash belongs to a pre-defined script (Case A below) and when the extension script is exactly the lock script of this cell (Case B below).
A. Built-in extension script
Some extension scripts have a hard-coded hash. For example, we use0x0000000000000000000000000000000000000000000000000000000000000001
to represent regulation extension. The actual code for such scripts is already embedded in xUDT type script binary. So one doesn’t need to look up binaries in the dependent cells.
B. When the extension script is exactly the lock script of the cell
If an input cell in the current transaction uses a lock script that has the same script hash as the current extension script, we consider the extension script to be validated already. This is designed to avoid consuming extra cycles. Since we have already validated the lock script, there is no need to run the same script again.
C. Load external extension script with dynamic library
If an extension script does not fall into the cases above, xUDT will use the code_hash
and hash_type
included in the extension script to invoke ckb_dlopen2 function that loads a dynamically linked script from cell_deps
in the current transaction. If a script can be located successfully, xUDT will then look for an exported function with the following signature and call the function:
int validate(int is_owner_mode, size_t extension_index, const uint8_t* args, size_t args_length);
A non-zero return value of the function validate
indicates there is some error while running the extension script. Here,is_owner_mode
(its accurate definition is presented below) indicates whether the current xUDT is unlocked via owner mode (as described by sUDT),extension_index
refers to the index of the current extension in the ScriptVec
structure. args
and args_length
are set to the script args and the Script
structure length of the current extension script.
Enable owner mode
Via an owner cell
If any input or output cell (whether to use input or output cell depends on the flags
, see below for the exact rule) in the current transaction has the same script hash as the owner lock script hash, then is_owner_mode
will be set to true. For convenience, we call this cell an owner cell. Although extension scripts can use is_owner_mode
at their discretion, it is generally expected that everything done by an owner is permitted.
To set owner mode, we match the owner hash with the script hash as follows:
match input lock script hash if
flags & 0x20000000
is zero or theflags
is not presentmatch output type script hash if
flags & 0x40000000
is non-zeromatch input type script if
flags & 0x80000000
is non-zero
Output lock scripts are always ignored because they won’t run in a transaction.
Via a script
One problem with the above approach to enable owner mode is that it needs to consume a cell. When you want to do a lot of owner operations (minting or other privileged operations), there could be a serious cell contention. To mitigate this problem, we enable owner mode by adding additional information to the witness.
Concretely, the witness has the following structure:
Witnesses:
WitnessArgs structure:
Lock: <user defined>
Input Type: <Script Vector> <Raw Extension Data>
<Owner Script, optional> <Owner Signature, optional>
Ouptut Type: <user defined>
As stated above, the Input Type
of the witness may contain the Script Vector
and Raw Extension Data
, representing all the extension scripts and their data. Here, an Owner Script
and an Owner Signature
, may be specified. The Owner Script
of type Script
is a script used to check if owner mode should be enabled in this transaction (In this case, this script must have a hash matching the owner hash specified in xUDT args); the Owner Signature
of type Bytes
may be used by the Owner Script
to check the legality of this transaction to claim owner mode.
To check if owner mode should be enabled, xUDT invokes the script and run the function int validate(int is_owner_mode, size_t extension_index, const uint8_t* args, size_t args_length)
, where args
and args_length
are set to the owner hash and its length respectively, while is_owner_mode
and extension_index
are irreverent here. This script runs whatever it needs to check the validity of the request to enable owner mode. When it considers owner mode should be enabled, this script must return success. Following is one simple workflow to obtain Owner Signature
and return success once the signature is verified as valid:
__attribute__((visibility("default"))) int validate(
int _is_owner_mode,
size_t _extension_index,
const uint8_t *args,
size_t args_len) {
int ret = 0;
uint8_t signature[SIGNATURE_SIZE];
// Read signature from witness.
ret = get_owner_signature(signature);
if (ret != 0) {
return ret;
}
// Read the signing message. We use transaction hash for simplicity.
uint64_t tx_hash_len = BLAKE2B_BLOCK_SIZE;
uint8_t tx_hash[BLAKE2B_BLOCK_SIZE];
int ret = ckb_load_tx_hash(tx_hash, &tx_hash_len, 0);
if (ret != CKB_SUCCESS) {
return ret;
}
// Validate signature.
ret = verify_signature((uint8_t *)args, args_len, signature, SIGNATURE_SIZE, tx_hash, BLAKE2B_BLOCK_SIZE);
return ret;
}
We first obtain the signature using syscall ckb_load_witness
in get_owner_signature
, then read the message that needs to be signed (in this case, just the transaction hash obtained from ckb_load_tx_hash
) and then verify the signature is indeed signed by the secp256k1 public key with owner hash. Full code can be found here.
Complete workflow
Figure 1. Owner mode complete workflow (made from the source code with GraphvizOnline)
The blue box indicates how to enable owner mode via an owner cell, and the green box shows how to enable owner mode via a script. Overall, the attempt is to enable owner mode via an owner cell first. If that fails, then try to enable owner mode via a script.
Example extension script
Now that we have familiarized ourselves with the basic concepts of xUDT. Let’s use xUDT to build a practical example. Imagine you want to limit the transfer amount of user-defined token in a single transaction. To do this, all you have to do is write a simple validation function.
#include <stddef.h>
#include <stdint.h>
#include "blockchain.h"
#include "ckb_syscalls.h"
#define ERROR_AMOUNT -100
// Maximum amount of the token for a user account to transfer.
const uint128_t MAX_AMOUNT = 0;
__attribute__((visibility("default"))) int validate(
int is_owner_mode,
size_t extension_index,
const uint8_t* args,
size_t args_len) {
// always return success if the token owner is running this function.
if (is_owner_mode) {
return 0;
}
size_t i = 0;
while (1) {
uint128_t current_amount = 0;
uint64_t len = 16;
// Load the token amount.
ret = ckb_load_cell_data((uint8_t *)¤t_amount, &len, 0, i,
CKB_SOURCE_GROUP_INPUT);
// When `CKB_INDEX_OUT_OF_BOUND` is reached, we know we have iterated
// through all cells of current type.
if (ret == CKB_INDEX_OUT_OF_BOUND) {
break;
}
if (ret != CKB_SUCCESS) {
return ret;
}
if (len < 16) {
return ERROR_ENCODING;
}
input_amount += current_amount;
if (input_amount > MAX_AMOUNT) {
return ERROR_AMOUNT;
}
i += 1;
}
// No error occurred, return success.
return 0;
}
Note that we don’t need to check whether the token amount summation overflows, since xUDT automatically applies the usual sUDT rules to validate transactions. It is only necessary to embed the application specific logic to the validate
function. We do this to ensure the amount is not larger than MAX_AMOUNT
.
This is a rather simple example. A more realistic extension script would be putting the amount limit in an upgradeable cell and looking up the limit in the extension script. When there is a security bug, you can upgrade the amount limit cell to protect users’ assets. You may also refer to Regulation Compliance Extension (RCE), which allows you to make a whitelist or blacklist of token transfers and to update these lists when needed as well.
Example transactions
Below are two example transactions that leverage the advanced features of xUDT.
Raw extension script
When flags & 0x1FFFFFFF
is set to 0x1, the extension script data is directly included in xUDT args.
Inputs:
<vec> xUDT_Cell
Data:
<amount: uint128> <xudt data>
Type:
code_hash: extensible_udt type script
args: <owner lock script hash> <4-byte xUDT flags (optional)>,
<extension data (variable length)>
Lock:
<user defined>
<...>
Outputs:
<vec> xUDT_Cell
Data:
<amount: uint128> <xudt data>
Type:
code_hash: extensible_udt type script
args: <owner lock script hash> <4-byte xUDT flags (optional)>,
<extension data (variable length)>
Lock:
<user defined>
<...>
Witnesses:
WitnessArgs structure:
Lock: <user defined>
Input Type: <BytesVec structure>
The witness of the same index as the first input xUDT cell is located by the xUDT script. We first parse the witness as a WitnessArgs structure, and then input_type
field of the parsed WitnessArgs
is treated as a vector of bytes (i.e. a BytesVec). This vector must be the same length as that of xUDT args and xUDT data. An extension script might also require transaction-specific data to validate. Witness here provides a place for this data.
P2SH style extension script
Just as Bitcoin‘s Pay to Script Hash (P2SH), this extension script enables users to lock data in the hash of a script.
When flags & 0x1FFFFFFF
is set to 0x2, only the blake160 hash of the extension data is included in xUDT args. The user is required to provide the actual extension script data in witness:
Inputs:
<vec> xUDT_Cell
Data:
<amount: uint128> <xudt data>
Type:
code_hash: extensible_udt type script
args: <owner lock script hash> <4-byte xUDT flags (optional)>,
<extension data (variable length)>
Lock:
<user defined>
<...>
Outputs:
<vec> xUDT_Cell
Data:
<amount: uint128> <xudt data>
Type:
code_hash: extensible_udt type script
args: <owner lock script hash> <4-byte xUDT flags (optional)>,
<extension data (variable length)>
Lock:
<user defined>
<...>
Witnesses:
WitnessArgs structure:
Lock: <user defined>
Input Type: <Script Vector> <Raw Extension Data>
The only difference here is that the input_type
field in the corresponding WitnessArgs
structure contains both ScriptVec
(the extension scripts vector) and raw extension data in ScriptVec
data structure. xUDT script must first validate that the hash of raw extension data provided here is the same as blake160 hash included in xUDT args. After this, it uses the same logic as the previous workflow.
Conclusion
This article gives a short introduction to the xUDT script, which is a modular, developer-friendly, and powerful extension of sUDT. Users can now combine various extension scripts to suit their needs (for example, using both an RCE extension and an emergency response extension). Developers can easily extend their token's functionality, since writing an extension script is as easy as writing a validation function.
XUDT can be improved further. One way to unlock more possibilities would be extending xUDT with other scripting languages, e.g. Lua. This would make extending xUDT’s functionality even easier.
XUDT has been deployed to the testnet. You are encouraged to try all its new features.
Parameter | Value |
code_hash | 0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb |
hash_type | type |
tx_hash | 0xbf6fb538763efec2a70a6a3dcb7242787087e1030c4e7d86585bc63a9d337f5f |
index | 0x0 |
dep_type | code |
✍🏻 Witten by Biao Yi, Jiandong Xu
You may also be interested in:
By the same author: