RFC: Extensible UDT

Extensible UDT

Extensible UDT(xUDT) is an extension on Simple UDT for defining more behaviors a UDT might need. While simple UDT provides a minimal core for issuing UDTs on Nervos CKB, extensible UDT builds on top of simple UDT for more potential needs, such as regulations.

Data Structure

xUDT Cell

An xUDT cell is backwards compatible with Simple UDT, all the existing rules defined in the Simple UDT spec must still hold true for xUDT cells. On top of sUDT, xUDT extends a cell like following:

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 parts provide all the new functions needed by xUDT, we will explain the detailed structure below:

xUDT Args

xUDT args has the following structure:

4 byte xUDT flags Variable length extension data

Depending on the content of flags, different extension data might be attached:

  • If flags is all 0, we won’t need any extension data. Note a backward-compatible way of viewing things, is that a plain sUDT cell also has a hidden flags field with all zeros.
  • If flags is 0x1, extension data will contain a molecule serialized ScriptVec structure:
table Script {
    code_hash:      Byte32,
    hash_type:      byte,
    args:           Bytes,
}

vector ScriptVec <Script>;

Each entry included in ScriptVec structure is interpreted as a CKB script hash for an extension script with additional behaviors. When an xUDT script is executed, it will run through each included extension script here. Only when all extension scripts pass validation, will xUDT also consider the validation to be successful.

An extension script can be loaded in any of the following ways:

  1. Some extension logics might have predefined hash, for example, we can use 0x0000 ... 0001 to represent regulation extension. The actual code for such scripts can be embedded in xUDT script itself.
  2. Owner lock can be used: if an input cell in current transaction uses a lock script with the same script hash as current extension script, we can consider the extension script to be validated already.
  3. If an extension script does not match any of the above criteria, xUDT will use the code_hash and hash_type included in the extension script to invoke ckb_dlopen2 function, hoping to load a dynamically linked script from cell deps in current transaction. If a script can be located successfully, xUDT will then look for an exported function with the following signature:
int validate(int is_owner_mode, size_t extension_index, const uint8_t* args, size_t args_length);

is_owner_mode indicates if current xUDT is unlocked via owner mode(as described by sUDT), extension_index refers to the index of current extension in the ScriptVec structure. args and args_length are set to the script args included in Script structure of current extension script.

If this function returns 0, the validation for current extension script is consider successful.

  • If flags is 0x2, extension data will contain the blake160 hash of the ScriptVec structure as explained in previous section. The actual ScriptVec structure data will be included in a witness contained in the current transaction. We will explain this part below.

xUDT Data

xUDT data is a molecule serialized XUDTData structure:

vector Bytes <byte>;
vector BytesVec <Bytes>;

table XUDTData {
  lock: Bytes;
  data: BytesVec;
}

The data field included in XUDTData, must be of the same length as ScriptVec structure included in xUDT args. Some extensions might require user specific data stored in each xUDT cell. xUDT data provides a place for such data.

The lock field included in XUDTData will not be used by xUDT script. It is reserved for lock script specific data for current cells.;

An extension script will first need to first locate the index it resides in xUDT args, then look for the data for current extension script at the same index in data field of XUDTData structure.

Operations

xUDT uses the same governance operations as Simple UDT: an owner lock controls all governance operations, such as minting.

A normal transfer operation of xUDT, however, differs from Simple UDT. Depending on the flags used, there might be 2 usage pattern:

Raw Extension Script

When flags is set to 0x1, raw extension data is included in xUDT args directly.

Inputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Outputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        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 xUDT script. It is parsed first as WitnessArgs structure, the input_type field of WitnessArgs, is thus treated as BytesVec structure. This structure must also be of the same length as xUDT args and xUDT data part. An extension script might also require transaction specific data so as to validate. Witness here provides a place for this data needs.

Notice each extension script is only executed once in the transaction. The extension script is responsible for checking all xUDT cells of the current type, ensuring each cell data and witness for current extension script, can be validated per extension script’s rules.

P2SH Style Extension Script

When flags is set to 0x2, only the blake160 hash of extension data is included in xUDT args. User is required to provide the actual extension data in witness directly:

Inputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Outputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Witnesses:
    WitnessArgs structure:
      Lock: <user defined>
      Input Type: <Raw Extension Data> <BytesVec structure>

The only difference here, is that input_type field in the corresponding WitnessArgs structure, contains raw extension data in ScriptVec data structure, xUDT script must first validate that the hash of raw extension data provide here, is the same as blake160 hash included in xUDT args. After this, it uses the same logic as the previous workflow.

