以太坊 - Plasma Group的Plasma Spec

本文介绍了一种基于Plasma Cash的区块链扩容解决方案,包含了设计规范和Node.js及Vyper的实现细节。文中详细讨论了该协议的属性、区块结构、交易和证明检查机制等关键部分,并展示了相关代码和架构,作者希望通过这一实现推动以太坊社区的Layer 2扩展进步。

TLDR:我们创建了一个 Plasma Cash 变种的规范,并在 Node.js 和 Vyper 中实现了它。本文涵盖了设计规范,同时提供了实施过程中的参考。我们的代码支持在测试网部署新链、其他 Plasma 链及其区块探测器的链上注册,以及通过命令行钱包进行交易。

引言

区块链网络作为可扩展解决方案的愿景正在迅速传播。通过多链方法来并行处理交易是提高吞吐量的一种有希望的方法……但不幸的是,这也带来了重大挑战:

  • 我们不想分散安全性,例如100条链每条只有1%的总安全性。
  • 像 sharding 这样的先进解决方案很有前景,但尚未成熟。

我们需要一个可扩展性的解决方案:

  • 提供与 Ethereum 主网相似的安全级别,而无需支付数百万美元的挖矿费用。
  • 可以在目前的 Ethereum 上实现。

我们相信,满足这些标准的最强候选者是一个链的网络,每条链通过 Plasma 框架连接到主网。

Plasma 是一系列协议,允许个人轻松部署高吞吐量、安全的区块链。Ethereum 主链上的智能合约可以确保用户的资金安全,即使“plasma 链”完全恶意行为。这样便消除了像侧链那样需要可信锚定机制的必要。Plasma 链是非托管的,允许在不牺牲安全性的基础上优先考虑可扩展性。

我们设想一个拥有多个 Plasma 链的未来,让用户可以选择交易的地方。因此,除了发布我们的 plasma 链实现外,我们还创建了一个 PlasmaRegistry.vy。该注册表允许新的链通过列出其 IP/DNS 地址、自定义的“名称”字符串和其合约地址来加入网络。注册表合约验证受信任的部署,因此用户可以放心将任何合约存入该注册表 —— 即使其运营者存在恶意行为

我们 Plasma 链实现的特性

这篇文章指定了 Plasma Group 当前的协议和实现,吸取了研究社区的最新进展。

我们的规范具有以下特性:

  • 单个交易跨越大量Coin,解决了 Plasma Cash 中的 “固定面额”问题
  • 块大小与交易数量而非存款数量成比例的扩展。
  • 轻客户端证明其扩展与块大小的对数成正比,与自存款以来的块数量线性扩展,使得操作员成为系统唯一的(计算)瓶颈。
  • 简化的乐观退出程序,允许退出仅指定最新交易,而不是 同时指定交易及其父交易
  • 跨链原子交换,为去中心化交易协议打下基础。
  • 无限存款能力。

我们的实现遵循上述规范,提供以下功能:

如果你对协议和代码实现感兴趣,你来对地方了!

然而,在深入探讨之前,有几点免责声明:

  • 我们的 plasma 实现是 beta 软件,当前仅适用于测试网。此时肯定存在关键错误。
  • 该协议与其他 Plasma 实现之间的主要区别(下面会解释!)在于区块结构:Merkle _和_树。这有显著的好处,但增添了复杂性。与侧链相比,Plasma 本身已经很复杂。
  • 代码尚未经过审计或正式验证,也未进行任何优化。
  • 虽然操作员是唯一的 计算 瓶颈,但当前主要的性能限制仍然是带宽。保管证明要求下载数量与块数线性相关。我们的代码在每个块上有所改进,但仍然是线性的。这个 活跃的 研究领域 目前尚未准备好实现。
  • 尽管我们的安全机制和退出游戏都已实现并进行测试,但我们尚未构建自动化的守卫服务,这意味着挑战和回应必须手动构造。

说完这些,我们开始吧!本文的其余部分将全面深入探讨我们的规范、代码的位置和其功能。

仓库与架构

我们的 Github 提供了所有实现,采用 MIT 许可:

  • plasma-chain-operator: 启动你自己的 plasma 链并部署到测试网。
  • plasma-core: 核心 plasma 客户端功能 —— 可移植的逻辑核心。
  • plasma-node: plasma-core 的 Node.js 封装,实现 CLI。
  • plasma-js-lib: 用于构建 Web 应用程序集成 plasma 交易的 JS 帮助库。
  • plasma-contracts: PlasmaChain.vyPlasmaRegistry.vy 的 Vyper 合约。
  • plasma-explorer: 由操作员托管的区块探测器。
  • plasma-utils: 在我们的 plasma 规范上构建的共享工具。
  • plasma: 对上述组件的集成测试。

