解析CKB交易结构 | 技术帖

在本篇文章中,让我们和 CKB 开发者 Ian 一起深入探究 CKB 基本的数据结构——交易。

作者:Ian,Nervos 工程师,专注于系统设计和客户端实现,前 Hooya Game CTO,前 Groupon 软件工程师。原文链接:https://github.com/nervosnetwork/rfcs/blob/transaction-structure/rfcs/0022-transaction-structure/0022-transaction-structure.md
翻译 & 校对:Williams & Kelly

这篇文章分成两个部分。第一个部分包含了核心的交易特征,而第二部分介绍一些扩展内容。在撰写本文时,对应的 CKB 版本是 v0.25.0,在未来的版本中交易结构还可能有所变动。您可以点击阅读原文,查看最新版本。

解析CKB交易结构 | 技术帖

上图是关于交易结构的概览。有别于逐字逐句的解释个名词,我将会介绍 CKB 转账能够提供的各种特殊结构,以及这些名词在其中的具体意思。

Part I:核心特征

价值储存

CKB 采用的是 UTXO 模型。一笔交易销毁了一些在先前交易下创建的输出(作为输入),并且创建一些新的输出,我们在 CKB 中将此交易输出称做一个 Cell。因此在这里的 Cell 和交易输出是可以替换的。

下图显示了在此层中会出现的专有名词。

解析CKB交易结构 | 技术帖
此交易销毁了 inputs 中的 Cell,同时创建了在 outputs 中的 Cell。

CKB 主链将交易打包成块。我们可以在区块中利用从零(也就是创世区块)开始递增的非负整数(编号),作为区块编号来关联链上的区块。在区块中的交易也是按照顺序排列的。我们可以说编号较小的区块是较早(old)的区块,如果一个交易在较早的区块上,或者它所在的区块的位置早于其它区块,那么它也会是比较早的交易。在下面的示例中,区块 i 比区块 i+1 早。交易 tx1 要比 tx2 早,也比 tx3 早。

解析CKB交易结构 | 技术帖

在所有先前的交易中,一个可用(Live)的 Cell 会以输出而非输入的形式出现。一个被销毁(Dead)的 Cell 代表它是以输入的形式在其它较早的交易中被使用过。一个交易只能以可用的 Cell 作为输入。

我们可以从除了 witnesses 之外的所有交易字段计算交易的哈希。关于如何计算交易哈希的更多信息,可以参阅附录 A。

交易哈希是独一无二的。因为一个 Cell 总是被一个交易创建出来,而每个新的 Cell 在交易输出的数组中都有他自己的位置,所以我们可以通过交易哈希以及输出索引去指向一个 Cell。OutPoint 结构是一种引用类型。交易在输入时会使用 OutPoint 来指向先前被创建的 Cell,而非嵌入其中。

解析CKB交易结构 | 技术帖

Cell 将 CKB 代币存储在字段 capacity 中。一个交易不能够凭空铸造 capacity,所以交易必将符合以下规则:

    sum(cell's capacity for each cell in inputs)≥ sum(cell's capacity for each cell in outputs)

    在输入中每个 Cell 容量的总和要大于等于在输出中每个 Cell 容量的总和。

    矿工可以收取这两者之间的价差做为手续费。

      fee = sum(cell's capacity for each cell in inputs)- sum(cell's capacity for each cell in outputs)

      如果你熟悉比特币,那么就会发现在价值储存层都是相似的,但是比特币缺乏锁定脚本来保护交易输出的所有权。CKB 正好有这个特征,但是在我们探讨这个话题之前,让我们先来谈谈 Cell Data 和 Code Locating 层吧,这是任何 CKB 中脚本特征的依据。

      Cell Data

      除了能够存储价值通证以外,CKB Cell 还能储存任意数据。
      解析CKB交易结构 | 技术帖
      字段 outputs_data 是输出的并行数组。在 outputs 中第 i 个 Cell 的数据对应的是 outputs_data 中的第 i 项。

      解析CKB交易结构 | 技术帖Cell 中的 capacity 不只代表通证的数量,也代表能够存储数据的限制。这也是它如此命名的原因,因为它代表 Cell 的存储容量。

      capacity 不仅能用于存储数据,它还需要涵盖 Cell 中的所有字段,包括  data、 locktype 以及 capacity 本身。

      计算占用容量的规范请参考:

      https://github.com/nervosnetwork/ckb/wiki/Occupied-Capacity

      交易势必会创建一个占用容量小于(输入) Cell 容量的输出 Cell。

        occupied(cell) ≤ cell's capacity

        代码定位

        Cell 中有两个字段的类型是 Script。CKB-VM 会运行所有输入 Cell 中的 lock 脚本,还会运行所有输入和输出 Cell 中的 type 脚本。

        我们区分了代码和脚本这两种术语:

        • 脚本具有脚本结构
        • 代码是 RISC-V (可运行的)二进制
        • 一个代码 Cell 是其数据为代码的 Cell

        脚本并没有直接包含代码。看看下面的脚本结构。现在我们可以忽略哈希类型的 Type 以及 args 字段。

        解析CKB交易结构 | 技术帖

        当 CKB-VM 需要运行一个脚本时,它必须要先找到它的代码。字段 code_hash 和 hash_type 就是用来查看代码的。

        在 CKB 中,脚本代码会被编译成 RISC-V 二进制文件。这个二进制文件是以数据的形式存储在 Cell 中的。当 hash_type 是数据时,脚本会被定位在一个数据哈希和脚本的 code_hash 相等的 Cell 中。Cell 数据哈希,如其名所示,是从 Cell 的数据中算出来的(详见附录 A)。在交易中的范围是有限制的,脚本只能从 cell_deps 中找到一个匹配的 Cell。

        解析CKB交易结构 | 技术帖

        下图将解释 CKB 如何找到相应的脚本代码。
        解析CKB交易结构 | 技术帖

        如果你想使用 CKB 中的脚本,那么应该遵循代码定位的规则:

        • 把你的代码编译成 RISC-V 二进制文件。你可以在建构系统 Cell 代码的仓库中找到一些案例:

          https://github.com/nervosnetwork/ckb-system-scripts

        • 通过一笔交易,创建一个将二进制文件作为数据的 cell,并将交易发到链上。

        • 建构一个脚本结构,其 hash_type 是「数据」,code_hash 只是

          构建二进制文件的哈希

        • 使用脚本作为 Cell 中的一种形态或锁定脚本。
        • 如果脚本必须在交易中运行,请包含指向 cell_deps 的代码 Cell。

        在 cell_deps 的 Cell 必定是可用的,就像是 inputs 一样。但有别于 inputs,一个只在 cell_deps 中被使用的 Cell 不会被认为是被销毁的。

        下面两个章节我们将讨论脚本如何在交易中用于锁定 Cell,以及如何建立 Cell 上的合约。

        锁定脚本

        每个 Cell 都有一个锁定脚本。当交易中的 Cell 被以输入的形式使用时,锁定脚本必须执行。当脚本只出现在输出时, 则不需要显示在 cell_deps 中相应的代码中。交易只有在所有的输入中锁定脚本都正常(执行并)退出时才有效。因为脚本在输入上运行,所以它可以扮演锁的角色来控制谁可以解锁或者销毁 Cell,以及花费储存在 Cell 中的容量。

        解析CKB交易结构 | 技术帖

        以下是一个总是可以正常(执行并)退出的锁脚本的代码示例。如果使用这段代码作为锁脚本,那么任何人都可以销毁这个 Cell。

          int main(int argc, char *argv[]) {    return 0;}

          最主流的锁定数字资产的方式是用非对称加密创建的数字签名。

          这个签名演算法有两个要求:

          • Cell 必须要包含公钥的信息,所以只有真正的私钥可以创建有效的签名;
          • 交易必须包含签名,而且通常以整个交易作为消息去签名。

          在 CKB 中,公钥指纹可以在脚本结构的 args 字段中被存储,同时这个签名可以在交易的 witnesses 字段中被存储。我使用「可以」是因为这只是一个我推荐的方式,并且只用在默认的 secp256k1 锁定脚本中:https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/secp256k1_blake160_sighash_all.c
          脚本代码可以读取交易的任何一部分。所以锁定脚本可以选择不同的协定,例如,储存公钥的信息在 Cell 数据中。然而,如果所有锁定脚本都跟随推荐的协定,他就可以简化创建交易的应用程序,像是钱包。
          解析CKB交易结构 | 技术帖

          让我们看一下脚本代码是如何被定位和载入的,以及代码如何访问输入、脚本参数(script args)和 witnesses。

          首先,请注意 CKB 并不会在逐个输入之间运行锁定脚本。首先它会先按锁定脚本进行分组,并且只运行一次相同的锁定脚本。CKB 会按照这三个步骤运行:脚本分组(script grouping),代码定位(code locating)以及运行

          解析CKB交易结构 | 技术帖
          上图展示了第一步中的两个步骤:

          1. 首先,CKB 会借由锁定脚本去进行分组。在示例的交易里有两个不同的锁定脚本在输入中被使用。虽然它们都定位在相同的代码中,但它们有不同的 args。我们来看一下 g1。这里有两个索引为 0 和 2 的输入。脚本和输入索引会在步骤三后被使用。
          2. 然后 CKB 会从 cell deps 上查找代码。这将 Cell 解析成带有数据的哈希 Hs,并且将会用此数据作为代码。

          目前 CKB 可以载入脚本代码的二进制文件并通过 entry 函数开始运行代码。脚本可以通过 syscall 的 ckb_load_script 进行自读取:

            ckb_load_script(addr, len, offset)

          许多 CKB 的 syscall 都被设计为从交易中读取数据。这些 syscall 用有一个指明哪里可以读取数据的参数。例如,载入第一个 witness:

            ckb_load_witness(addr, len, offset, 0, CKB_SOURCE_INPUT);

            第一个参数控制哪里可以存储读取的数据,以及有多少 byte 已被读取。在接下来的章节中我们先忽略它。

            第五个参数是数据源。CKB_SOURCE_INPUT 代表从交易输入中读取,第四个参数 0 则是输入数组的索引。CKB_SOURCE_INPUT 也用于读取 witnesses

            记住当我们通过锁脚本将输入进行分组时,我们已经保存了输入的索引。这个信息用于为分组创建虚拟的 witness 和输入数组。这段代码可以通过一个特殊的数据源 CKB_SOURCE_GROUP_INPUT利用虚拟数组中的索引去读取输入或 witness。读取 witness 时,通过 CKB_SOURCE_GROUP_INPUT 只读取拥有相同位置并具有特定输入的 witness。

            解析CKB交易结构 | 技术帖

            所有读取和输入相关数据的 syscall,都可以使用 CKB_SOURCE_GROUP_INPUT 以及在虚拟输入数组中的索引,例如 ckb_load_cell_* 的 syscall 系列。

            类型脚本

            类型脚本和锁定脚本很相似,但有两点不同:

            • 类型脚本是可选的;
            • 在任一交易中,CKB 必须在输入和输出端都运行类型脚本。

            虽然我们只能在 Cell 中维持一种脚本,但我们不会想要在一个单一的脚本中扰乱(脚本)不同的职责。

            锁定脚本只为输出执行,所以他的首要任务是保护 Cell。只有所有者可以以输入的形式使用 Cell,以及花费储存于其中的通证。

            类型脚本的目的是在 Cell 上建立合约。当你得到一个特殊类型的合约时,你可以确定 Cell 已经在特定代码中通过验证。同时这个代码也会在 Cell 被销毁时被执行。类型脚本的典型情况是用户自定义的 Token,这种类型脚本必须在输出上运行,所以通证的发行必须被授权。

            在输入上运行类型脚本对合约而言非常重要;例如一个让用户可以在线下抵押 CKB 来租用资产的合约,如果这个类型脚本不在输入上运行,用户可以在没有权限的情况下从合约中取回 CKB,只需销毁这个 Cell 并将容量转移到一个没有类型脚本的新 Cell 上即可。

            这个运行类型脚本的步骤和锁定脚本也很相似,除了:

            1. 没有类型脚本的 Cell 会被忽略
            2. 脚本群组包含输入与输出

            解析CKB交易结构 | 技术帖

            像 CKB_SOURCE_GROUP_INPUT,有一个特殊的数据源 CKB_SOURCE_GROUP_OUTPUT 可以将索引用于脚本组的虚拟输出数组中。

            PART I 交易结构概述


            解析CKB交易结构 | 技术帖

            Part II:扩展

            在第一部分,我介绍了交易提供的核心特征。在这个部分,我将介绍一些 CKB 不需要他们也能运行的延伸套件的特征,但这些套件可以使 Cell 模型变的更好。

            下图是在这个部分会出现的新字段(标有黄色底)的总览。
            解析CKB交易结构 | 技术帖

            Dep Group

            Dep Group 是一个捆绑许多 Cell 作为其成员的 Cell。当一个 dep group 的 Cell 在 cell_deps 中被使用时,它的效果和添加全部的成员到 cell_deps 中是一样的。
            Dep Group 在 Cell 数据中存储了一系列的 OutPoint 清单。每一个 OutPoint 都指向其中一个群体的成员。

            CellDep 结构中有个叫 dep_type 的字段,可以用于区分直接提供代码的普通 Cell,和在 cell_deps 中扩展至其成员的 dep group。解析CKB交易结构 | 技术帖

            Dep group 会在定位和运行节点之前被扩展,只有被扩展的 cell_deps 才是可见的。

            解析CKB交易结构 | 技术帖

            在 v0.19.0 版中,锁定脚本 secp256k1 被分成 code cell 和 data cell。code cell 通过 cell_deps 载入 data cell。所以如果一个交易要解锁一个被 secp256k1 锁定的 Cell,那么他一定要在 cell_deps 加上两个 Cell。在 dep group 中,交易就只需要 dep group 即可。

            我们分离 secp256k1 cell 有两个原因。

            • code cell 很小,这让我们可以在区块大小限制很低的时候就更新它。
            • data cell 可以被共享。例如,我们已经安装了另一个使用 ripemd160 的锁定脚本来验证公钥的哈希值。这个脚本就重用了 data cell。

            可升级脚本

            在第一部分的锁定脚本中,我描述了一个脚本如何通过 Cell 的数据哈希来定位它的代码。一旦一个 Cell 被创建,那么相关联的脚本代码就不会被改变,因为要找到一个有相同的哈希的另一段代码是不可能的。

            脚本有另一个 hash_type 的选项,Type

            解析CKB交易结构 | 技术帖

            当脚本使用了 hash type 的 Type,它会匹配相等于 code_hash 的类型脚本哈希的 Cell。类型脚本的哈希是从 Cell 的 type 字段中计算出来的(详见附录 A)。

            解析CKB交易结构 | 技术帖

            现在,如果 Cell 用一个通过类型脚本哈希来定位代码的脚本,并通过创建一个具有相同类型脚本的新 Cell,那么我们就可以升级代码了。新的 Cell 已经有更新的脚本,在 dep_cells 中增加了新 Cell 的交易将会使用这个新的版本。

            然而,这只解决了一个问题。如果一个作恶者创建一个拥有同种类型脚本的 Cell,但使用伪造的代码作为数据,那么这还是不安全的。作恶者可以通过使用假的 Cell 作为 dep 来绕开脚本验证。下一章将描述解决第二个问题的脚本。

            因为被类型脚本哈希引用的代码是可以被改变的,所以你必须信任脚本作者使用的这种类型脚本。虽然使用哪个版本取决于哪一个 Cell 在 dep_cells 的交易中被添加。用户总是可以在签署交易之前检查代码。但是,如果脚本用于解锁 Cell,那么签名检查甚至是可以被略过的。

            Type ID

            我们选择 Cell 类型脚本哈希的理由是用来支持可更新的脚本。如果作恶者想要创建具有特化的形态脚本的 Cell,那么交易必须被类型脚本的代码验证。

            Type ID 就是这种类型的类型脚本。如其名所示,他确保了类型脚本的独特性。

            这个特征包括了多种类型脚本,所以我会用不同专有名词去区分他们:

            • Type ID 的代码 Cell 是一个存储代码来验证 type id 是否是独一无二的 Cell。
            • Type ID 的代码一样有类型脚本。我们现在不会在意实际的内容,让我们假设类型脚本的哈希是 T1
            • Type ID 是一个 hash_type 是 Type,且 code_hash 是 T1 的类型脚本。

            解析CKB交易结构 | 技术帖

            从 Part I 的类型脚本中我们知道,类型脚本会先将输入与输出聚集。换句话说,如果一个类型脚本是 type ID,那么在同个群组的输入与输出都有同样的 type ID。

            Type ID 的代码在验证的就是在任何代码中,至少都会有一组输出和一组输入。根据输入与输出的数量,一个交易允许有多种 type id 群组,type id 群组被分成以下三种不同的类型:

            • Type ID Creation Group 只有一个输出
            • Type ID Deletion Group 只有一个输入
            • Type ID Transfer Group 有一个输入和一个输出

            解析CKB交易结构 | 技术帖

            上图的交易中有三种交易 ID 群组

            • G1 是将 type id 从 cell1 转移到 cell4 的 Type ID Transfer Group
            • G2 是依据 cell2 去删除 type id 的 Type ID Deletion Group
            • G3 是为 cell3 创建新的 type id 的 Type ID Creation Group

            在 Type ID Creation Group 中,在 args 中的唯一参数,是在群组中这个交易的第一个 CellInput 结构的哈希,还有 Cell 的输出索引。例如,在群组 g3 中,id3 是一个在 tx.inputs[0] 和 0(cell3 在 tx.outputs 的索引)上的哈希。

            这里有两种可以透过特定的 type id 来创建新 Cell 的方法:

            1. 在 tx.inputs[0] 的哈希上创建一个交易以及任意等于特定值的索引。因为一个 Cell 只能被以输出的形式在链上被使用一次,所以 tx.inputs[0]在每个交易中必须是不同的。这个问题和找到一个哈希碰撞的值是一样的,可能性几乎可以忽略不计。
            2. 在同一个交易中销毁较早的 Cell

            我们假设方法 2 是唯一一个创建相等于既有 type id 的新 Cell 的方法。这个方法需要原有的所有者的授权。

            Type ID 的代码只能通过 CKB-VM 的代码实行,但我们会选择在 CKB 的节点上以一个特殊的系统脚本来实行它,因为如果我们以后想升级 Type ID 的代码,就必须要通过一个递归依赖的类型脚本代码,来将自己作为类型脚本。
            解析CKB交易结构 | 技术帖

            Type ID 的代码 Cell 使用了一个特殊的类型脚本哈希,也就是文本 TYPE_ID

              0x00000000000000000000000000000000000000000000000000545950455f4944

              Header Deps

              Header Deps 可以让脚本来读取区块头。这个功能有一定的限制以确保交易被确定。

              我们只会在所有交易中的脚本已经有确定的结果时,才会说一笔交易已经确定了。

              Header Deps 允许脚本去读取其哈希已经列在 header_deps 中的区块头。还有另一个先决条件,是交易只能在所有列在 header_deps 的区块都已经在链上时(叔块除外),才可以被添加到链上。

              有两种方式可以利用 ckb_load_header 系统调用来载入脚本中的区块头:

              • 通过 header deps 的索引
              • 通过输入或一个 cell dep。如果区块有被列在 header_deps 的话,系统调用会返回 Cell 被创建出的那个区块。

              第二种载入区块头的方法有另一个好处是,脚本会知道 Cell 位于被载入的区块中。DAO 取出交易借此来获取存储容量的区块号。
              解析CKB交易结构 | 技术帖

              下面是上图中 loading header 的一些例子。

                // Load the first block in header_deps, the block Hiload_header(..., 0, CKB_SOURCE_HEADER_DEP);
                // Load the second block in header_deps, the block Hjload_header(..., 1, CKB_SOURCE_HEADER_DEP);
                // Load the block in which the first input is created, the block Hiload_header(..., 0, CKB_SOURCE_INPUT);
                // Load the block in which the second input is created.// Since the block creating cell2 is not in header_deps, this call loads nothing.load_header(..., 1, CKB_SOURCE_INPUT);

                其他字段

                字段 since 可以避免交易在特定时间前被挖出。详见 since RFC。https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0017-tx-valid-since/0017-tx-valid-since.md
                字段 version  是预留在未来使用的。在当前版本中,它必须等于0。

                特例

                在此系统中有两个特殊的交易。

                第一个是 cellbase,这是每个区块中的第一个交易。cellbase 中只有一个仿真的输入。在这个仿真的交易中,previous_outpoint 不会关联人其他的 Cell,而是会设置一个特殊值。since 必须设置为区块编号。

                cellbase 的输出是链上给早区块的奖励和交易费。

                cellbase 是特殊的,因为他的输出容量并不来自于输出。

                另一个特殊的交易是 DAO 的取款交易。它也有部分的输出容量并非来自于输入。这部分是在 DAO 中锁定 Cell 所获得的收益。CKB 会通过检查是否有使用 DAO 作为类型脚本的输入来识别 DAO 的取款交易。

                附录A:计算多种哈希

                加密原语

                blake256

                CKB 使用 blake2b 作为默认的哈希演算法。我们用 blake256 来表示一个函数,用个人的「ckb-default-hash」来获取 blake2b 哈希的前 256 位。

                交易哈希

                交易哈希是 blake256(tx_hash_digest(tx)) 其中 tx_hash_digest 是将交易中的所有字段(不包括  witnesses )序列化为二进制的方法。序列化规范还没有进行最终的确定,现在,您可以通过 RPC _compute_transaction_hash 获取交易哈希。

                Cell 数据哈希

                cell 的数据哈希只是 blake256(data)。

                脚本哈希

                脚本哈希是 blake256(serialize(script))serialize 将脚本结构转换成二进制块。序列化规范还没有进行最终的确定,现在你可以通过 RPC _compute_script_hash 来获取脚本哈希。

                转载声明:本文 由CoinON抓取收录,观点仅代表作者本人,不代表CoinON资讯立场,CoinON不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。若以此作为投资依据,请自行承担全部责任。

                声明:图文来源于网络,如有侵权请联系删除

                风险提示:投资有风险,入市需谨慎。本资讯不作为投资理财建议。

                (0)
                打赏 微信扫一扫 微信扫一扫
                上一篇 2019年12月25日 下午9:21
                下一篇 2019年12月25日 下午9:23

                相关推荐

                解析CKB交易结构 | 技术帖

                星期三 2019-12-25 21:21:23

                作者:Ian,Nervos 工程师,专注于系统设计和客户端实现,前 Hooya Game CTO,前 Groupon 软件工程师。原文链接:https://github.com/nervosnetwork/rfcs/blob/transaction-structure/rfcs/0022-transaction-structure/0022-transaction-structure.md
                翻译 & 校对:Williams & Kelly

                这篇文章分成两个部分。第一个部分包含了核心的交易特征,而第二部分介绍一些扩展内容。在撰写本文时,对应的 CKB 版本是 v0.25.0,在未来的版本中交易结构还可能有所变动。您可以点击阅读原文,查看最新版本。

                解析CKB交易结构 | 技术帖

                上图是关于交易结构的概览。有别于逐字逐句的解释个名词,我将会介绍 CKB 转账能够提供的各种特殊结构,以及这些名词在其中的具体意思。

                Part I:核心特征

                价值储存

                CKB 采用的是 UTXO 模型。一笔交易销毁了一些在先前交易下创建的输出(作为输入),并且创建一些新的输出,我们在 CKB 中将此交易输出称做一个 Cell。因此在这里的 Cell 和交易输出是可以替换的。

                下图显示了在此层中会出现的专有名词。

                解析CKB交易结构 | 技术帖
                此交易销毁了 inputs 中的 Cell,同时创建了在 outputs 中的 Cell。

                CKB 主链将交易打包成块。我们可以在区块中利用从零(也就是创世区块)开始递增的非负整数(编号),作为区块编号来关联链上的区块。在区块中的交易也是按照顺序排列的。我们可以说编号较小的区块是较早(old)的区块,如果一个交易在较早的区块上,或者它所在的区块的位置早于其它区块,那么它也会是比较早的交易。在下面的示例中,区块 i 比区块 i+1 早。交易 tx1 要比 tx2 早,也比 tx3 早。

                解析CKB交易结构 | 技术帖

                在所有先前的交易中,一个可用(Live)的 Cell 会以输出而非输入的形式出现。一个被销毁(Dead)的 Cell 代表它是以输入的形式在其它较早的交易中被使用过。一个交易只能以可用的 Cell 作为输入。

                我们可以从除了 witnesses 之外的所有交易字段计算交易的哈希。关于如何计算交易哈希的更多信息,可以参阅附录 A。

                交易哈希是独一无二的。因为一个 Cell 总是被一个交易创建出来,而每个新的 Cell 在交易输出的数组中都有他自己的位置,所以我们可以通过交易哈希以及输出索引去指向一个 Cell。OutPoint 结构是一种引用类型。交易在输入时会使用 OutPoint 来指向先前被创建的 Cell,而非嵌入其中。

                解析CKB交易结构 | 技术帖

                Cell 将 CKB 代币存储在字段 capacity 中。一个交易不能够凭空铸造 capacity,所以交易必将符合以下规则:

                  sum(cell's capacity for each cell in inputs)≥ sum(cell's capacity for each cell in outputs)

                  在输入中每个 Cell 容量的总和要大于等于在输出中每个 Cell 容量的总和。

                  矿工可以收取这两者之间的价差做为手续费。

                    fee = sum(cell's capacity for each cell in inputs)- sum(cell's capacity for each cell in outputs)

                    如果你熟悉比特币,那么就会发现在价值储存层都是相似的,但是比特币缺乏锁定脚本来保护交易输出的所有权。CKB 正好有这个特征,但是在我们探讨这个话题之前,让我们先来谈谈 Cell Data 和 Code Locating 层吧,这是任何 CKB 中脚本特征的依据。

                    Cell Data

                    除了能够存储价值通证以外,CKB Cell 还能储存任意数据。
                    解析CKB交易结构 | 技术帖
                    字段 outputs_data 是输出的并行数组。在 outputs 中第 i 个 Cell 的数据对应的是 outputs_data 中的第 i 项。

                    解析CKB交易结构 | 技术帖Cell 中的 capacity 不只代表通证的数量,也代表能够存储数据的限制。这也是它如此命名的原因,因为它代表 Cell 的存储容量。

                    capacity 不仅能用于存储数据,它还需要涵盖 Cell 中的所有字段,包括  data、 locktype 以及 capacity 本身。

                    计算占用容量的规范请参考:

                    https://github.com/nervosnetwork/ckb/wiki/Occupied-Capacity

                    交易势必会创建一个占用容量小于(输入) Cell 容量的输出 Cell。

                      occupied(cell) ≤ cell's capacity

                      代码定位

                      Cell 中有两个字段的类型是 Script。CKB-VM 会运行所有输入 Cell 中的 lock 脚本,还会运行所有输入和输出 Cell 中的 type 脚本。

                      我们区分了代码和脚本这两种术语:

                      • 脚本具有脚本结构
                      • 代码是 RISC-V (可运行的)二进制
                      • 一个代码 Cell 是其数据为代码的 Cell

                      脚本并没有直接包含代码。看看下面的脚本结构。现在我们可以忽略哈希类型的 Type 以及 args 字段。

                      解析CKB交易结构 | 技术帖

                      当 CKB-VM 需要运行一个脚本时,它必须要先找到它的代码。字段 code_hash 和 hash_type 就是用来查看代码的。

                      在 CKB 中,脚本代码会被编译成 RISC-V 二进制文件。这个二进制文件是以数据的形式存储在 Cell 中的。当 hash_type 是数据时,脚本会被定位在一个数据哈希和脚本的 code_hash 相等的 Cell 中。Cell 数据哈希,如其名所示,是从 Cell 的数据中算出来的(详见附录 A)。在交易中的范围是有限制的,脚本只能从 cell_deps 中找到一个匹配的 Cell。

                      解析CKB交易结构 | 技术帖

                      下图将解释 CKB 如何找到相应的脚本代码。
                      解析CKB交易结构 | 技术帖

                      如果你想使用 CKB 中的脚本,那么应该遵循代码定位的规则:

                      • 把你的代码编译成 RISC-V 二进制文件。你可以在建构系统 Cell 代码的仓库中找到一些案例:

                        https://github.com/nervosnetwork/ckb-system-scripts

                      • 通过一笔交易,创建一个将二进制文件作为数据的 cell,并将交易发到链上。

                      • 建构一个脚本结构,其 hash_type 是「数据」,code_hash 只是

                        构建二进制文件的哈希

                      • 使用脚本作为 Cell 中的一种形态或锁定脚本。
                      • 如果脚本必须在交易中运行,请包含指向 cell_deps 的代码 Cell。

                      在 cell_deps 的 Cell 必定是可用的,就像是 inputs 一样。但有别于 inputs,一个只在 cell_deps 中被使用的 Cell 不会被认为是被销毁的。

                      下面两个章节我们将讨论脚本如何在交易中用于锁定 Cell,以及如何建立 Cell 上的合约。

                      锁定脚本

                      每个 Cell 都有一个锁定脚本。当交易中的 Cell 被以输入的形式使用时,锁定脚本必须执行。当脚本只出现在输出时, 则不需要显示在 cell_deps 中相应的代码中。交易只有在所有的输入中锁定脚本都正常(执行并)退出时才有效。因为脚本在输入上运行,所以它可以扮演锁的角色来控制谁可以解锁或者销毁 Cell,以及花费储存在 Cell 中的容量。

                      解析CKB交易结构 | 技术帖

                      以下是一个总是可以正常(执行并)退出的锁脚本的代码示例。如果使用这段代码作为锁脚本,那么任何人都可以销毁这个 Cell。

                        int main(int argc, char *argv[]) {    return 0;}

                        最主流的锁定数字资产的方式是用非对称加密创建的数字签名。

                        这个签名演算法有两个要求:

                        • Cell 必须要包含公钥的信息,所以只有真正的私钥可以创建有效的签名;
                        • 交易必须包含签名,而且通常以整个交易作为消息去签名。

                        在 CKB 中,公钥指纹可以在脚本结构的 args 字段中被存储,同时这个签名可以在交易的 witnesses 字段中被存储。我使用「可以」是因为这只是一个我推荐的方式,并且只用在默认的 secp256k1 锁定脚本中:https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/secp256k1_blake160_sighash_all.c
                        脚本代码可以读取交易的任何一部分。所以锁定脚本可以选择不同的协定,例如,储存公钥的信息在 Cell 数据中。然而,如果所有锁定脚本都跟随推荐的协定,他就可以简化创建交易的应用程序,像是钱包。
                        解析CKB交易结构 | 技术帖

                        让我们看一下脚本代码是如何被定位和载入的,以及代码如何访问输入、脚本参数(script args)和 witnesses。

                        首先,请注意 CKB 并不会在逐个输入之间运行锁定脚本。首先它会先按锁定脚本进行分组,并且只运行一次相同的锁定脚本。CKB 会按照这三个步骤运行:脚本分组(script grouping),代码定位(code locating)以及运行

                        解析CKB交易结构 | 技术帖
                        上图展示了第一步中的两个步骤:

                        1. 首先,CKB 会借由锁定脚本去进行分组。在示例的交易里有两个不同的锁定脚本在输入中被使用。虽然它们都定位在相同的代码中,但它们有不同的 args。我们来看一下 g1。这里有两个索引为 0 和 2 的输入。脚本和输入索引会在步骤三后被使用。
                        2. 然后 CKB 会从 cell deps 上查找代码。这将 Cell 解析成带有数据的哈希 Hs,并且将会用此数据作为代码。

                        目前 CKB 可以载入脚本代码的二进制文件并通过 entry 函数开始运行代码。脚本可以通过 syscall 的 ckb_load_script 进行自读取:

                          ckb_load_script(addr, len, offset)

                        许多 CKB 的 syscall 都被设计为从交易中读取数据。这些 syscall 用有一个指明哪里可以读取数据的参数。例如,载入第一个 witness:

                          ckb_load_witness(addr, len, offset, 0, CKB_SOURCE_INPUT);

                          第一个参数控制哪里可以存储读取的数据,以及有多少 byte 已被读取。在接下来的章节中我们先忽略它。

                          第五个参数是数据源。CKB_SOURCE_INPUT 代表从交易输入中读取,第四个参数 0 则是输入数组的索引。CKB_SOURCE_INPUT 也用于读取 witnesses

                          记住当我们通过锁脚本将输入进行分组时,我们已经保存了输入的索引。这个信息用于为分组创建虚拟的 witness 和输入数组。这段代码可以通过一个特殊的数据源 CKB_SOURCE_GROUP_INPUT利用虚拟数组中的索引去读取输入或 witness。读取 witness 时,通过 CKB_SOURCE_GROUP_INPUT 只读取拥有相同位置并具有特定输入的 witness。

                          解析CKB交易结构 | 技术帖

                          所有读取和输入相关数据的 syscall,都可以使用 CKB_SOURCE_GROUP_INPUT 以及在虚拟输入数组中的索引,例如 ckb_load_cell_* 的 syscall 系列。

                          类型脚本

                          类型脚本和锁定脚本很相似,但有两点不同:

                          • 类型脚本是可选的;
                          • 在任一交易中,CKB 必须在输入和输出端都运行类型脚本。

                          虽然我们只能在 Cell 中维持一种脚本,但我们不会想要在一个单一的脚本中扰乱(脚本)不同的职责。

                          锁定脚本只为输出执行,所以他的首要任务是保护 Cell。只有所有者可以以输入的形式使用 Cell,以及花费储存于其中的通证。

                          类型脚本的目的是在 Cell 上建立合约。当你得到一个特殊类型的合约时,你可以确定 Cell 已经在特定代码中通过验证。同时这个代码也会在 Cell 被销毁时被执行。类型脚本的典型情况是用户自定义的 Token,这种类型脚本必须在输出上运行,所以通证的发行必须被授权。

                          在输入上运行类型脚本对合约而言非常重要;例如一个让用户可以在线下抵押 CKB 来租用资产的合约,如果这个类型脚本不在输入上运行,用户可以在没有权限的情况下从合约中取回 CKB,只需销毁这个 Cell 并将容量转移到一个没有类型脚本的新 Cell 上即可。

                          这个运行类型脚本的步骤和锁定脚本也很相似,除了:

                          1. 没有类型脚本的 Cell 会被忽略
                          2. 脚本群组包含输入与输出

                          解析CKB交易结构 | 技术帖

                          像 CKB_SOURCE_GROUP_INPUT,有一个特殊的数据源 CKB_SOURCE_GROUP_OUTPUT 可以将索引用于脚本组的虚拟输出数组中。

                          PART I 交易结构概述


                          解析CKB交易结构 | 技术帖

                          Part II:扩展

                          在第一部分,我介绍了交易提供的核心特征。在这个部分,我将介绍一些 CKB 不需要他们也能运行的延伸套件的特征,但这些套件可以使 Cell 模型变的更好。

                          下图是在这个部分会出现的新字段(标有黄色底)的总览。
                          解析CKB交易结构 | 技术帖

                          Dep Group

                          Dep Group 是一个捆绑许多 Cell 作为其成员的 Cell。当一个 dep group 的 Cell 在 cell_deps 中被使用时,它的效果和添加全部的成员到 cell_deps 中是一样的。
                          Dep Group 在 Cell 数据中存储了一系列的 OutPoint 清单。每一个 OutPoint 都指向其中一个群体的成员。

                          CellDep 结构中有个叫 dep_type 的字段,可以用于区分直接提供代码的普通 Cell,和在 cell_deps 中扩展至其成员的 dep group。解析CKB交易结构 | 技术帖

                          Dep group 会在定位和运行节点之前被扩展,只有被扩展的 cell_deps 才是可见的。

                          解析CKB交易结构 | 技术帖

                          在 v0.19.0 版中,锁定脚本 secp256k1 被分成 code cell 和 data cell。code cell 通过 cell_deps 载入 data cell。所以如果一个交易要解锁一个被 secp256k1 锁定的 Cell,那么他一定要在 cell_deps 加上两个 Cell。在 dep group 中,交易就只需要 dep group 即可。

                          我们分离 secp256k1 cell 有两个原因。

                          • code cell 很小,这让我们可以在区块大小限制很低的时候就更新它。
                          • data cell 可以被共享。例如,我们已经安装了另一个使用 ripemd160 的锁定脚本来验证公钥的哈希值。这个脚本就重用了 data cell。

                          可升级脚本

                          在第一部分的锁定脚本中,我描述了一个脚本如何通过 Cell 的数据哈希来定位它的代码。一旦一个 Cell 被创建,那么相关联的脚本代码就不会被改变,因为要找到一个有相同的哈希的另一段代码是不可能的。

                          脚本有另一个 hash_type 的选项,Type

                          解析CKB交易结构 | 技术帖

                          当脚本使用了 hash type 的 Type,它会匹配相等于 code_hash 的类型脚本哈希的 Cell。类型脚本的哈希是从 Cell 的 type 字段中计算出来的(详见附录 A)。

                          解析CKB交易结构 | 技术帖

                          现在,如果 Cell 用一个通过类型脚本哈希来定位代码的脚本,并通过创建一个具有相同类型脚本的新 Cell,那么我们就可以升级代码了。新的 Cell 已经有更新的脚本,在 dep_cells 中增加了新 Cell 的交易将会使用这个新的版本。

                          然而,这只解决了一个问题。如果一个作恶者创建一个拥有同种类型脚本的 Cell,但使用伪造的代码作为数据,那么这还是不安全的。作恶者可以通过使用假的 Cell 作为 dep 来绕开脚本验证。下一章将描述解决第二个问题的脚本。

                          因为被类型脚本哈希引用的代码是可以被改变的,所以你必须信任脚本作者使用的这种类型脚本。虽然使用哪个版本取决于哪一个 Cell 在 dep_cells 的交易中被添加。用户总是可以在签署交易之前检查代码。但是,如果脚本用于解锁 Cell,那么签名检查甚至是可以被略过的。

                          Type ID

                          我们选择 Cell 类型脚本哈希的理由是用来支持可更新的脚本。如果作恶者想要创建具有特化的形态脚本的 Cell,那么交易必须被类型脚本的代码验证。

                          Type ID 就是这种类型的类型脚本。如其名所示,他确保了类型脚本的独特性。

                          这个特征包括了多种类型脚本,所以我会用不同专有名词去区分他们:

                          • Type ID 的代码 Cell 是一个存储代码来验证 type id 是否是独一无二的 Cell。
                          • Type ID 的代码一样有类型脚本。我们现在不会在意实际的内容,让我们假设类型脚本的哈希是 T1
                          • Type ID 是一个 hash_type 是 Type,且 code_hash 是 T1 的类型脚本。

                          解析CKB交易结构 | 技术帖

                          从 Part I 的类型脚本中我们知道,类型脚本会先将输入与输出聚集。换句话说,如果一个类型脚本是 type ID,那么在同个群组的输入与输出都有同样的 type ID。

                          Type ID 的代码在验证的就是在任何代码中,至少都会有一组输出和一组输入。根据输入与输出的数量,一个交易允许有多种 type id 群组,type id 群组被分成以下三种不同的类型:

                          • Type ID Creation Group 只有一个输出
                          • Type ID Deletion Group 只有一个输入
                          • Type ID Transfer Group 有一个输入和一个输出

                          解析CKB交易结构 | 技术帖

                          上图的交易中有三种交易 ID 群组

                          • G1 是将 type id 从 cell1 转移到 cell4 的 Type ID Transfer Group
                          • G2 是依据 cell2 去删除 type id 的 Type ID Deletion Group
                          • G3 是为 cell3 创建新的 type id 的 Type ID Creation Group

                          在 Type ID Creation Group 中,在 args 中的唯一参数,是在群组中这个交易的第一个 CellInput 结构的哈希,还有 Cell 的输出索引。例如,在群组 g3 中,id3 是一个在 tx.inputs[0] 和 0(cell3 在 tx.outputs 的索引)上的哈希。

                          这里有两种可以透过特定的 type id 来创建新 Cell 的方法:

                          1. 在 tx.inputs[0] 的哈希上创建一个交易以及任意等于特定值的索引。因为一个 Cell 只能被以输出的形式在链上被使用一次,所以 tx.inputs[0]在每个交易中必须是不同的。这个问题和找到一个哈希碰撞的值是一样的,可能性几乎可以忽略不计。
                          2. 在同一个交易中销毁较早的 Cell

                          我们假设方法 2 是唯一一个创建相等于既有 type id 的新 Cell 的方法。这个方法需要原有的所有者的授权。

                          Type ID 的代码只能通过 CKB-VM 的代码实行,但我们会选择在 CKB 的节点上以一个特殊的系统脚本来实行它,因为如果我们以后想升级 Type ID 的代码,就必须要通过一个递归依赖的类型脚本代码,来将自己作为类型脚本。
                          解析CKB交易结构 | 技术帖

                          Type ID 的代码 Cell 使用了一个特殊的类型脚本哈希,也就是文本 TYPE_ID

                            0x00000000000000000000000000000000000000000000000000545950455f4944

                            Header Deps

                            Header Deps 可以让脚本来读取区块头。这个功能有一定的限制以确保交易被确定。

                            我们只会在所有交易中的脚本已经有确定的结果时,才会说一笔交易已经确定了。

                            Header Deps 允许脚本去读取其哈希已经列在 header_deps 中的区块头。还有另一个先决条件,是交易只能在所有列在 header_deps 的区块都已经在链上时(叔块除外),才可以被添加到链上。

                            有两种方式可以利用 ckb_load_header 系统调用来载入脚本中的区块头:

                            • 通过 header deps 的索引
                            • 通过输入或一个 cell dep。如果区块有被列在 header_deps 的话,系统调用会返回 Cell 被创建出的那个区块。

                            第二种载入区块头的方法有另一个好处是,脚本会知道 Cell 位于被载入的区块中。DAO 取出交易借此来获取存储容量的区块号。
                            解析CKB交易结构 | 技术帖

                            下面是上图中 loading header 的一些例子。

                              // Load the first block in header_deps, the block Hiload_header(..., 0, CKB_SOURCE_HEADER_DEP);
                              // Load the second block in header_deps, the block Hjload_header(..., 1, CKB_SOURCE_HEADER_DEP);
                              // Load the block in which the first input is created, the block Hiload_header(..., 0, CKB_SOURCE_INPUT);
                              // Load the block in which the second input is created.// Since the block creating cell2 is not in header_deps, this call loads nothing.load_header(..., 1, CKB_SOURCE_INPUT);

                              其他字段

                              字段 since 可以避免交易在特定时间前被挖出。详见 since RFC。https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0017-tx-valid-since/0017-tx-valid-since.md
                              字段 version  是预留在未来使用的。在当前版本中,它必须等于0。

                              特例

                              在此系统中有两个特殊的交易。

                              第一个是 cellbase,这是每个区块中的第一个交易。cellbase 中只有一个仿真的输入。在这个仿真的交易中,previous_outpoint 不会关联人其他的 Cell,而是会设置一个特殊值。since 必须设置为区块编号。

                              cellbase 的输出是链上给早区块的奖励和交易费。

                              cellbase 是特殊的,因为他的输出容量并不来自于输出。

                              另一个特殊的交易是 DAO 的取款交易。它也有部分的输出容量并非来自于输入。这部分是在 DAO 中锁定 Cell 所获得的收益。CKB 会通过检查是否有使用 DAO 作为类型脚本的输入来识别 DAO 的取款交易。

                              附录A:计算多种哈希

                              加密原语

                              blake256

                              CKB 使用 blake2b 作为默认的哈希演算法。我们用 blake256 来表示一个函数,用个人的「ckb-default-hash」来获取 blake2b 哈希的前 256 位。

                              交易哈希

                              交易哈希是 blake256(tx_hash_digest(tx)) 其中 tx_hash_digest 是将交易中的所有字段(不包括  witnesses )序列化为二进制的方法。序列化规范还没有进行最终的确定,现在,您可以通过 RPC _compute_transaction_hash 获取交易哈希。

                              Cell 数据哈希

                              cell 的数据哈希只是 blake256(data)。

                              脚本哈希

                              脚本哈希是 blake256(serialize(script))serialize 将脚本结构转换成二进制块。序列化规范还没有进行最终的确定,现在你可以通过 RPC _compute_script_hash 来获取脚本哈希。