本文介绍了一种基于Plasma Cash的区块链扩容解决方案,包含了设计规范和Node.js及Vyper的实现细节。文中详细讨论了该协议的属性、区块结构、交易和证明检查机制等关键部分,并展示了相关代码和架构,作者希望通过这一实现推动以太坊社区的Layer 2扩展进步。
TLDR:我们创建了一个 Plasma Cash 变种的规范,并在 Node.js 和 Vyper 中实现了它。本文涵盖了设计规范,同时提供了实施过程中的参考。我们的代码支持在测试网部署新链、其他 Plasma 链及其区块探测器的链上注册,以及通过命令行钱包进行交易。
区块链网络作为可扩展解决方案的愿景正在迅速传播。通过多链方法来并行处理交易是提高吞吐量的一种有希望的方法……但不幸的是,这也带来了重大挑战:
我们需要一个可扩展性的解决方案:
我们相信,满足这些标准的最强候选者是一个链的网络,每条链通过 Plasma 框架连接到主网。
Plasma 是一系列协议,允许个人轻松部署高吞吐量、安全的区块链。Ethereum 主链上的智能合约可以确保用户的资金安全,即使“plasma 链”完全恶意行为。这样便消除了像侧链那样需要可信锚定机制的必要。Plasma 链是非托管的,允许在不牺牲安全性的基础上优先考虑可扩展性。
我们设想一个拥有多个 Plasma 链的未来,让用户可以选择交易的地方。因此,除了发布我们的 plasma 链实现外,我们还创建了一个 PlasmaRegistry.vy
。该注册表允许新的链通过列出其 IP/DNS 地址、自定义的“名称”字符串和其合约地址来加入网络。注册表合约验证受信任的部署,因此用户可以放心将任何合约存入该注册表 —— 即使其运营者存在恶意行为。
这篇文章指定了 Plasma Group 当前的协议和实现,吸取了研究社区的最新进展。
我们的规范具有以下特性:
我们的实现遵循上述规范,提供以下功能:
如果你对协议和代码实现感兴趣,你来对地方了!
然而,在深入探讨之前,有几点免责声明:
说完这些,我们开始吧!本文的其余部分将全面深入探讨我们的规范、代码的位置和其功能。
我们的 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.vy
和 PlasmaRegistry.vy
的 Vyper 合约。plasma-explorer
: 由操作员托管的区块探测器。plasma-utils
: 在我们的 plasma 规范上构建的共享工具。plasma
: 对上述组件的集成测试。这是 plasma-core
实现的架构:
这是 plasma-chain-operator
实现的架构:
本节将涵盖协议组件的术语和直觉。这些数据结构由 plasma-utils
库的 serialization
编码和解码。每个结构的所有数据结构的确切字节表示可以在 schemas 中找到。
任何 plasma 资产的基本单位表示为一枚Coin。就像在标准 Plasma Cash 中,这些Coin是非同质化的,我们称一个Coin的索引为其 coinID
,它是16个字节。它们根据在每种资产(ERC 20/ETH)上的存款顺序分配。值得注意的是,链中的所有资产共享同一个 ID 空间,即使它们是不同的 ERC20 或 ETH。这意味着跨所有资产类别的交易(我们称之为 tokenType
或 token
)共享同一树,以提供最大的压缩。
我们通过让前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之间的小数位数偏移。这些计算可以在 智能合约 中的 depositERC20
、depositETH
和 finalizeExit
函数中找到。
//注意: decimalOffset
在此版本中被硬编码为0,因为在客户端/操作员代码中缺少支持。
一个交易由指定的 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 | |
} | |
... |
我们可以看到,Transaction
中的每个 Transfer
都指定了 tokenType
、start
、end
、sender
和 recipient
。
上面需要注意的一点是,start
和 end
的值不是 16 个字节,正如 coinID
,而是 12 个字节。这在上面关于存款的部分应该是明了的。要获取转账所描述的实际 coinID
,我们将 token
字段的4个字节连接到 start
和 end
的左侧。我们通常将12个字节版本称为转账的 untypedStart
和 untypedEnd
,而连接版本被称为 typedStart
和 typedEnd
。这些值 也被序列化程序暴露。另一条说明:在任何转移中,相应的 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
。
Transaction
结构由一个 4 字节的 block
编号组成(该交易只有在包含于那个特定的 plasma 区块中时才有效),以及一个 数组 的 Transfer
对象。这意味着一个交易可以描述多个转移,所有这些转移要么全部原子性地执行,要么不执行,取决于 整个交易 的包含和有效性。这将为后续版本的去中心化交易和 碎片整理 打下基础。
如上所示,plasma-utils
实现了一个自定义的序列化库用于数据结构。JSON RPC 和智能合约均使用序列化器编码的字节数组。
编码相当简单,是将每个值按 schema 定义的字节数串联而成。
对于涉及可变大小数组的编码,例如包含一个或多个 Transfer
的 Transaction
对象,前面会有一个字节用于表示元素的数量。序列化库的测试可以在 此处 找到。
目前,我们有以下对象的结构:
Transfer
UnsignedTransaction
Signature
SignedTransaction
TransferProof
TransactionProof
Plasma Cash 引入的最重要改进之一是“轻证明”。之前,plasma 的构建要求用户下载整个 plasma 链,以确保他们资金的安全。有了 Plasma Cash,他们只需下载与自己资金相关的 Merkle 树的分支。
这是通过引入 新的交易有效性条件 实现的:特定 coinID
的交易仅在 Merkle 树的第 coinID
个叶子上有效。因此,只需下载该分支即可确信该币没有 有效 的交易。这个方案的问题在于,交易在该面额上是“卡住”的:如果想要交易多个币,需要多个交易,即每个叶子一个交易。
不幸的是,如果我们将基于范围的交易放入常规 Merkle 树的分支中,轻证明将变得不安全。这是因为拥有一个分支并不能保证其他分支不会相交:
叶子 4 和 6 都描述了范围 (3,4) 的交易。拥有一个分支并不保证另一个分支不存在。
在常规 Merkle 树中,确保没有其他分支相交的 唯一 方法是下载 所有 分支并进行检查。但这不再是轻证明!
在我们 plasma 实现的核心是一个 新的区块结构 和一个随之而来的 新的交易有效性条件,这使我们能够获得基于范围的交易的轻证明。该区块结构称为 Merkle sum 树,在每个哈希旁边有一个 sum
值。
新的有效性条件使用特定分支的 sum
值来计算 start
和 end
范围。这个计算经过特别设计,以使两个分支的计算范围重叠是 不可能的。** transfer
仅在其自身范围在该范围内时有效,因此这使我们重新获得了轻客户端!
本节将指定 sum 树的确切规范、范围计算的实际内容以及我们如何构建满足范围计算的树。关于我们导致这个规范的研究的更详细背景和动机,请随时查看 这个 文章。
我们已经编写了两个 plasma Merkle sum 树的实现:一个在 数据库 中为操作员,另一个在内存中供测试 用 的 plasma-utils
。
Merkle sum 树中的每个节点为 48 字节,结构如下:
[32 字节哈希][16 字节和]
sum
的 16 字节长度与 coinID
相同并不是巧合!
我们有两个辅助属性,.hash
和 .sum
,用于提取这两部分。例如,对于某个 node = 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315ffffffffffffffffffffffffffffffff
,我们有
node.hash == 0x1b2e79791f28c27ed669f257397e1deb3e522cf1f27024c161b619d276a25315
和 node.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% 确信没有其他有效的、重叠的分支存在。
我们通过在分支上累加 leftSum
和 rightSum
来计算此范围。将两个值都初始化为 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) 相交的分支。只需填写灰色框中的 ?
:
你会在树的某一层发现这总是不可能的:
这就是我们获得轻客户端的方式。我们称分支的范围为 implicitStart
和 implicitEnd
,因为它们是根据包含证明 “隐式地” 计算得出的。我们在 plasma-utils
中实现了一个分支检查器,通过 calculateRootAndBounds()
进行测试和客户端证明检查:
PG Plasma 客户端侧 sum 树分支检查器
以及在 Vyper 智能合约中:
注意,这些范围是 类型化 的开始和结束,完整的 16 字节。
在常规 Merkle 树中,我们通过对“叶子”进行哈希处理来构造最底层的节点:
在我们的案例中,我们希望叶子是交易。因此,哈希处理是直接的,但我们仍然需要树的底层的 .sum
值。
给定某个 txA
,其中包含一个 transferA
,那么和的值应该是什么呢?事实证明,并不是 仅仅 transferA.end — transferA.start
。这样做的原因是,如果转移不相邻,会扰乱分支的范围。我们需要对和值进行“填充”以考虑这一间隙,否则 root.sum
将会太小。
有趣的是,这是一个非确定性的选择,因为你可以对间隙右侧或左侧的节点进行填充。我们选择了以下“左对齐”方案,将叶子解析为块:
Transfer 和 sum 解析
我们将最底层的 .sum
值称为该分支的 parsedSum
,而 TransferProof
结构包括了一个用于重构底层节点的 .parsedSum
值。
因此,由智能合约检查分支的有效性条件如下:implicitStart <= transfer.typedStart < transfer.typedEnd <= implicitEnd
。注意,在 “Plasma Cashflow” 中 sum 树的最初设计中,一些叶子填充了特殊的 “NoTx” 交易,以表示未进行任何交易的范围。采用此格式后,未进行交易的币正好是那些在范围 [implicitStart, transfer.typedStart)
和 [transfer.typedEnd, implicitEnd)
内的币。智能合约保证这些范围内的币不会被用于任何挑战或对退出的响应。
通常(为了支持交易费用和交换),交易要求多次转移要么全部发生,要么不发生,以保持有效性。其结果是,有效的交易需要为其每个 .transfers
包含一次—每个在与该 transfer.typedStart
和 .typedEnd
具有有效和相关性。这些包含中的每个,仍然是完整的 UnsignedTransaction
的哈希—而不是单独的 Transfer
—被解析到底部的 .hash
。
与传统区块链系统不同,完整的 plasma 节点不存储每一笔交易,他们只需存储与其拥有的资产相关的信息。这意味着 sender
必须 证明 给 recipient
,发送者确实拥有给定的范围。完整的证明包含所有足够的信息,以保证,如果以太坊链本身不分叉,则代币可以在主链上赎回。
证明主要包含交易的包含和未包含,更新这些币的保管链。必须检查包含根是否与操作员提交给主链智能合约的区块哈希相符。通过在证明方案中跟踪保管链,从代币初始存款到当前,仅此能够赎回是有保障的。
plasma-core
遵循相对简单的方法来验证传入的交易证明。本节描述了这个方法。
历史证明包含一组 存款记录 和一长串相关的 Transaction
及对应的 TransactionProof
。
plasma-utils
公开 一个 static checkTransactionProof(transaction, transactionProof, root)
方法,该方法在 plasma-core
这里 通过 ProofService
被调用。
TransactionProof
对象包含检查给定 Transaction
有效性的所有必要信息。简单来说,它是 仅仅 一组 TransferProof
对象。根据上述的原子多重发送部分,给定的 TransactionProof
仅在其所有的 TransferProofs
都有效时才有效。
TransferProofs
包含恢复与该交易在正确区块号下的给定 Transfer
相关的有效分支的包含所需的所有信息。这包括:
inclusionProof
.sum
值signature
。来自 plasma-utils
架构:
注意,inclusionProof
是一个变长数组,其大小取决于树的深度。
验证过程的核心是将每个证明元素应用于当前的“验证”状态,开始于存款。如果任何证明元素未能导致有效状态转换,则必须拒绝该证明。
处理每个证明元素的过程很直观;我们只需按照合约的保管规则在每个区块上应用交易即可。
我们跟踪历史拥有范围的方式称为 snapshot
。
简单来说,它表示在一个区块内某个范围的验证所有者:
{
typedStart: Number,
typedEnd: Number,
block: Number,
owner: address
}
每个接收的范围必须来自相应的存款。
存款记录由其 token
、start
、end
、depositer
和 blockNumber
组成。
对于每个存款记录,验证者 必须 与以太坊进行双重检查,以验证所声明的存款确实发生,且其间没有发生退出。
如果如此,一个 verifiedSnapshots
数组被初始化为这些存款,并将每个 snapshot.owner
设置为存款者。
接下来,我们应用所有给定的 TransactionProof
,相应地更新 verifiedSnapshots
。对于每个 transaction
和相应的 transactionProof
,验证者执行以下步骤:
对于 transaction
中的每个 transfer
,执行以下操作:
a. “拆分” 任何在 transfer.typedStart
、transfer.typedEnd
、implicitStart
和 implicitEnd
处更新的快照
b. 为所有 block
等于 transaction.blockNumber — 1
的 verifiedSnapshots
增加 .block
编号
c. 对于每个在 transfer.start
和 transfer.end
之间的拆分 snapshot
:
i. 验证 snapshot.owner === transfer.from
。如果不相等,抛出错误。
ii. 设置 snapshot.owner = transfer.sender
。
TransactionProofs
必须按升序的 blockNumber
应用。
一旦针对所有 TransactionProof
递归应用了此操作,客户端可以自行检查她现在拥有的新币,方法是搜索所有在 verifiedSnapshots
中,blockNumber
等于当前 plasma 区块且 owner
等于她的地址。
上面第一步中的交易有效性检查等同于检查智能合约的有效性条件。基于上面 sum 树规范的基本有效性检查如下:
检查交易编码是否格式良好。
对于每个 transfer
和相应的 transferProof
:
a. 检查 signature
是否解析为其 transfer.sender
地址
b. 验证 inclusionProof
的根是否等于该 plasma 区块的根哈希,该根的二元路径由 leafIndex
定义
c. 计算分支的 implicitStart
和 implicitEnd
,验证 implicitStart <= transfer.start < transfer.end <= implicitEnd
当然,保管链的证明是没用的,除非它也能够传递到主链以保持资金的安全。接受链上证明的机制是 plasma 的安全模型的核心,称为“退出游戏”。
当用户希望将他们的钱从 plasma 链上转走时,他们会进行一次“退出”,这会开启一个争议期。在争议期结束时,如果没有未处理的争议,资金将从主链的 plasma 合约中发送到退出者。在争议期内,用户可以提交“挑战”,宣称正在退出的资金并不真正属于退出者。上述的证明确保对这些挑战的“响应”始终可以计算。
退出游戏的目标是确保资金安全,即使在最大程度的敌对操作员情况下。特别是,我们必须减轻三种主要攻击:
sender
不是前一个 recipient
的交易包含在区块中。在所有这些情况下,退出游戏的挑战/响应协议确保这些行为不会允许最多在 1 次挑战之后,进行 1 次响应。
存款映射
每当一组新币被存入时,合约更新一个映射,每项都包含一个 deposit
结构。从合约:
注意,此结构既不包含 untypedEnd
也不包含存款的 tokenType
。原因在于合约使用这些值作为映射的映射的键。例如,访问给定存款的存款者如下所示:someDepositer: address = self.deposits[tokenType][untypedEnd].depositer
这个选择节省了一些 gas,并且还让部分代码更清晰,因为我们不需要存储任何类型的存款 ID 来引用存款。
可退出范围映射
除了在每次存款时添加 self.deposits
条目外,合约还需要以某种方式跟踪历史退出,以防止在同一范围内进行多次退出。这要复杂一些,因为退出并不是像存款那样顺序进行,查询退出列表会很昂贵。
我们的合约实现了一个固定大小的解决方案,它存储一个可退出范围的列表,并在新退出发生时更新该列表。从智能合约:
同样,我们使用双重嵌套映射,其键为 tokenType
和 untypedEnd
,以便可以通过 self.exitable[tokenType][untpyedEnd].untypedStart
访问范围的开始。请注意,Vyper 对于所有未设置的映射键返回 0,因此我们需要一个 isSet
布尔值,以便用户无法通过传递未设置的 exitableRange
来“欺骗”合约。
合约的 self.exitable
范围基于通过名为 removeFromExitable
的帮助函数成功调用 finalizeExit
进行拆分和删除。注意,在之前已退出的范围上的退出甚至不需要挑战;它们将永远不会通过 finalizeExit
中调用的 checkRangeExitable
测试。你可以在 这里 找到这段代码。
从本质上讲,我们规范中的退出游戏与原始 Plasma Cash 设计非常相似。退出通过调用以下函数发起:
beginExit(tokenType: uint256, blockNumber: uint256, untypedStart: uint256, untypedEnd: uint256) -> uint256:
要对退出提出异议,所有挑战都指定一个具体的 coinID
,并在该特定Coin上进行 Plasma Cash 风格的挑战游戏。只需证明一个币是无效的,就足以取消整个退出。
退出和两种可响应挑战都获得一个 exitID
和 challengeID
,它们按递增的 challengeNonce
和 exitNonce
顺序分配。
在原始的 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
的交易相关值将返回给退出游戏函数:即 sender
、recipient
、typedStart
、typedEnd
和 plasmaBlockNumber
。
说完这一切,我们可以指定完整的退出挑战/响应游戏集。
有两种类型的挑战可以立即取消退出:那些挑战已用的币和那些挑战存款发生前的退出。
已用币挑战
此挑战用于证明交易的退出者已将资金发送给其他人。
@public
def challengeSpentCoin(
exitID: uint256,
coinID: uint256,
transferIndex: int128,
transactionEncoding: bytes[277],
transactionProofEncoding: bytes[1749],
):
它使用 checkTransactionProofAndGetTypedTransfer
然后检查以下条件:
transaction.transfers
的 transferIndex
项的 typedStart
和 typedEnd
之间。plasmaBlockNumber
大于退出的。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
):
第二个案例允许用户避免本金在存款后被操作员审查的情况下退出。
这两个响应都在以下条件下取消挑战:
复杂的挑战 – 响应游戏,无论是原版 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],
):
智能合约然后执行以下检查:
transferIndex
的 Transfer
位于所挑战的 coinID
处。transferIndex
的 transfer.sender
实际上是该无效历史挑战的原告。另一个响应是显示挑战发生在币实际上被存入之前,从而使挑战无效。这类似于对退出本身进行的 challengeBeforeDeposit
的操作。
@public
def respondInvalidHistoryDeposit(
challengeID: uint256,
depositUntypedEnd: uint256
):
在这种情况下,没有对发送者是挑战接收者的检查因为这场挑战是无效的。因此,合约只需要检查:
coinID
。如果是,则取消退出。
这就结束了完整的退出游戏规范。凭借这些基础构件,资金即使在最大恶意的 plasma 链上也可以保持安全。
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!