这是 plasma-core 实现的架构:

这是 plasma-chain-operator 实现的架构:

1. 一般定义和数据结构

本节将涵盖协议组件的术语和直觉。这些数据结构由 plasma-utils 库的 serialization 编码和解码。每个结构的所有数据结构的确切字节表示可以在 schemas 中找到。

Coin ID 分配

任何 plasma 资产的基本单位表示为一枚Coin。就像在标准 Plasma Cash 中,这些Coin是非同质化的,我们称一个Coin的索引为其 coinID,它是16个字节。它们根据在每种资产(ERC 20/ETH)上的存款顺序分配。值得注意的是,链中的所有资产共享同一个 ID 空间,即使它们是不同的 ERC20 或 ETH。这意味着跨所有资产类别的交易(我们称之为 tokenTypetoken)共享同一树,以提供最大的压缩。

我们通过让前4个字节指向Coin的 tokenType,接下来的12个字节代表该特定 tokenType 的所有可能Coin来实现这一点。

例如:0th tokenType 始终为 ETH,因此第一个 ETH 存款将为存款人提供Coin 0x00000000000000000000000000000000 的支出权。

每次存款获得的总Coin数量恰好是 (存入的 token 数量)/(最低 token 面额)

例如:假设 tokenType 1 是 DAI,Coin面额是 0.1 DAI,第一个存款人发送了 0.5 DAI。这意味着它的 tokenType == 1,因此第一个存款人将从 0x00000001000000000000000000000000 到包括Coin 0x00000001000000000000000000000004 收到 coinID

Coin共享同一 ID 空间

面额

在实践中,面额将远低于 0.1。合约并不直接存储面额,而是存储一个 decimalOffset 映射,针对每个 tokenType,表示存入的 ERC20(或 ETH 的 wei)与收到的 plasma Coin之间的小数位数偏移。这些计算可以在 智能合约 中的 depositERC20depositETHfinalizeExit 函数中找到。

//注意: decimalOffset 在此版本中被硬编码为0,因为在客户端/操作员代码中缺少支持。

2. 跨越Coin范围的交易

转账

一个交易由指定的 block 号和一个 Transfer 对象数组组成,后者描述每个交易范围的详细信息。从 plasma-utils 中的 schema(字节的 length):

PG Plasma 转移方案 – 中型

...
constTransferSchema=newSchema({
sender: {
type: Address,
required: true
},
recipient: {
type: Address,
required: true
},
token: {
type: Number,
length: 4,
required: true
},
start: {
type: Number,
length: 12,
required: true
},
end: {
type: Number,
length: 12,
required: true
}
...

查看原始 transfer.js

我们可以看到,Transaction 中的每个 Transfer 都指定了 tokenTypestartendsenderrecipient

有类型和无类型的边界

上面需要注意的一点是,startend 的值不是 16 个字节,正如 coinID,而是 12 个字节。这在上面关于存款的部分应该是明了的。要获取转账所描述的实际 coinID,我们将 token 字段的4个字节连接到 startend 的左侧。我们通常将12个字节版本称为转账的 untypedStartuntypedEnd,而连接版本被称为 typedStarttypedEnd。这些值 也被序列化程序暴露。另一条说明:在任何转移中,相应的 coinID 被定义为 start 包括且 end 不包括。也就是说,确切的被转移的 coinID[typedStart, typedEnd)。例如,前 100 个 ETH coins 可以用一个 Transfer 来发送,其中 transfer.token = 0, transfer.start = 0, 和 transfer.end = 100。第二 100 个将设置为 transfer.start = 100transfer.end = 200

多重发送和转移/交易原子性

Transaction 结构由一个 4 字节的 block 编号组成(该交易只有在包含于那个特定的 plasma 区块中时才有效),以及一个 数组Transfer 对象。这意味着一个交易可以描述多个转移,所有这些转移要么全部原子性地执行,要么不执行,取决于 整个交易 的包含和有效性。这将为后续版本的去中心化交易和 碎片整理 打下基础。

序列化

如上所示,plasma-utils 实现了一个自定义的序列化库用于数据结构。JSON RPC 和智能合约均使用序列化器编码的字节数组。

编码相当简单,是将每个值按 schema 定义的字节数串联而成。

对于涉及可变大小数组的编码,例如包含一个或多个 TransferTransaction 对象,前面会有一个字节用于表示元素的数量。序列化库的测试可以在 此处 找到。

目前,我们有以下对象的结构:

  • Transfer
  • UnsignedTransaction
  • Signature
  • SignedTransaction
  • TransferProof
  • TransactionProof

3. 区块结构规范

Plasma Cash 引入的最重要改进之一是“轻证明”。之前,plasma 的构建要求用户下载整个 plasma 链,以确保他们资金的安全。有了 Plasma Cash,他们只需下载与自己资金相关的 Merkle 树的分支。

这是通过引入 新的交易有效性条件 实现的:特定 coinID 的交易仅在 Merkle 树的第 coinID 个叶子上有效。因此,只需下载该分支即可确信该币没有 有效 的交易。这个方案的问题在于,交易在该面额上是“卡住”的:如果想要交易多个币,需要多个交易,即每个叶子一个交易。

不幸的是,如果我们将基于范围的交易放入常规 Merkle 树的分支中,轻证明将变得不安全。这是因为拥有一个分支并不能保证其他分支不会相交:

叶子 4 和 6 都描述了范围 (3,4) 的交易。拥有一个分支并不保证另一个分支不存在。

在常规 Merkle 树中,确保没有其他分支相交的 唯一 方法是下载 所有 分支并进行检查。但这不再是轻证明!

在我们 plasma 实现的核心是一个 新的区块结构 和一个随之而来的 新的交易有效性条件,这使我们能够获得基于范围的交易的轻证明。该区块结构称为 Merkle sum 树,在每个哈希旁边有一个 sum 值。

新的有效性条件使用特定分支的 sum 值来计算 startend 范围。这个计算经过特别设计,以使两个分支的计算范围重叠是 不可能的。** transfer 仅在其自身范围在该范围内时有效,因此这使我们重新获得了轻客户端!

本节将指定 sum 树的确切规范、范围计算的实际内容以及我们如何构建满足范围计算的树。关于我们导致这个规范的研究的更详细背景和动机,请随时查看 这个 文章。

我们已经编写了两个 plasma Merkle sum 树的实现:一个在 数据库 中为操作员,另一个在内存中供测试 plasma-utils

Sum 树节点规范

Merkle sum 树中的每个节点为 48 字节,结构如下:

[32 字节哈希][16 字节和]

sum 的 16 字节长度与 coinID 相同并不是巧合!

我们有两个辅助属性,.hash.sum,用于提取这两部分。例如,对于某个 node = 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315ffffffffffffffffffffffffffffffff,我们有

node.hash == 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315node.sum == 0xffffffffffffffffffffffffffffffff

父级计算

在常规 Merkle 树中,我们构造一个哈希节点的二叉树,直到形成一个单一的根节点。指定 sum 树格式只需定义 parent(left, right) 计算函数,该函数接受两个兄弟节点作为参数。例如,常规 Merkle sum 树有:

parent = function (left, right) { return Sha3(left.concat(right)) } 其中 Sha3 是哈希函数,concat 用于将两者连接在一起。

要创建一个 Merkle sum 树,parent 函数还必须连接对其子节点 .sum 值求和的结果:

parent = function (left, right) {
 return Sha3(left.concat(right)).concat(left.sum + right.sum)
}

例如,我们可能有

parent(0xabc…0001, 0xdef…0002) ===
hash(0xabc…0001.concat(0xdef…0002)).concat(0001 + 0002) ===
0x123…0003

注意,parent.hash 是对每个 sibling.sum 及其哈希值的承诺:我们对所有 96 字节进行哈希处理。

计算分支范围

我们使用 Merkle sum 树的原因在于它允许我们计算分支所描述的特定范围,并且可以 100% 确信没有其他有效的、重叠的分支存在。

我们通过在分支上累加 leftSumrightSum 来计算此范围。将两个值都初始化为 0,在每次父节点计算时,如果包含证明指示有右侧兄弟节点,我们取 rightSum += right.sum,如果是左侧,则取 leftSum += left.sum

然后,分支所描述的范围为 [leftSum, root.sum — rightSum)。请看下面的例子:

分支的 Merkle 和计算。

在这个例子中,分支 6 的有效范围为 [21+3, 36–5) == [24, 31)。注意到 31–24=7,这是叶子 6 的和值!同样,分支 5 的有效范围为 [21, 36-(7+5)) == [21, 24)。注意,其结束与分支 6 的开始相同!

如果你稍微玩一下,你会发现,构造一个 Merkle sum 树而有两个不同分支覆盖相同范围是不可行的。在树的某一层,和就必须是被打破的!去试试 “欺骗” 叶子 5 或 6,方法是构造另一个与范围 (4.5,6) 相交的分支。只需填写灰色框中的 ?

你会在树的某一层发现这总是不可能的:

这就是我们获得轻客户端的方式。我们称分支的范围为 implicitStartimplicitEnd,因为它们是根据包含证明 “隐式地” 计算得出的。我们在 plasma-utils 中实现了一个分支检查器,通过 calculateRootAndBounds() 进行测试和客户端证明检查:

PG Plasma 客户端侧 sum 树分支检查器

以及在 Vyper 智能合约中:

注意,这些范围是 类型化 的开始和结束,完整的 16 字节。

解析 Transfers 作为叶子

在常规 Merkle 树中,我们通过对“叶子”进行哈希处理来构造最底层的节点:

在我们的案例中,我们希望叶子是交易。因此,哈希处理是直接的,但我们仍然需要树的底层的 .sum 值。

给定某个 txA,其中包含一个 transferA,那么和的值应该是什么呢?事实证明,并不是 仅仅 transferA.end — transferA.start。这样做的原因是,如果转移不相邻,会扰乱分支的范围。我们需要对和值进行“填充”以考虑这一间隙,否则 root.sum 将会太小。

有趣的是,这是一个非确定性的选择,因为你可以对间隙右侧或左侧的节点进行填充。我们选择了以下“左对齐”方案,将叶子解析为块:

Transfer 和 sum 解析

我们将最底层的 .sum 值称为该分支的 parsedSum,而 TransferProof 结构包括了一个用于重构底层节点的 .parsedSum 值。

分支有效性和隐式 NoTx

因此,由智能合约检查分支的有效性条件如下:implicitStart <= transfer.typedStart < transfer.typedEnd <= implicitEnd。注意,在 “Plasma Cashflow” 中 sum 树的最初设计中,一些叶子填充了特殊的 “NoTx” 交易,以表示未进行任何交易的范围。采用此格式后,未进行交易的币正好是那些在范围 [implicitStart, transfer.typedStart)[transfer.typedEnd, implicitEnd) 内的币。智能合约保证这些范围内的币不会被用于任何挑战或对退出的响应。

原子多重发送

通常(为了支持交易费用和交换),交易要求多次转移要么全部发生,要么不发生,以保持有效性。其结果是,有效的交易需要为其每个 .transfers 包含一次—每个在与该 transfer.typedStart.typedEnd 具有有效和相关性。这些包含中的每个,仍然是完整的 UnsignedTransaction 的哈希—而不是单独的 Transfer—被解析到底部的 .hash

5. 证明结构和检查

与传统区块链系统不同,完整的 plasma 节点不存储每一笔交易,他们只需存储与其拥有的资产相关的信息。这意味着 sender 必须 证明recipient,发送者确实拥有给定的范围。完整的证明包含所有足够的信息,以保证,如果以太坊链本身不分叉,则代币可以在主链上赎回。

证明主要包含交易的包含和未包含,更新这些币的保管链。必须检查包含根是否与操作员提交给主链智能合约的区块哈希相符。通过在证明方案中跟踪保管链,从代币初始存款到当前,仅此能够赎回是有保障的。

plasma-core 遵循相对简单的方法来验证传入的交易证明。本节描述了这个方法。

证明格式

历史证明包含一组 存款记录 和一长串相关的 Transaction 及对应的 TransactionProof

plasma-utils 公开 一个 static checkTransactionProof(transaction, transactionProof, root) 方法,该方法在 plasma-core 这里 通过 ProofService 被调用。

交易证明

TransactionProof 对象包含检查给定 Transaction 有效性的所有必要信息。简单来说,它是 仅仅 一组 TransferProof 对象。根据上述的原子多重发送部分,给定的 TransactionProof 仅在其所有的 TransferProofs 都有效时才有效。

转移证明

TransferProofs 包含恢复与该交易在正确区块号下的给定 Transfer 相关的有效分支的包含所需的所有信息。这包括:

  • Merkle sum 树中实际的节点,它们表示分支的完整 inclusionProof
  • 为计算分支所描绘的二元路径所需的叶子索引
  • 如上文 sum 树规范所述的解析底部 .sum
  • 对于特定发送者的 signature

来自 plasma-utils 架构

注意,inclusionProof 是一个变长数组,其大小取决于树的深度。

证明步骤

验证过程的核心是将每个证明元素应用于当前的“验证”状态,开始于存款。如果任何证明元素未能导致有效状态转换,则必须拒绝该证明。

处理每个证明元素的过程很直观;我们只需按照合约的保管规则在每个区块上应用交易即可。

快照对象

我们跟踪历史拥有范围的方式称为 snapshot

简单来说,它表示在一个区块内某个范围的验证所有者:

{
  typedStart: Number,
  typedEnd: Number,
  block: Number,
  owner: address
}

存款记录

每个接收的范围必须来自相应的存款。

存款记录由其 tokenstartenddepositerblockNumber 组成。

对于每个存款记录,验证者 必须 与以太坊进行双重检查,以验证所声明的存款确实发生,且其间没有发生退出。

如果如此,一个 verifiedSnapshots 数组被初始化为这些存款,并将每个 snapshot.owner 设置为存款者。

接下来,我们应用所有给定的 TransactionProof,相应地更新 verifiedSnapshots。对于每个 transaction 和相应的 transactionProof,验证者执行以下步骤:

  1. 验证给定的证明元素有效。如果无效,则抛出错误。
  2. 对于 transaction 中的每个 transfer,执行以下操作:

    a. “拆分” 任何在 transfer.typedStarttransfer.typedEndimplicitStartimplicitEnd 处更新的快照

    b. 为所有 block 等于 transaction.blockNumber — 1verifiedSnapshots 增加 .block 编号

    c. 对于每个在 transfer.starttransfer.end 之间的拆分 snapshot

    i. 验证 snapshot.owner === transfer.from。如果不相等,抛出错误。

    ii. 设置 snapshot.owner = transfer.sender

TransactionProofs 必须按升序的 blockNumber 应用。

一旦针对所有 TransactionProof 递归应用了此操作,客户端可以自行检查她现在拥有的新币,方法是搜索所有在 verifiedSnapshots 中,blockNumber 等于当前 plasma 区块且 owner 等于她的地址。

TransactionProof 有效性

上面第一步中的交易有效性检查等同于检查智能合约的有效性条件。基于上面 sum 树规范的基本有效性检查如下:

  1. 检查交易编码是否格式良好。

  2. 对于每个 transfer 和相应的 transferProof

a. 检查 signature 是否解析为其 transfer.sender 地址

b. 验证 inclusionProof 的根是否等于该 plasma 区块的根哈希,该根的二元路径由 leafIndex 定义

c. 计算分支的 implicitStartimplicitEnd,验证 implicitStart <= transfer.start < transfer.end <= implicitEnd

4. 合约和退出游戏

当然,保管链的证明是没用的,除非它也能够传递到主链以保持资金的安全。接受链上证明的机制是 plasma 的安全模型的核心,称为“退出游戏”。

当用户希望将他们的钱从 plasma 链上转走时,他们会进行一次“退出”,这会开启一个争议期。在争议期结束时,如果没有未处理的争议,资金将从主链的 plasma 合约中发送到退出者。在争议期内,用户可以提交“挑战”,宣称正在退出的资金并不真正属于退出者。上述的证明确保对这些挑战的“响应”始终可以计算。

退出游戏的目标是确保资金安全,即使在最大程度的敌对操作员情况下。特别是,我们必须减轻三种主要攻击:

  • 数据隐匿: 操作员可以向合约发布根哈希,但不告诉任何人区块的内容。
  • 包括伪造/无效交易: 操作员可能在其保管链中将一笔其 sender 不是前一个 recipient 的交易包含在区块中。
  • 审查: 在某人存入他们的钱后,操作员可能拒绝发布任何发送这些资金的交易。

在所有这些情况下,退出游戏的挑战/响应协议确保这些行为不会允许最多在 1 次挑战之后,进行 1 次响应。

跟踪存款和退出

存款映射

每当一组新币被存入时,合约更新一个映射,每项都包含一个 deposit 结构。从合约:

注意,此结构既不包含 untypedEnd 也不包含存款的 tokenType。原因在于合约使用这些值作为映射的映射的键。例如,访问给定存款的存款者如下所示:someDepositer: address = self.deposits[tokenType][untypedEnd].depositer

这个选择节省了一些 gas,并且还让部分代码更清晰,因为我们不需要存储任何类型的存款 ID 来引用存款。

可退出范围映射

除了在每次存款时添加 self.deposits 条目外,合约还需要以某种方式跟踪历史退出,以防止在同一范围内进行多次退出。这要复杂一些,因为退出并不是像存款那样顺序进行,查询退出列表会很昂贵。

我们的合约实现了一个固定大小的解决方案,它存储一个可退出范围的列表,并在新退出发生时更新该列表。从智能合约:

同样,我们使用双重嵌套映射,其键为 tokenTypeuntypedEnd,以便可以通过 self.exitable[tokenType][untpyedEnd].untypedStart 访问范围的开始。请注意,Vyper 对于所有未设置的映射键返回 0,因此我们需要一个 isSet 布尔值,以便用户无法通过传递未设置的 exitableRange 来“欺骗”合约。

合约的 self.exitable 范围基于通过名为 removeFromExitable 的帮助函数成功调用 finalizeExit 进行拆分和删除。注意,在之前已退出的范围上的退出甚至不需要挑战;它们将永远不会通过 finalizeExit 中调用的 checkRangeExitable 测试。你可以在 这里 找到这段代码。

退出游戏与传统 Plasma Cash 的关系

从本质上讲,我们规范中的退出游戏与原始 Plasma Cash 设计非常相似。退出通过调用以下函数发起:

beginExit(tokenType: uint256, blockNumber: uint256, untypedStart: uint256, untypedEnd: uint256) -> uint256:

要对退出提出异议,所有挑战都指定一个具体的 coinID,并在该特定Coin上进行 Plasma Cash 风格的挑战游戏。只需证明一个币是无效的,就足以取消整个退出。

退出和两种可响应挑战都获得一个 exitIDchallengeID,它们按递增的 challengeNonceexitNonce 顺序分配。

基于区块号的交易

在原始的 Plasma Cash 规范中,退出者需要同时指定退出的交易及其之前的“父”交易,以防止“在途”攻击,操作员延迟包含有效交易并在其间插入无效交易。

这对我们基于范围的方案pose了问题,因为一笔交易可能有多个父级。例如,如果 Alice 将 (0, 50] 发送给 Carol,Bob 又将 (50, 100] 发送给 Carol,那么 Carol 现在可以将三个 (0, 100] 发送给 Dave。但是,如果 Dave 想退出,那两个 (0, 50](50, 100] 都是父级。

尽管指定多个父级当然是可行的,但此规范的 gas 成本较高,且实现起来似乎更复杂。因此,我们选择了更简单的替代方案,每个交易指定发件人打算将发送到的 block,并且如果包含在不同的区块中,则无效。这解决了在途攻击,并意味着合约不需要交易的父级。对于那些有兴趣了解这种方案的正式书面和安全性证明的人,可以考虑看看 这篇优秀的帖子

每个币的交易有效性

我们退出游戏的一个反直觉特性是,某个交易可能对其范围内的某些币是“有效”的,但对其他币则无效。

例如,设想 Alice 将 (0, 100] 发送给 Bob,Bob 随后将 (50, 100] 发送给 Carol。Carol 不需要 验证 Alice 是否为整个 (0, 100] 的合法拥有者。Carol 只需确认 Alice 拥有 (50, 100] — 适用于她收据的保管链的一部分。尽管如果 Alice 不拥有 (0, 50] 该交易在某种意义上是“不合法”的,但就用于针对 (50, 100] 的退出争议而言,智能合约 并不关心。只要收到的币的所有权得到验证,其余交易并无关紧要。

这是为了保持轻客户端证明的大小至关重要的要求。如果 Carol 必须检查完整的 (0, 100],她可能还必须检查 (0, 10000] 的一个重叠父级,然后再检查它的所有父级,依此类推。如果交易之间的相互依赖性很强,这种“级联”效应可能会大大增加证明的大小。

注意,这一属性同样适用于描述多个范围交换的原子多重发送。如果 Alice 用 Bob 的 1 DAI 交换 1 ETH,那 Alice 有责任在签名之前检查 Bob 是否拥有 1 DAI。然而,在此之后,如果 Bob 将 1 ETH 发送给 Carol,Carol 不需要验证 Bob 是否拥有 1 DAI,只需验证 Alice 提供给 Bob 的 1 ETH 的所有权。风险由 Alice 承担,因此 Carol 不必承担。

从智能合约的角度来看,这一属性是挑战始终在退出中针对特定的 coinID 提交的直接后果。

合约如何处理交易检查

请注意,要在退出游戏中使用,Transaction 必须通过上述证明部分描述的 TransactionProof 检查(有效签名、分支边界等)。在合约层面进行的检查如下所示:

def checkTransactionProofAndGetTypedTransfer(
   transactionEncoding: bytes[277],
   transactionProofEncoding: bytes[1749],
   transferIndex: int128
 ) -> (
   address, # transfer.to
   address, # transfer.from
   uint256, # transfer.start (typed)
   uint256, # transfer.end (typed)
   uint256 # transaction plasmaBlockNumber
 ):

这里有一个重要的注意事项是 transferIndex 参数。请记住,交易可能包含多个转移,必须为每个转移包含在树中。但由于挑战仅指向一个具体的 coinID,因此只有一个转移是相关的。因此,挑战者和响应者提供了一个 transferIndex — 该转移与争议的币相关。该检查解码并检查交易证明中的所有 TransferProof,然后使用以下函数检查每个证明:

def checkTransferProofAndGetTypedBounds(
 leafHash: bytes32,
 blockNum: uint256,
 transferProof: bytes[1749]
) -> (uint256, uint256): # typedimplicitstart, typedimplicitEnd

一旦所有 TransferProof 被验证,针对 transferIndex 的交易相关值将返回给退出游戏函数:即 senderrecipienttypedStarttypedEndplasmaBlockNumber

说完这一切,我们可以指定完整的退出挑战/响应游戏集。

立即取消退出的挑战

有两种类型的挑战可以立即取消退出:那些挑战已用的币和那些挑战存款发生前的退出。

已用币挑战

此挑战用于证明交易的退出者已将资金发送给其他人。

@public
def challengeSpentCoin(
 exitID: uint256,
 coinID: uint256,
 transferIndex: int128,
 transactionEncoding: bytes[277],
 transactionProofEncoding: bytes[1749],
):

它使用 checkTransactionProofAndGetTypedTransfer 然后检查以下条件:

  1. 挑战的 coinID 位于指定退出范围内。
  2. 挑战的 coinID 位于 transaction.transferstransferIndex 项的 typedStarttypedEnd 之间。
  3. 挑战的 plasmaBlockNumber 大于退出的。
  4. transfer.sender 是退出者。

引入原子交换意味著一旦事情发生,需要加倍确保已用币的挑战周期必须严格短于其他的,因为操作员在两个或多个方之间隐匿原子交换的边缘案例中。在这种情况下,这些方必须退出他们预_SWAP的币,迫使操作员提出已用币的挑战并表明交换是否被纳入。但如果我们允许操作员在最后时刻这么做,那将成为条件竞争,导致当事方没有时间利用公开信息去取消其他退出。因此,时限比普通挑战窗口缩短(1/2)。

存款前挑战

此挑战用于证明退出是发生在该币的实际存款之前的早期 plasmaBlockNumber 中。

@public
def challengeBeforeDeposit(
 exitID: uint256,
 coinID: uint256,
 depositUntypedEnd: uint256
):

合约查找 self.deposits[self.exits[exitID].tokenType][depositUntypedEnd].precedingPlasmaBlockNumber 并检查它是否晚于退出的块编号。如果是,则取消。

乐观退出和包含挑战

我们的合约允许在乐观情况下退出无任何包含检查。为此,任何退出都可以通过直接调用

@public
def challengeInclusion(exitID: uint256):

来挑战,退出者必须直接响应退出的交易或存款。

@public
def respondTransactionInclusion(
 challengeID: uint256,
 transferIndex: int128,
 transactionEncoding: bytes[277],
 transactionProofEncoding: bytes[1749],
):...
@public
def respondDepositInclusion(
 challengeID: uint256,
 depositEnd: uint256
):

第二个案例允许用户避免本金在存款后被操作员审查的情况下退出。

这两个响应都在以下条件下取消挑战:

  1. 存款或交易确实是在退出的 plasma 块号下。
  2. 存款者或接收者确实是退出者。
  3. 退出的开始和结束均在存款或转移的开始和结束之间。

无效历史挑战

复杂的挑战 – 响应游戏,无论是原版 Plasma Cash 还是本规范中,都是历史无效的案例。该协议的这部分减轻了操作员包括一种伪造“无效”交易的攻击,该交易的发送者不是前一个接收者。解决方案称为无效历史挑战:由于合法拥有者尚未花费他们的币,他们向其发出证明:“哦,那币是你的?那它早些时候是我的,你无法证明我曾花费它。”

无效历史挑战和响应均可通过存款或交易进行。

挑战

根据当前合法所有者,有两种挑战方式:

@public
def challengeInvalidHistoryWithTransaction(
 exitID: uint256,
 coinID: uint256,
 transferIndex: int128,
 transactionEncoding: bytes[277],
 transactionProofEncoding: bytes[1749]
):

@public
def challengeInvalidHistoryWithDeposit(
 exitID: uint256,
 coinID: uint256,
 depositUntypedEnd: uint256
):

这两者都调用:

@private
def challengeInvalidHistory(
 exitID: uint256,
 coinID: uint256,
 claimant: address,
 typedStart: uint256,
 typedEnd: uint256,
 blockNumber: uint256
):

此函数负责验证 coinID 是否在已挑战的退出范围内,并且该 blockNumber 是否早于退出。

响应无效历史挑战

当然,无效历史挑战可能是一种麻烦,其中实际的发起者确实花费了他们的 BN,并且保管链确实有效。我们必须允许这种响应。有两种类型。

第一种是通过显示发起者支出而响应的交易:

@public
def respondInvalidHistoryTransaction(
 challengeID: uint256,
 transferIndex: int128,
 transactionEncoding: bytes[277],
 transactionProofEncoding: bytes[1749],
):

智能合约然后执行以下检查:

  1. transferIndexTransfer 位于所挑战的 coinID 处。
  2. transferIndextransfer.sender 实际上是该无效历史挑战的原告。
  3. 该交易的 plasma 块号位于无效历史挑战和退出之间。

另一个响应是显示挑战发生在币实际上被存入之前,从而使挑战无效。这类似于对退出本身进行的 challengeBeforeDeposit 的操作。

@public
def respondInvalidHistoryDeposit(
 challengeID: uint256,
 depositUntypedEnd: uint256
):

在这种情况下,没有对发送者是挑战接收者的检查因为这场挑战是无效的。因此,合约只需要检查:

  1. 存款覆盖所挑战的 coinID
  2. 存款的 plasma 块号位于挑战与退出之间。

如果是,则取消退出。

这就结束了完整的退出游戏规范。凭借这些基础构件,资金即使在最大恶意的 plasma 链上也可以保持安全。

6. 未来

Plasma Group 致力于为更广泛的以太坊社区创建一个开放的 plasma 实现。我们的使命是通过探索 plasma 框架的全部潜力来推动二层扩展的未来。毫无疑问,还可以有更多事情需要推进!以下是我们希望下一个努力工作的几点。

实现中的缺失部分

自动化维护

作为一个良好的开始,需要许多改进来实现 Plasma 的真正潜力,无论在本规范及更高版本中。目前我们实施中最明显的缺失部分是维护,自动化过程,代表用户提交挑战和响应。值得庆幸的是,退出游戏本身已实施且已手动进行 测试,因此客户端软件可以在链部署后更新。我们认为这对于测试网的发布已足够,但也是代码最紧迫的补充。

P2P 历史证明

目前,当用户接收到交易时,他们向操作员请求并重新下载完整证明。这会大幅增加操作员的开销。实际上应该发生的是,发送者直接将其本地存储的证明传输给接收者,绕过操作员,使 plasma 链的运行成本大大降低。

碎片整理策略

由于我们支持原子交换,因此我们当前的规范兼容任何碎片整理策略,在合约无更新的情况下。但是,仍需找出合适的方法,特别是因为我们要求交易指定 Plasma 区块号。我们希望 plasma 社区能够建立一个可扩展的碎片整理抽象库,以允许操作员和用户尝试不同的方法。

前端钱包集成

我们有一些前端钱包的设计,但目前客户端仅支持命令行交易,并不支持不同 ERC20 的交易。提供一个良好的用户界面给测试网用户,将大幅提升用户体验和可访问性。

操作员费用

由于我们支持原子多重发送,我们可以在无需任何协议修改的情况下支持交易费用。但是,我们尚未为此测试网发布实施任何费用。

网络化操作员

我们尚未利用的一点是 Merkle 树的构造是高度并行的。如果操作员作为网络集群部署,我们可以通过并行构造子树来增加区块大小。

有谁想要体验 Raspberry plasma 吗?

代码审查

在这个时刻,非常可能 客户端、合约和操作员的实现中存在严重的错误。我们希望此次公开发布的一部分是为外部贡献者提供机会,帮助指出许多错误!

规范中的缺失部分

简洁证明方案

如前文所述,Plasma 研究中最活跃的领域是一种减少历史证明大小的方案。无论是 P2P 还是其他,老币(例如,超过 1 年)可能会有相当多的相关证明数据,使交易变得笨重。这是因为历史证明至少包含每个区块的一个分支。

RSA 累加器和 STARKS/SNARKS 目前是最有可能的候选者,它们批量整理多个区块的分支证明。两者都需要协议更改:对于 RSA(这还引入了一个受信赖的设置),退出游戏必须添加一个全新的有效性条件。对于后者,树需要使用对 SNARK/STARK 友好的哈希算法构建,但在 EVM 中未实现。大规模退出/存款方案 如果操作方真的变得恶意,用户必须(最终,不急!)退出他们的资金。“大规模退出”的概念是指许多用户同意通过单个链上交易一起退出,这将显著提高可扩展性。理想情况下,退出可以通过余额的 merkle root 自动将资金直接存入不同的 plasma 链。这将使许多用户能够切换链,而无需在主链上解决单个余额——这是多个 plasma 链“网络连通性”的显著改善。

多操作员网络

尽管操作方不能偷盗资金,但他们可以随意审查交易。一种解决办法是用一组操作员替代单一的操作员模型,使得只需存在一个诚实的操作员便足以让客户进行交易。

改进的退出记录

我们的可退出范围构造允许对退出范围进行恒定大小的检查。然而,由于每次最终确定都会更新此映射,如果对同一范围的退出未按升序处理,就会出现竞争条件。这是因为第一个退出的最终确定会分割范围,改变 self.exitable 映射的键,导致针对未分割范围的退出时 checkRangeExitable 失败。这些退出将被回退,并且必须在下一个以太坊块中重新提交。可能存在一种更为高效的替代方案,可能使用某种树、队列或某种 batchFinalizeExits 方法。

状态通道和脚本

最近,研究社区在提出使用 covenants 的状态通道和脚本的可行性方面取得了很大进展。我们当前的规范不支持这两个特性,需要对智能合约进行重大升级以支持。

让我们共同努力,朝着实现更去中心化未来的愿景迈进。

  • 原文链接: medium.com/plasma-group/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
optimismpbc
optimismpbc
江湖只有他的大名,没有他的介绍。