10 Likes

翻译:可扩展的 UDT(Extensible UDT)

Extensible UDT

可扩展的 UDT (Extensible UDT,本文统一称为 xUDT)是一种基于 Simple UDT 的扩展,可用来定义更多 UDT 可能需要的行为。在 sUDT 只提供了在 Nervos CKB 发行 UDT 的最小核心功能之际,xUDT 可以建立于其上以面向更多潜藏的需求,例如监管。

数据结构

xUDT Cell

xUDT 是一个向后兼容于 sUDT 的 cell,所有 sUDT 规范定义的既存规则在 xUDT 中一样需符合,基于sUDT 扩展的 xUDT cell 可以长的像这样:

data:
    <amount: uint128> <xudt data>
type:
    code_hash: extensible_udt type script
    args: <owner lock script hash> <xudt args>
lock:
    <user_defined>

这个被加上去的 xudt argsxudt data 的部分提供了所有 xUDT 所需的新功能 ,我们将会在下文阐述这些细节的架构:

xUDT Args

xUDT args 拥有的架构如下:

4 个字节 xUDT标记可变动的扩展数据变数的长度
依赖于 flags 的内容,可能会附加不同的扩展数据:

  • 如果 flags 全部为 0,我们不要需要任何扩展数据。值得注意的是,向后兼容的查看方式是,一个空白的 sUDT cell 也等于有一个全部为 0 的隐藏 flags 字段。
  • 如果 flags 是 0x1,那么扩展数据将包含一个以 ScriptVec 结构进行序列化的 molecule
table Script {
    code_hash:      Byte32,
    hash_type:      byte,
    args:           Bytes,
}

vector ScriptVec <Script>;

ScriptVec 结构中包含的每个条目,都被解释为具有附加行为的扩展脚本的 CKB 脚本哈希。当一个xUDT 脚本被执行时,它将运行这里所包含的每个扩展脚本。只有当所有扩展脚本都通过验证时,xUDT才会认为该验证是成功的。

一个扩展的脚本可以被下列的任一种方式加载:

  1. 有些扩展的逻辑可能已经有预定义的哈希,例如,我们可以使用 0x0000 ... 0001 来表示监管的扩展。这些脚本的实际代码已经被镶嵌在 xUDT 自身的脚本中了。
  2. Owner lock (所有者的锁)可拿来使用:如果当前交易中的一个 input cell 使用了与当前扩展脚本相同脚本哈希的 lock script,我们可以认为扩展脚本已经被验证。
  3. 如果一个扩展脚本不匹配任何上述标准,xUDT 将使用包含在扩展脚本中的 code_hash 和 hash_type 以调用 ckb_dlopen2 的功能,从当前交易的 cell deps 中加载动态链接的脚本。如果脚本可以成功定位,xUDT 将寻找一个带有以下签名的导出函数:
int validate(int is_owner_mode, size_t extension_index, const uint8_t* args, size_t args_length);

is_owner_mode 表示当前 xUDT 是否通过所有者模式解锁(如 sUDT 所述),extension_index 指的是当前的扩展在 ScriptVec 结构中的索引。argsargs_length 被设置为当前扩展脚本的 script 结构中所包含的 script args。

如果该函数返回0,则认为对当前扩展脚本的验证是成功的。

  • 如果 flags是0x2,扩展数据将包含前一节解释的 ScriptVec 结构的 blake160 哈希。实际的 ScriptVec 结构数据将包含在当前交易中包含的 witness 中。我们将在下面解释这一部分。’

xUDT Data

xUDT 数据是一个以 XUDTData 结构来进行序列化的模组

vector Bytes <byte>;
vector BytesVec <Bytes>;

table XUDTData {
  lock: Bytes;
  data: BytesVec;
}

包含在 XUDTData 中的 data 字段,必须与包含在 xUDT args 中的 ScriptVec 结构的长度相同。一些扩展可能需要在每个 xUDT cell 中存储特定于用户的数据。xUDT数据为此类数据提供了一个放置的位子。

XUDTData 中包含的 lock 字段不会被 xUDT 脚本使用,它会留给当前 Cell 的锁定脚本的特定数据。

扩展脚本首先需要找到它定位在 xUDT args 中的索引,然后在 XUDTData 结构的 data 字段的同一个索引处,查找当前扩展脚本的数据。

#操作

