Enhance sUDT’s Programmability with xUDT

Enhance sUDT’s Programmability with xUDT

·

13 min read

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 or flags & 0x1FFFFFFF is 0, no extension data is needed. xUDT behaves exactly as sUDT.

  • If flags & 0x1FFFFFFF is 0x1, extension data is the molecule-serialized ScriptVec.

  • If flags & 0x1FFFFFFF is 0x2, extension data contains the blake160 hash of the ScriptVec. The actual ScriptVec 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 of XUDTData will not be used by the xUDT script. It is reserved for lock script specific data for the current cell.

  • The data field of XUDTData, which can be thought as cell data's counterpart for extension scripts, must be of the same length as ScriptVec 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 in data field of XUDTData 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 the flags is not present

  • match output type script hash if flags & 0x40000000 is non-zero

  • match 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 *)&current_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.

ParameterValue
code_hash0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb
hash_typetype
tx_hash0xbf6fb538763efec2a70a6a3dcb7242787087e1030c4e7d86585bc63a9d337f5f
index0x0
dep_typecode

✍🏻 Witten by Biao Yi, Jiandong Xu


You may also be interested in:

By the same author: