{"content":{"title":"以太坊 - Plasma Group的Plasma Spec","body":"_TLDR：我们创建了一个 Plasma Cash 变种的规范，并在 Node.js 和 Vyper 中实现了它。本文涵盖了设计规范，同时提供了实施过程中的参考。我们的代码支持在测试网部署新链、其他 Plasma 链及其区块探测器的链上注册，以及通过命令行钱包进行交易。_\r\n\r\n## 引言\r\n\r\n区块链网络作为可扩展解决方案的愿景正在迅速传播。通过多链方法来并行处理交易是提高吞吐量的一种有希望的方法……但不幸的是，这也带来了重大挑战：\r\n\r\n- 我们不想分散安全性，例如100条链每条只有1%的总安全性。\r\n- 像 sharding 这样的先进解决方案很有前景，但尚未成熟。\r\n\r\n我们需要一个可扩展性的解决方案：\r\n\r\n- 提供与 Ethereum 主网相似的安全级别，而无需支付数百万美元的挖矿费用。\r\n- **可以在目前的 Ethereum 上实现。**\r\n\r\n我们相信，满足这些标准的最强候选者是一个链的网络，每条链通过 Plasma 框架连接到主网。\r\n\r\nPlasma 是一系列协议，允许个人轻松部署高吞吐量、安全的区块链。Ethereum 主链上的智能合约可以确保用户的资金安全，即使“plasma 链”完全恶意行为。这样便消除了像侧链那样需要可信锚定机制的必要。Plasma 链是非托管的，允许在不牺牲安全性的基础上优先考虑可扩展性。\r\n\r\n我们设想一个拥有多个 Plasma 链的未来，让用户可以选择交易的地方。因此，除了发布我们的 plasma 链实现外，我们还创建了一个 `PlasmaRegistry.vy`。该注册表允许新的链通过列出其 IP/DNS 地址、自定义的“名称”字符串和其合约地址来加入网络。注册表合约验证受信任的部署，因此用户可以放心将任何合约存入该注册表 —— **即使其运营者存在恶意行为**。\r\n\r\n## 我们 Plasma 链实现的特性\r\n\r\n这篇文章指定了 Plasma Group 当前的协议和实现，吸取了研究社区的最新进展。\r\n\r\n**我们的规范具有以下特性：**\r\n\r\n- 单个交易跨越大量Coin，解决了 Plasma Cash 中的 [“固定面额”问题](https://ethresear.ch/t/plasma-cash-was-a-transaction-format/4261)。\r\n- 块大小与交易数量而非存款数量成比例的扩展。\r\n- 轻客户端证明其扩展与块大小的对数成正比，与自存款以来的块数量线性扩展，使得操作员成为系统唯一的（计算）瓶颈。\r\n- 简化的乐观退出程序，允许退出仅指定最新交易，而不是 [同时指定交易及其父交易](https://ethresear.ch/t/plasma-cash-plasma-with-much-less-per-user-data-checking/1298)。\r\n- 跨链原子交换，为去中心化交易协议打下基础。\r\n- 无限存款能力。\r\n\r\n**我们的实现遵循上述规范，提供以下功能：**\r\n\r\n- 用 JavaScript 编写的命令行 [plasma 链操作员](https://github.com/plasma-group/plasma-chain-operator)。\r\n- 用 JavaScript 编写的 [plasma 客户端实现](https://github.com/plasma-group/plasma-core)，带有命令行钱包。\r\n- 用 Vyper 编写的支持 ETH 和 ERC20 代币的 [智能合约](https://github.com/plasma-group/plasma-contracts)。\r\n- 一个集成的 JSON RPC，允许客户端下载和验证 [轻客户端证明](https://github.com/plasma-group/plasma-node) 并进行交易。\r\n- 由 plasma 操作员托管的 [区块探测器](https://github.com/plasma-group/plasma-explorer)。\r\n- 一个 [模拟客户端群体](https://github.com/plasma-group/plasma-chain-operator)，生成交易以进行负载测试。\r\n- 一个 Plasma “注册表”合约，列出一组经过验证的安全合约和运营者 IP 地址，供用户探索。\r\n\r\n如果你对协议和代码实现感兴趣，你来对地方了！\r\n\r\n**然而，在深入探讨之前，有几点免责声明：**\r\n\r\n- 我们的 plasma 实现是 beta 软件，当前仅适用于测试网。**此时肯定存在关键错误。**\r\n- 该协议与其他 Plasma 实现之间的主要区别（下面会解释！）在于区块结构：Merkle _和_树。这有显著的好处，但增添了复杂性。与侧链相比，Plasma 本身已经很复杂。\r\n- 代码尚未经过审计或正式验证，也未进行任何优化。\r\n- 虽然操作员是唯一的 _计算_ 瓶颈，但当前主要的性能限制仍然是带宽。保管证明要求下载数量与块数线性相关。我们的代码在每个块上有所改进，但仍然是线性的。这个 [活跃的](https://ethresear.ch/t/rsa-accumulators-for-plasma-cash-history-reduction/3739) [研究领域](https://ethresear.ch/t/log-coins-sized-proofs-of-inclusion-and-exclusion-for-rsa-accumulators/3839/7) 目前尚未准备好实现。\r\n- 尽管我们的安全机制和退出游戏都已实现并进行测试，但我们尚未构建自动化的守卫服务，这意味着挑战和回应必须手动构造。\r\n\r\n说完这些，我们开始吧！本文的其余部分将全面深入探讨我们的规范、代码的位置和其功能。\r\n\r\n \r\n\r\n## 仓库与架构\r\n\r\n[我们的 Github](https://github.com/plasma-group/) 提供了所有实现，采用 MIT 许可：\r\n\r\n- `plasma-chain-operator`: 启动你自己的 plasma 链并部署到测试网。\r\n- `plasma-core`: 核心 plasma 客户端功能 —— 可移植的逻辑核心。\r\n- `plasma-node`: `plasma-core` 的 Node.js 封装，实现 CLI。\r\n- `plasma-js-lib`: 用于构建 Web 应用程序集成 plasma 交易的 JS 帮助库。\r\n- `plasma-contracts`: `PlasmaChain.vy` 和 `PlasmaRegistry.vy` 的 Vyper 合约。\r\n- `plasma-explorer`: 由操作员托管的区块探测器。\r\n- `plasma-utils`: 在我们的 plasma 规范上构建的共享工具。\r\n- `plasma`: 对上述组件的集成测试。\r\n\r\n这是 `plasma-core` 实现的架构：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0w6JoXXzW1QSYYxW0.png)\r\n\r\n这是 `plasma-chain-operator` 实现的架构：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0rUHFuhj_56LoFpPW.png)\r\n\r\n## 1. 一般定义和数据结构\r\n\r\n本节将涵盖协议组件的术语和直觉。这些数据结构由 `plasma-utils` 库的 `serialization` 编码和解码。每个结构的所有数据结构的确切字节表示可以在 [schemas](https://github.com/plasma-group/plasma-utils/tree/master/src/serialization/schemas) 中找到。\r\n\r\n### Coin ID 分配\r\n\r\n任何 plasma 资产的基本单位表示为一枚Coin。就像在标准 Plasma Cash 中，这些Coin是非同质化的，我们称一个Coin的索引为其 `coinID`，它是16个字节。它们根据在每种资产（ERC 20/ETH）上的存款顺序分配。值得注意的是，链中的所有资产共享同一个 ID 空间，即使它们是不同的 ERC20 或 ETH。这意味着跨所有资产类别的交易（我们称之为 `tokenType` 或 `token`）共享同一树，以提供最大的压缩。\r\n\r\n我们通过让前4个字节指向Coin的 `tokenType`，接下来的12个字节代表该特定 `tokenType` 的所有可能Coin来实现这一点。\r\n\r\n例如：0th `tokenType` 始终为 `ETH`，因此第一个 `ETH` 存款将为存款人提供Coin `0x00000000000000000000000000000000` 的支出权。\r\n\r\n每次存款获得的总Coin数量恰好是 `(存入的 token 数量)/(最低 token 面额)`。\r\n\r\n例如：假设 `tokenType` 1 是 `DAI`，Coin面额是 `0.1 DAI`，第一个存款人发送了 `0.5 DAI`。这意味着它的 `tokenType == 1`，因此第一个存款人将从 `0x00000001000000000000000000000000` 到包括Coin `0x00000001000000000000000000000004` 收到 `coinID`。\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/1_BNsC7-z06ilTUYj-2dLAg.png)\r\n\r\nCoin共享同一 ID 空间\r\n\r\n### 面额\r\n\r\n在实践中，面额将远低于 `0.1`。合约并不直接存储面额，而是存储一个 `decimalOffset` 映射，针对每个 `tokenType`，表示存入的 `ERC20`（或 ETH 的 `wei`）与收到的 plasma Coin之间的小数位数偏移。这些计算可以在 [智能合约](https://github.com/plasma-group/plasma-contracts/blob/master/contracts/PlasmaChain.vy) 中的 `depositERC20`、`depositETH` 和 `finalizeExit` 函数中找到。\r\n\r\n_//注意：_ `decimalOffset` _在此版本中被硬编码为0，因为在客户端/操作员代码中缺少支持。_\r\n\r\n## 2. 跨越Coin范围的交易\r\n\r\n### 转账\r\n\r\n一个交易由指定的 `block` 号和一个 `Transfer` 对象数组组成，后者描述每个交易范围的详细信息。从 `plasma-utils` 中的 [schema](https://github.com/plasma-group/plasma-utils/blob/master/src/serialization/schemas/transfer.js)（字节的 `length`）：\r\n\r\nPG Plasma 转移方案 – 中型\r\n\r\n|     |     |\r\n| --- | --- |\r\n|  | ... |\r\n|  | constTransferSchema=newSchema({ |\r\n|  | sender: { |\r\n|  | type: Address, |\r\n|  | required: true |\r\n|  | }, |\r\n|  | recipient: { |\r\n|  | type: Address, |\r\n|  | required: true |\r\n|  | }, |\r\n|  | token: { |\r\n|  | type: Number, |\r\n|  | length: 4, |\r\n|  | required: true |\r\n|  | }, |\r\n|  | start: { |\r\n|  | type: Number, |\r\n|  | length: 12, |\r\n|  | required: true |\r\n|  | }, |\r\n|  | end: { |\r\n|  | type: Number, |\r\n|  | length: 12, |\r\n|  | required: true |\r\n|  | } |\r\n|  | ... |\r\n\r\n[查看原始](https://gist.github.com/ben-chain/2c3818f78846289a15be1aa175f35ca5/raw/04c7d06627c995b0641c792cf8fac9c12af16318/transfer.js) [transfer.js](https://gist.github.com/ben-chain/2c3818f78846289a15be1aa175f35ca5#file-transfer-js)\r\n\r\n我们可以看到，`Transaction` 中的每个 `Transfer` 都指定了 `tokenType`、`start`、`end`、`sender` 和 `recipient`。\r\n\r\n### 有类型和无类型的边界\r\n\r\n上面需要注意的一点是，`start` 和 `end` 的值不是 16 个字节，正如 `coinID`，而是 12 个字节。这在上面关于存款的部分应该是明了的。要获取转账所描述的实际 `coinID`，我们将 `token` 字段的4个字节连接到 `start` 和 `end` 的左侧。我们通常将12个字节版本称为转账的 `untypedStart` 和 `untypedEnd`，而连接版本被称为 `typedStart` 和 `typedEnd`。这些值 [也被序列化程序暴露](https://github.com/plasma-group/plasma-utils/blob/master/src/serialization/models/transfer.js)。另一条说明：在任何转移中，相应的 `coinID` 被定义为 `start` 包括且 `end` 不包括。也就是说，确切的被转移的 `coinID` 为 `[typedStart, typedEnd)`。例如，前 100 个 ETH coins 可以用一个 `Transfer` 来发送，其中 `transfer.token = 0`, `transfer.start = 0`, 和 `transfer.end = 100`。第二 100 个将设置为 `transfer.start = 100` 和 `transfer.end = 200`。\r\n\r\n### 多重发送和转移/交易原子性\r\n\r\n`Transaction` 结构由一个 4 字节的 `block` 编号组成（该交易只有在包含于那个特定的 plasma 区块中时才有效），以及一个 _数组_ 的 `Transfer` 对象。这意味着一个交易可以描述多个转移，所有这些转移要么全部原子性地执行，要么不执行，取决于 _整个交易_ 的包含和有效性。这将为后续版本的去中心化交易和 [碎片整理](https://ethresear.ch/t/plasma-cash-defragmentation-take-3/3737) 打下基础。\r\n\r\n### 序列化\r\n\r\n如上所示，`plasma-utils` 实现了一个自定义的序列化库用于数据结构。JSON RPC 和智能合约均使用序列化器编码的字节数组。\r\n\r\n编码相当简单，是将每个值按 schema 定义的字节数串联而成。\r\n\r\n对于涉及可变大小数组的编码，例如包含一个或多个 `Transfer` 的 `Transaction` 对象，前面会有一个字节用于表示元素的数量。序列化库的测试可以在 [此处](https://github.com/plasma-group/plasma-utils/blob/master/test/serialization/test-serialization.js) 找到。\r\n\r\n目前，我们有以下对象的结构：\r\n\r\n- `Transfer`\r\n- `UnsignedTransaction`\r\n- `Signature`\r\n- `SignedTransaction`\r\n- `TransferProof`\r\n- `TransactionProof`\r\n\r\n## 3. 区块结构规范\r\n\r\nPlasma Cash 引入的最重要改进之一是“轻证明”。之前，plasma 的构建要求用户下载整个 plasma 链，以确保他们资金的安全。有了 Plasma Cash，他们只需下载与自己资金相关的 Merkle 树的分支。\r\n\r\n这是通过引入 _新的交易有效性条件_ 实现的：特定 `coinID` 的交易仅在 Merkle 树的第 `coinID` 个叶子上有效。因此，只需下载该分支即可确信该币没有 _有效_ 的交易。这个方案的问题在于，交易在该面额上是“卡住”的：如果想要交易多个币，需要多个交易，即每个叶子一个交易。\r\n\r\n不幸的是，如果我们将基于范围的交易放入常规 Merkle 树的分支中，轻证明将变得不安全。这是因为拥有一个分支并不能保证其他分支不会相交：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0rF68tR-5SdzlI5uY.png)\r\n\r\n叶子 4 和 6 都描述了范围 (3,4) 的交易。拥有一个分支并不保证另一个分支不存在。\r\n\r\n在常规 Merkle 树中，确保没有其他分支相交的 _唯一_ 方法是下载 _所有_ 分支并进行检查。但这不再是轻证明！\r\n\r\n在我们 plasma 实现的核心是一个 _新的区块结构_ 和一个随之而来的 _新的交易有效性条件_，这使我们能够获得基于范围的交易的轻证明。该区块结构称为 Merkle _sum_ 树，在每个哈希旁边有一个 `sum` 值。\r\n\r\n新的有效性条件使用特定分支的 `sum` 值来计算 `start` 和 `end` 范围。这个计算经过特别设计，以使**两个分支的计算范围重叠是 **不可能的。** `transfer` 仅在其自身范围在该范围内时有效，因此这使我们重新获得了轻客户端！\r\n\r\n本节将指定 sum 树的确切规范、范围计算的实际内容以及我们如何构建满足范围计算的树。关于我们导致这个规范的研究的更详细背景和动机，请随时查看 [这个](https://ethresear.ch/t/plasma-cash-was-a-transaction-format/4261) 文章。\r\n\r\n我们已经编写了两个 plasma Merkle sum 树的实现：一个在 [数据库](https://github.com/plasma-group/plasma-operator/blob/master/src/block-manager/leveldb-sum-tree.js) 中为操作员，另一个在内存中供测试 [用](https://github.com/plasma-group/plasma-utils/tree/master/src/sum-tree) 的 `plasma-utils`。\r\n\r\n### Sum 树节点规范\r\n\r\nMerkle sum 树中的每个节点为 48 字节，结构如下：\r\n\r\n`[32 字节哈希][16 字节和]`\r\n\r\n`sum` 的 16 字节长度与 `coinID` 相同并不是巧合！\r\n\r\n我们有两个辅助属性，`.hash` 和 `.sum`，用于提取这两部分。例如，对于某个 `node = 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315ffffffffffffffffffffffffffffffff`，我们有\r\n\r\n`node.hash == 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315` 和 `node.sum == 0xffffffffffffffffffffffffffffffff`。\r\n\r\n### 父级计算\r\n\r\n在常规 Merkle 树中，我们构造一个哈希节点的二叉树，直到形成一个单一的根节点。指定 sum 树格式只需定义 `parent(left, right)` 计算函数，该函数接受两个兄弟节点作为参数。例如，常规 Merkle sum 树有：\r\n\r\n`parent = function (left, right) { return Sha3(left.concat(right)) }` 其中 `Sha3` 是哈希函数，`concat` 用于将两者连接在一起。\r\n\r\n要创建一个 Merkle _sum_ 树，`parent` 函数还必须连接对其子节点 `.sum` 值求和的结果：\r\n\r\n```\r\nparent = function (left, right) {\r\n return Sha3(left.concat(right)).concat(left.sum + right.sum)\r\n}\r\n```\r\n\r\n例如，我们可能有\r\n\r\n```\r\nparent(0xabc…0001, 0xdef…0002) ===\r\nhash(0xabc…0001.concat(0xdef…0002)).concat(0001 + 0002) ===\r\n0x123…0003\r\n```\r\n\r\n注意，`parent.hash` 是对每个 `sibling.sum` 及其哈希值的承诺：我们对所有 96 字节进行哈希处理。\r\n\r\n### 计算分支范围\r\n\r\n我们使用 Merkle sum 树的原因在于它允许我们计算分支所描述的特定范围，并且可以 100% 确信没有其他有效的、重叠的分支存在。\r\n\r\n我们通过在分支上累加 `leftSum` 和 `rightSum` 来计算此范围。将两个值都初始化为 0，在每次父节点计算时，如果包含证明指示有右侧兄弟节点，我们取 `rightSum += right.sum`，如果是左侧，则取 `leftSum += left.sum`。\r\n\r\n然后，分支所描述的范围为 `[leftSum, root.sum — rightSum)`。请看下面的例子：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0tbtu6Mzpk-ObXHjd.png)\r\n\r\n分支的 Merkle 和计算。\r\n\r\n在这个例子中，分支 6 的有效范围为 `[21+3, 36–5) == [24, 31)`。注意到 `31–24=7`，这是叶子 6 的和值！同样，分支 5 的有效范围为 `[21, 36-(7+5)) == [21, 24)`。注意，其结束与分支 6 的开始相同！\r\n\r\n如果你稍微玩一下，你会发现，构造一个 Merkle sum 树而有两个不同分支覆盖相同范围是不可行的。在树的某一层，和就必须是被打破的！去试试 “欺骗” 叶子 5 或 6，方法是构造另一个与范围 (4.5,6) 相交的分支。只需填写灰色框中的 `?`：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0LL4h2OKGjW_kfmWo.png)\r\n\r\n你会在树的某一层发现这总是不可能的：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0-h88VLvsb5Z8gQEz.png)\r\n\r\n这就是我们获得轻客户端的方式。我们称分支的范围为 `implicitStart` 和 `implicitEnd`，因为它们是根据包含证明 “隐式地” 计算得出的。我们在 `plasma-utils` 中实现了一个分支检查器，通过 `calculateRootAndBounds()` 进行测试和客户端证明检查：\r\n\r\nPG Plasma 客户端侧 sum 树分支检查器\r\n\r\n以及在 Vyper 智能合约中：\r\n\r\n注意，这些范围是 _类型化_ 的开始和结束，完整的 16 字节。\r\n\r\n### 解析 Transfers 作为叶子\r\n\r\n在常规 Merkle 树中，我们通过对“叶子”进行哈希处理来构造最底层的节点：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0CJSl0_ZcnJ53-VxG.png)\r\n\r\n在我们的案例中，我们希望叶子是交易。因此，哈希处理是直接的，但我们仍然需要树的底层的 `.sum` 值。\r\n\r\n给定某个 `txA`，其中包含一个 `transferA`，那么和的值应该是什么呢？事实证明，_并不是_ 仅仅 `transferA.end — transferA.start`。这样做的原因是，如果转移不相邻，会扰乱分支的范围。我们需要对和值进行“填充”以考虑这一间隙，否则 `root.sum` 将会太小。\r\n\r\n有趣的是，这是一个非确定性的选择，因为你可以对间隙右侧或左侧的节点进行填充。我们选择了以下“左对齐”方案，将叶子解析为块：\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0YqGAq6DPh9MoYZML.png)\r\n\r\nTransfer 和 sum 解析\r\n\r\n我们将最底层的 `.sum` 值称为该分支的 `parsedSum`，而 `TransferProof` 结构包括了一个用于重构底层节点的 `.parsedSum` 值。\r\n\r\n### 分支有效性和隐式 NoTx\r\n\r\n因此，由智能合约检查分支的有效性条件如下：`implicitStart <= transfer.typedStart < transfer.typedEnd <= implicitEnd`。注意，在 “Plasma Cashflow” 中 sum 树的最初设计中，一些叶子填充了特殊的 “NoTx” 交易，以表示未进行任何交易的范围。采用此格式后，未进行交易的币正好是那些在范围 `[implicitStart, transfer.typedStart)` 和 `[transfer.typedEnd, implicitEnd)` 内的币。智能合约保证这些范围内的币不会被用于任何挑战或对退出的响应。\r\n\r\n### 原子多重发送\r\n\r\n通常（为了支持交易费用和交换），交易要求多次转移要么全部发生，要么不发生，以保持有效性。其结果是，有效的交易需要为其每个 `.transfers` 包含一次—每个在与该 `transfer.typedStart` 和 `.typedEnd` 具有有效和相关性。这些包含中的每个，仍然是完整的 `UnsignedTransaction` 的哈希—而不是单独的 `Transfer`—被解析到底部的 `.hash`。\r\n\r\n## 5. 证明结构和检查\r\n\r\n与传统区块链系统不同，完整的 plasma 节点不存储每一笔交易，他们只需存储与其拥有的资产相关的信息。这意味着 `sender` 必须 _证明_ 给 `recipient`，发送者确实拥有给定的范围。完整的证明包含所有足够的信息，以保证，如果以太坊链本身不分叉，则代币可以在主链上赎回。\r\n\r\n证明主要包含交易的包含和未包含，更新这些币的保管链。必须检查包含根是否与操作员提交给主链智能合约的区块哈希相符。通过在证明方案中跟踪保管链，从代币初始存款到当前，仅此能够赎回是有保障的。\r\n\r\n`plasma-core` 遵循相对简单的方法来验证传入的交易证明。本节描述了这个方法。\r\n\r\n### 证明格式\r\n\r\n历史证明包含一组 _存款记录_ 和一长串相关的 `Transaction` 及对应的 `TransactionProof`。\r\n\r\n`plasma-utils` [公开](https://github.com/plasma-group/plasma-utils/blob/13ab042d8962852cf8c1905727d616448923764d/src/sum-tree/plasma-sum-tree.js#L281) 一个 `static checkTransactionProof(transaction, transactionProof, root)` 方法，该方法在 `plasma-core` [这里](https://github.com/plasma-group/plasma-core/blob/3caa359681db62106ba703eb0fd99171ebb86365/src/services/proof/snapshot-manager.js#L117) 通过 `ProofService` 被调用。\r\n\r\n### 交易证明\r\n\r\n`TransactionProof` 对象包含检查给定 `Transaction` 有效性的所有必要信息。简单来说，它是 [仅仅](https://github.com/plasma-group/plasma-utils/blob/master/src/serialization/schemas/transaction-proof.js) 一组 `TransferProof` 对象。根据上述的原子多重发送部分，给定的 `TransactionProof` 仅在其所有的 `TransferProofs` 都有效时才有效。\r\n\r\n### 转移证明\r\n\r\n`TransferProofs` 包含恢复与该交易在正确区块号下的给定 `Transfer` 相关的有效分支的包含所需的所有信息。这包括：\r\n\r\n- Merkle sum 树中实际的节点，它们表示分支的完整 `inclusionProof`\r\n- 为计算分支所描绘的二元路径所需的叶子索引\r\n- 如上文 sum 树规范所述的解析底部 `.sum` 值\r\n- 对于特定发送者的 `signature`。\r\n\r\n来自 `plasma-utils` [架构](https://github.com/plasma-group/plasma-utils/blob/master/src/serialization/schemas/transfer-proof.js)：\r\n\r\n注意，`inclusionProof` 是一个变长数组，其大小取决于树的深度。\r\n\r\n### 证明步骤\r\n\r\n验证过程的核心是将每个证明元素应用于当前的“验证”状态，开始于存款。如果任何证明元素未能导致有效状态转换，则必须拒绝该证明。\r\n\r\n处理每个证明元素的过程很直观；我们只需按照合约的保管规则在每个区块上应用交易即可。\r\n\r\n### 快照对象\r\n\r\n我们跟踪历史拥有范围的方式称为 `snapshot`。\r\n\r\n简单来说，它表示在一个区块内某个范围的验证所有者：\r\n\r\n```\r\n{\r\n  typedStart: Number,\r\n  typedEnd: Number,\r\n  block: Number,\r\n  owner: address\r\n}\r\n```\r\n\r\n### 存款记录\r\n\r\n每个接收的范围必须来自相应的存款。\r\n\r\n存款记录由其 `token`、`start`、`end`、`depositer` 和 `blockNumber` 组成。\r\n\r\n对于每个存款记录，验证者 _必须_ 与以太坊进行双重检查，以验证所声明的存款确实发生，且其间没有发生退出。\r\n\r\n如果如此，一个 `verifiedSnapshots` 数组被初始化为这些存款，并将每个 `snapshot.owner` 设置为存款者。\r\n\r\n接下来，我们应用所有给定的 `TransactionProof`，相应地更新 `verifiedSnapshots`。对于每个 `transaction` 和相应的 `transactionProof`，验证者执行以下步骤：\r\n\r\n1. 验证给定的证明元素有效。如果无效，则抛出错误。\r\n2. 对于 `transaction` 中的每个 `transfer`，执行以下操作：\r\n\r\n    a. “拆分” 任何在 `transfer.typedStart`、`transfer.typedEnd`、`implicitStart` 和 `implicitEnd` 处更新的快照\r\n\r\n    b. 为所有 `block` 等于 `transaction.blockNumber — 1` 的 `verifiedSnapshots` 增加 `.block` 编号\r\n\r\n    c. 对于每个在 `transfer.start` 和 `transfer.end` 之间的拆分 `snapshot`：\r\n\r\n    i. 验证 `snapshot.owner === transfer.from`。如果不相等，抛出错误。\r\n\r\n    ii. 设置 `snapshot.owner = transfer.sender`。\r\n\r\n`TransactionProofs` 必须按升序的 `blockNumber` 应用。\r\n\r\n一旦针对所有 `TransactionProof` 递归应用了此操作，客户端可以自行检查她现在拥有的新币，方法是搜索所有在 `verifiedSnapshots` 中，`blockNumber` 等于当前 plasma 区块且 `owner` 等于她的地址。\r\n\r\n### TransactionProof 有效性\r\n\r\n上面第一步中的交易有效性检查等同于检查智能合约的有效性条件。基于上面 sum 树规范的基本有效性检查如下：\r\n\r\n1. 检查交易编码是否格式良好。\r\n\r\n2. 对于每个 `transfer` 和相应的 `transferProof`：\r\n\r\na. 检查 `signature` 是否解析为其 `transfer.sender` 地址\r\n\r\nb. 验证 `inclusionProof` 的根是否等于该 plasma 区块的根哈希，该根的二元路径由 `leafIndex` 定义\r\n\r\nc. 计算分支的 `implicitStart` 和 `implicitEnd`，验证 `implicitStart <= transfer.start < transfer.end <= implicitEnd`\r\n\r\n## 4. 合约和退出游戏\r\n\r\n当然，保管链的证明是没用的，除非它也能够传递到主链以保持资金的安全。接受链上证明的机制是 plasma 的安全模型的核心，称为“退出游戏”。\r\n\r\n当用户希望将他们的钱从 plasma 链上转走时，他们会进行一次“退出”，这会开启一个争议期。在争议期结束时，如果没有未处理的争议，资金将从主链的 plasma 合约中发送到退出者。在争议期内，用户可以提交“挑战”，宣称正在退出的资金并不真正属于退出者。上述的证明确保对这些挑战的“响应”始终可以计算。\r\n\r\n退出游戏的目标是确保资金安全，即使在最大程度的敌对操作员情况下。特别是，我们必须减轻三种主要攻击：\r\n\r\n- **数据隐匿：** 操作员可以向合约发布根哈希，但不告诉任何人区块的内容。\r\n- **包括伪造/无效交易：** 操作员可能在其保管链中将一笔其 `sender` 不是前一个 `recipient` 的交易包含在区块中。\r\n- **审查：** 在某人存入他们的钱后，操作员可能拒绝发布任何发送这些资金的交易。\r\n\r\n在所有这些情况下，退出游戏的挑战/响应协议确保这些行为不会允许最多在 1 次挑战之后，进行 1 次响应。\r\n\r\n### 跟踪存款和退出\r\n\r\n**存款映射**\r\n\r\n每当一组新币被存入时，合约更新一个映射，每项都包含一个 `deposit` 结构。从合约：\r\n\r\n注意，此结构既不包含 `untypedEnd` 也不包含存款的 `tokenType`。原因在于合约使用这些值作为映射的映射的键。例如，访问给定存款的存款者如下所示：`someDepositer: address = self.deposits[tokenType][untypedEnd].depositer`\r\n\r\n这个选择节省了一些 gas，并且还让部分代码更清晰，因为我们不需要存储任何类型的存款 ID 来引用存款。\r\n\r\n**可退出范围映射**\r\n\r\n除了在每次存款时添加 `self.deposits` 条目外，合约还需要以某种方式跟踪历史退出，以防止在同一范围内进行多次退出。这要复杂一些，因为退出并不是像存款那样顺序进行，查询退出列表会很昂贵。\r\n\r\n我们的合约实现了一个固定大小的解决方案，它存储一个可退出范围的列表，并在新退出发生时更新该列表。从智能合约：\r\n\r\n同样，我们使用双重嵌套映射，其键为 `tokenType` 和 `untypedEnd`，以便可以通过 `self.exitable[tokenType][untpyedEnd].untypedStart` 访问范围的开始。请注意，Vyper 对于所有未设置的映射键返回 0，因此我们需要一个 `isSet` 布尔值，以便用户无法通过传递未设置的 `exitableRange` 来“欺骗”合约。\r\n\r\n合约的 `self.exitable` 范围基于通过名为 `removeFromExitable` 的帮助函数成功调用 `finalizeExit` 进行拆分和删除。注意，在之前已退出的范围上的退出甚至不需要挑战；它们将永远不会通过 `finalizeExit` 中调用的 `checkRangeExitable` 测试。你可以在 [这里](https://github.com/plasma-group/plasma-contracts/blob/068954a8584e4168daf38ebeaa3257ec08caa5aa/contracts/PlasmaChain.vy#L380) 找到这段代码。\r\n\r\n### 退出游戏与传统 Plasma Cash 的关系\r\n\r\n从本质上讲，我们规范中的退出游戏与原始 Plasma Cash 设计非常相似。退出通过调用以下函数发起：\r\n\r\n```\r\nbeginExit(tokenType: uint256, blockNumber: uint256, untypedStart: uint256, untypedEnd: uint256) -> uint256:\r\n```\r\n\r\n要对退出提出异议，所有挑战都指定一个具体的 `coinID`，并在该特定Coin上进行 Plasma Cash 风格的挑战游戏。只需证明一个币是无效的，就足以取消整个退出。\r\n\r\n退出和两种可响应挑战都获得一个 `exitID` 和 `challengeID`，它们按递增的 `challengeNonce` 和 `exitNonce` 顺序分配。\r\n\r\n### 基于区块号的交易\r\n\r\n在原始的 Plasma Cash 规范中，退出者需要同时指定退出的交易及其之前的“父”交易，以防止“在途”攻击，操作员延迟包含有效交易并在其间插入无效交易。\r\n\r\n这对我们基于范围的方案pose了问题，因为一笔交易可能有多个父级。例如，如果 Alice 将 `(0, 50]` 发送给 Carol，Bob 又将 `(50, 100]` 发送给 Carol，那么 Carol 现在可以将三个 `(0, 100]` 发送给 Dave。但是，如果 Dave 想退出，那两个 `(0, 50]` 和 `(50, 100]` 都是父级。\r\n\r\n尽管指定多个父级当然是可行的，但此规范的 gas 成本较高，且实现起来似乎更复杂。因此，我们选择了更简单的替代方案，每个交易指定发件人打算将发送到的 `block`，并且如果包含在不同的区块中，则无效。这解决了在途攻击，并意味着合约不需要交易的父级。对于那些有兴趣了解这种方案的正式书面和安全性证明的人，可以考虑看看 [这篇优秀的帖子](https://ethresear.ch/t/plasma-cash-with-smaller-exit-procedure-and-a-general-approach-to-safety-proofs/1942)。\r\n\r\n### 每个币的交易有效性\r\n\r\n我们退出游戏的一个反直觉特性是，某个交易可能对其范围内的某些币是“有效”的，但对其他币则无效。\r\n\r\n例如，设想 Alice 将 `(0, 100]` 发送给 Bob，Bob 随后将 `(50, 100]` 发送给 Carol。Carol _不需要_ 验证 Alice 是否为整个 `(0, 100]` 的合法拥有者。Carol 只需确认 Alice 拥有 `(50, 100]` — 适用于她收据的保管链的一部分。尽管如果 Alice 不拥有 `(0, 50]` 该交易在某种意义上是“不合法”的，但就用于针对 `(50, 100]` 的退出争议而言，智能合约 _并不关心_。只要收到的币的所有权得到验证，其余交易并无关紧要。\r\n\r\n这是为了保持轻客户端证明的大小至关重要的要求。如果 Carol 必须检查完整的 `(0, 100]`，她可能还必须检查 `(0, 10000]` 的一个重叠父级，然后再检查它的所有父级，依此类推。如果交易之间的相互依赖性很强，这种“级联”效应可能会大大增加证明的大小。\r\n\r\n注意，这一属性同样适用于描述多个范围交换的原子多重发送。如果 Alice 用 Bob 的 1 DAI 交换 1 ETH，那 Alice 有责任在签名之前检查 Bob 是否拥有 1 DAI。然而，在此之后，如果 Bob 将 1 ETH 发送给 Carol，Carol **不需要验证** Bob 是否拥有 1 DAI，只需验证 Alice 提供给 Bob 的 1 ETH 的所有权。风险由 Alice 承担，因此 Carol 不必承担。\r\n\r\n从智能合约的角度来看，这一属性是挑战始终在退出中针对特定的 `coinID` 提交的直接后果。\r\n\r\n### 合约如何处理交易检查\r\n\r\n请注意，要在退出游戏中使用，`Transaction` 必须通过上述证明部分描述的 `TransactionProof` 检查（有效签名、分支边界等）。在合约层面进行的检查如下所示：\r\n\r\n```\r\ndef checkTransactionProofAndGetTypedTransfer(\r\n   transactionEncoding: bytes[277],\r\n   transactionProofEncoding: bytes[1749],\r\n   transferIndex: int128\r\n ) -> (\r\n   address, # transfer.to\r\n   address, # transfer.from\r\n   uint256, # transfer.start (typed)\r\n   uint256, # transfer.end (typed)\r\n   uint256 # transaction plasmaBlockNumber\r\n ):\r\n```\r\n\r\n这里有一个重要的注意事项是 `transferIndex` 参数。请记住，交易可能包含多个转移，必须为每个转移包含在树中。但由于挑战仅指向一个具体的 `coinID`，因此只有一个转移是相关的。因此，挑战者和响应者提供了一个 `transferIndex` — 该转移与争议的币相关。该检查解码并检查交易证明中的所有 `TransferProof`，然后使用以下函数检查每个证明：\r\n\r\n```\r\ndef checkTransferProofAndGetTypedBounds(\r\n leafHash: bytes32,\r\n blockNum: uint256,\r\n transferProof: bytes[1749]\r\n) -> (uint256, uint256): # typedimplicitstart, typedimplicitEnd\r\n```\r\n\r\n一旦所有 `TransferProof` 被验证，针对 `transferIndex` 的交易相关值将返回给退出游戏函数：即 `sender`、`recipient`、`typedStart`、`typedEnd` 和 `plasmaBlockNumber`。\r\n\r\n说完这一切，我们可以指定完整的退出挑战/响应游戏集。\r\n\r\n### 立即取消退出的挑战\r\n\r\n有两种类型的挑战可以立即取消退出：那些挑战已用的币和那些挑战存款发生前的退出。\r\n\r\n**已用币挑战**\r\n\r\n此挑战用于证明交易的退出者已将资金发送给其他人。\r\n\r\n```\r\n@public\r\ndef challengeSpentCoin(\r\n exitID: uint256,\r\n coinID: uint256,\r\n transferIndex: int128,\r\n transactionEncoding: bytes[277],\r\n transactionProofEncoding: bytes[1749],\r\n):\r\n```\r\n\r\n它使用 `checkTransactionProofAndGetTypedTransfer` 然后检查以下条件：\r\n\r\n1. 挑战的 coinID 位于指定退出范围内。\r\n2. 挑战的 coinID 位于 `transaction.transfers` 的 `transferIndex` 项的 `typedStart` 和 `typedEnd` 之间。\r\n3. 挑战的 `plasmaBlockNumber` 大于退出的。\r\n4. `transfer.sender` 是退出者。\r\n\r\n引入原子交换意味著一旦事情发生，需要加倍确保已用币的挑战周期必须严格短于其他的，因为操作员在两个或多个方之间隐匿原子交换的边缘案例中。在这种情况下，这些方必须退出他们预_SWAP的币，迫使操作员提出已用币的挑战并表明交换是否被纳入。但如果我们允许操作员在最后时刻这么做，那将成为条件竞争，导致当事方没有时间利用公开信息去取消其他退出。因此，时限比普通挑战窗口缩短（1/2）。\r\n\r\n**存款前挑战**\r\n\r\n此挑战用于证明退出是发生在该币的实际存款之前的早期 `plasmaBlockNumber` 中。\r\n\r\n```\r\n@public\r\ndef challengeBeforeDeposit(\r\n exitID: uint256,\r\n coinID: uint256,\r\n depositUntypedEnd: uint256\r\n):\r\n```\r\n\r\n合约查找 `self.deposits[self.exits[exitID].tokenType][depositUntypedEnd].precedingPlasmaBlockNumber` 并检查它是否晚于退出的块编号。如果是，则取消。\r\n\r\n### 乐观退出和包含挑战\r\n\r\n我们的合约允许在乐观情况下退出无任何包含检查。为此，任何退出都可以通过直接调用\r\n\r\n```\r\n@public\r\ndef challengeInclusion(exitID: uint256):\r\n```\r\n\r\n来挑战，退出者必须直接响应退出的交易或存款。\r\n\r\n```\r\n@public\r\ndef respondTransactionInclusion(\r\n challengeID: uint256,\r\n transferIndex: int128,\r\n transactionEncoding: bytes[277],\r\n transactionProofEncoding: bytes[1749],\r\n):...\r\n@public\r\ndef respondDepositInclusion(\r\n challengeID: uint256,\r\n depositEnd: uint256\r\n):\r\n```\r\n\r\n第二个案例允许用户避免本金在存款后被操作员审查的情况下退出。\r\n\r\n这两个响应都在以下条件下取消挑战：\r\n\r\n1. 存款或交易确实是在退出的 plasma 块号下。\r\n2. 存款者或接收者确实是退出者。\r\n3. 退出的开始和结束均在存款或转移的开始和结束之间。\r\n\r\n### 无效历史挑战\r\n\r\n复杂的挑战 – 响应游戏，无论是原版 Plasma Cash 还是本规范中，都是历史无效的案例。该协议的这部分减轻了操作员包括一种伪造“无效”交易的攻击，该交易的发送者不是前一个接收者。解决方案称为无效历史挑战：由于合法拥有者尚未花费他们的币，他们向其发出证明：“哦，那币是你的？那它早些时候是我的，你无法证明我曾花费它。”\r\n\r\n无效历史挑战和响应均可通过存款或交易进行。\r\n\r\n**挑战**\r\n\r\n根据当前合法所有者，有两种挑战方式：\r\n\r\n```\r\n@public\r\ndef challengeInvalidHistoryWithTransaction(\r\n exitID: uint256,\r\n coinID: uint256,\r\n transferIndex: int128,\r\n transactionEncoding: bytes[277],\r\n transactionProofEncoding: bytes[1749]\r\n):\r\n```\r\n\r\n和\r\n\r\n```\r\n@public\r\ndef challengeInvalidHistoryWithDeposit(\r\n exitID: uint256,\r\n coinID: uint256,\r\n depositUntypedEnd: uint256\r\n):\r\n```\r\n\r\n这两者都调用：\r\n\r\n```\r\n@private\r\ndef challengeInvalidHistory(\r\n exitID: uint256,\r\n coinID: uint256,\r\n claimant: address,\r\n typedStart: uint256,\r\n typedEnd: uint256,\r\n blockNumber: uint256\r\n):\r\n```\r\n\r\n此函数负责验证 `coinID` 是否在已挑战的退出范围内，并且该 `blockNumber` 是否早于退出。\r\n\r\n**响应无效历史挑战**\r\n\r\n当然，无效历史挑战可能是一种麻烦，其中实际的发起者确实花费了他们的 BN，并且保管链确实有效。我们必须允许这种响应。有两种类型。\r\n\r\n第一种是通过显示发起者支出而响应的交易：\r\n\r\n```\r\n@public\r\ndef respondInvalidHistoryTransaction(\r\n challengeID: uint256,\r\n transferIndex: int128,\r\n transactionEncoding: bytes[277],\r\n transactionProofEncoding: bytes[1749],\r\n):\r\n```\r\n\r\n智能合约然后执行以下检查：\r\n\r\n1. `transferIndex` 的 `Transfer` 位于所挑战的 `coinID` 处。\r\n2. `transferIndex` 的 `transfer.sender` 实际上是该无效历史挑战的原告。\r\n3. 该交易的 plasma 块号位于无效历史挑战和退出之间。\r\n\r\n另一个响应是显示挑战发生在币实际上被存入之前，从而使挑战无效。这类似于对退出本身进行的 `challengeBeforeDeposit ` 的操作。\r\n\r\n```\r\n@public\r\ndef respondInvalidHistoryDeposit(\r\n challengeID: uint256,\r\n depositUntypedEnd: uint256\r\n):\r\n```\r\n\r\n在这种情况下，没有对发送者是挑战接收者的检查因为这场挑战是无效的。因此，合约只需要检查：\r\n\r\n1. 存款覆盖所挑战的 `coinID`。\r\n2. 存款的 plasma 块号位于挑战与退出之间。\r\n\r\n如果是，则取消退出。\r\n\r\n这就结束了完整的退出游戏规范。凭借这些基础构件，资金即使在最大恶意的 plasma 链上也可以保持安全。\r\n\r\n## 6. 未来\r\n\r\nPlasma Group 致力于为更广泛的以太坊社区创建一个开放的 plasma 实现。我们的使命是通过探索 plasma 框架的全部潜力来推动二层扩展的未来。毫无疑问，还可以有更多事情需要推进！以下是我们希望下一个努力工作的几点。\r\n\r\n### 实现中的缺失部分\r\n\r\n**自动化维护**\r\n\r\n作为一个良好的开始，需要许多改进来实现 Plasma 的真正潜力，无论在本规范及更高版本中。目前我们实施中最明显的缺失部分是维护，自动化过程，代表用户提交挑战和响应。值得庆幸的是，退出游戏本身已实施且已手动进行 [测试](https://github.com/plasma-group/plasma-contracts/blob/master/test/test-plasma.js)，因此客户端软件可以在链部署后更新。我们认为这对于测试网的发布已足够，但也是代码最紧迫的补充。\r\n\r\n**P2P 历史证明**\r\n\r\n目前，当用户接收到交易时，他们向操作员请求并重新下载完整证明。这会大幅增加操作员的开销。实际上应该发生的是，发送者直接将其本地存储的证明传输给接收者，绕过操作员，使 plasma 链的运行成本大大降低。\r\n\r\n**碎片整理策略**\r\n\r\n由于我们支持原子交换，因此我们当前的规范兼容任何碎片整理策略，在合约无更新的情况下。但是，仍需找出合适的方法，特别是因为我们要求交易指定 Plasma 区块号。我们希望 plasma 社区能够建立一个可扩展的碎片整理抽象库，以允许操作员和用户尝试不同的方法。\r\n\r\n**前端钱包集成**\r\n\r\n我们有一些前端钱包的设计，但目前客户端仅支持命令行交易，并不支持不同 ERC20 的交易。提供一个良好的用户界面给测试网用户，将大幅提升用户体验和可访问性。\r\n\r\n**操作员费用**\r\n\r\n由于我们支持原子多重发送，我们可以在无需任何协议修改的情况下支持交易费用。但是，我们尚未为此测试网发布实施任何费用。\r\n\r\n**网络化操作员**\r\n\r\n我们尚未利用的一点是 Merkle 树的构造是高度并行的。如果操作员作为网络集群部署，我们可以通过并行构造子树来增加区块大小。\r\n\r\n![](https://img.learnblockchain.cn/2025/03/09/0jHP1K2gdztKHsgxX.png)\r\n\r\n有谁想要体验 Raspberry plasma 吗？\r\n\r\n**代码审查**\r\n\r\n在这个时刻，**非常可能** 客户端、合约和操作员的实现中存在严重的错误。我们希望此次公开发布的一部分是为外部贡献者提供机会，帮助指出许多错误！\r\n\r\n### 规范中的缺失部分\r\n\r\n**简洁证明方案**\r\n\r\n如前文所述，Plasma 研究中最活跃的领域是一种减少历史证明大小的方案。无论是 P2P 还是其他，老币（例如，超过 1 年）可能会有相当多的相关证明数据，使交易变得笨重。这是因为历史证明至少包含每个区块的一个分支。\r\n\r\nRSA 累加器和 STARKS/SNARKS 目前是最有可能的候选者，它们批量整理多个区块的分支证明。两者都需要协议更改：对于 RSA（这还引入了一个受信赖的设置），退出游戏必须添加一个全新的有效性条件。对于后者，树需要使用对 SNARK/STARK 友好的哈希算法构建，但在 EVM 中未实现。**大规模退出/存款方案** 如果操作方真的变得恶意，用户必须（最终，不急！）退出他们的资金。“大规模退出”的概念是指许多用户同意通过单个链上交易一起退出，这将显著提高可扩展性。理想情况下，退出可以通过余额的 merkle root 自动将资金直接存入不同的 plasma 链。这将使许多用户能够切换链，而无需在主链上解决单个余额——这是多个 plasma 链“网络连通性”的显著改善。\r\n\r\n**多操作员网络**\r\n\r\n尽管操作方不能偷盗资金，但他们可以随意审查交易。一种解决办法是用一组操作员替代单一的操作员模型，使得只需存在一个诚实的操作员便足以让客户进行交易。\r\n\r\n**改进的退出记录**\r\n\r\n我们的可退出范围构造允许对退出范围进行恒定大小的检查。然而，由于每次最终确定都会更新此映射，如果对同一范围的退出未按升序处理，就会出现竞争条件。这是因为第一个退出的最终确定会分割范围，改变 `self.exitable` 映射的键，导致针对未分割范围的退出时 `checkRangeExitable` 失败。这些退出将被回退，并且必须在下一个以太坊块中重新提交。可能存在一种更为高效的替代方案，可能使用某种树、队列或某种 `batchFinalizeExits` 方法。\r\n\r\n**状态通道和脚本**\r\n\r\n最近，研究社区在提出使用 covenants 的状态通道和脚本的可行性方面取得了很大进展。我们当前的规范不支持这两个特性，需要对智能合约进行重大升级以支持。\r\n\r\n让我们共同努力，朝着实现更去中心化未来的愿景迈进。\r\n\r\n>- 原文链接： [medium.com/plasma-group/...](https://medium.com/plasma-group/plasma-spec-9d98d0f2fccf)\r\n>- 登链社区 AI 助手，为大家转译优秀英文文章，如有翻译不通的地方，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/26097","address":null},"history":null,"timestamp":1741524712,"version":1}