xUDT 采用与 sUDT 相同的治理操作: 所有者锁控制所有治理行为,如代币铸造。

然而,xUDT 的正常转移操作不同于 sUDT。根据所用的 flags,可能会有两种使用模式:

###原始扩展脚本

当’ flags设置为0x1时,原始扩展数据将直接包含在 xUDT args中。

Inputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Outputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Witnesses:
    WitnessArgs structure:
      Lock: <user defined>
      Input Type: <BytesVec structure>

与第一个输入 xUDT cell 相同的索引的 witness 是由 xUDT 脚本定位的。它首先被解析为 WitnessArgs 结构,因此 WitnessArgs 的 input_type 字段被视为 BytesVec 结构。这个结构也必须与 xUDT args xUDT data 部分的长度相同。扩展脚本可能还需要特定交易的数据,以便进行验证。Witness 在此为这种数据需求提供了一个放置的位置。

注意,每个扩展脚本在交易中只执行一次。扩展脚本负责检查当前类型的所有 xUDT cell,确保当前扩展脚本的每个 cell 数据和 witness 都可以根据扩展脚本的规则进行验证。

P2SH Style Extension Script

flags 设置为 0x2 时,xUDT args 只包含扩展数据的 blake160 哈希。用户会被要求直接提供在Witness 中的扩展数据:

Inputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Outputs:
    <vec> xUDT_Cell
        Data:
            <amount: uint128> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    <...>
Witnesses:
    WitnessArgs structure:
      Lock: <user defined>
      Input Type: <Raw Extension Data> <BytesVec structure>

这里唯一的区别是,在相应的 WitnessArgs 结构中的 input_type 字段包含在 ScriptVec 数据结构中的原始扩展数据,xUDT 脚本必须首先验证这里提供的原始扩展数据的哈希,是否与 xUDT args 中包含的blake160 哈希相同。在此之后,它将使用与前面工作流程相同的逻辑。

2 Likes

The lock field included in XUDTData will not be used by xUDT script. It is reserved for lock script specific data for current cells.;

As I understand it, here lock indicates “some data is reserved for the cell’s lock script”, is that right? Can you give some examples of what kinds of data will need to be reserved here?

Not sure if RCE is first use case of xUDT or not.

As a learner,I am also eager to know case and detail about that,I guess that the author Xuejie could answer it very well .

1 Like

请教下,xudt script的规范是否兼容以下这种情况:

Inputs:
    <vec> xUDT_Cell1
        Data:
            <amount: 100> 
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <0x00..>
        Lock:
            <user defined>
      
    <vec> xUDT_Cell2
        Data:
            <amount: 50> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
Outputs:

    <vec> xUDT_Cell1
        Data:
            <amount: 40> 
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <0x00..>
        Lock:
            <user defined>

    <vec> xUDT_Cell2
        Data:
            <amount: 110> <xudt data>
        Type:
            code_hash: extensible_udt type script
            args: <owner lock script hash> <xudt args>
        Lock:
            <user defined>
    

就是普通最小(s)xudt cell 是否可以往加载了 Extension Script 的该xudt cell 转账。或者换种说法,相同owner lock script hash但加载了 Extension Script的xudt 跟没加载Extension Script的xudt 是否视为同一币种?

This is not possible, a token type is distinguished by the full type script, including type script args. When type script args differ, they will be treated as different token types.

3 Likes

I noticed that the plug-ins in the Extensible UDT(xUDT) standard can only be added before UDT is released, rather than dynamically added by the owner or governance. Is this design to consider the compatibility of sUDT?
I think the dynamic add and delete plug-in is a very useful design, which can be used for token governance and so on.

Maybe we can design a xUDT plug-in, this plug-in can dynamically add and delete plug-ins, so that xUDT supports dynamic addition and deletion of plug-ins in a compromise way.

2 Likes

Although the owner can change the logic by upgrading the existing plug-ins, this greatly reduces the composability and reusability of the code. If the dynamic addition and deletion of plug-ins are adopted, after a specific plug-in is implemented, many different xUDTs can be combined to reduce the state space occupation on the chain and reduce the risk of code audit at the same time.

However, if the existing xUDT design is maintained, the fixed args of the xUDT plug-in may also limit the future expression of the dynamic plug-in. If it can no longer be compatible with sUDT, a fully dynamic xUDT implementation can be realized to cope with the infinite possibilities in the future.

This draft RFC is updated:

A new section “Owner Mode Update” is added.

An audited implementation can be found here:

1 Like