公开测试:以太坊的隐私和可扩展性解决方案

区块链是一项创新技术,有望改善人类生活的许多领域。 它将真实的流程和产品转移到数字空间,确保金融交易的速度和可靠性,降低其成本,还允许您使用分散网络中的智能合约创建现代DAPP应用程序。

鉴于区块链的诸多优势和用途广泛,这种有前途的技术尚未渗透到所有领域似乎有些奇怪。 问题在于现代分散式区块链缺乏可扩展性。 以太坊每秒处理大约20个事务,这不足以满足当今动态业务的需求。 同时,使用区块链技术的公司不敢放弃以太坊,因为它具有高度的防护能力,可防止黑客入侵和网络故障。

为了确保区块链上的去中心化,安全性和可扩展性,从而解决可扩展性难题,Opporty开发团队创建了Plasma Cash-Plasma Cash-一个由智能合约和基于Node.js的私有网络组成的子链,并定期将其状态转移到根链(以太坊)。



Plasma Cash的关键流程


1.用户调用智能合约“存款”功能,将以太坊金额转入其中,并希望将其放入等离子现金令牌中。 智能合约功能创建令牌并生成有关该令牌的事件。

2.订阅智能合约事件的Plasma Cash节点接收有关创建存款的事件,并将有关创建令牌的交易添加到池中。

3.定期,特殊的等离子现金节点从池中提取所有交易(最多100万),并从中形成一个块,计算Merkle树,并相应地计算哈希值。 该块被发送到其他节点进行验证。 节点检查Merkle哈希是否有效,交易是否有效(例如,令牌的发送者是其所有者)。 验证完块后,该节点调用智能合约的“ submitBlock”功能,该功能将块的编号和Merkle哈希存储在跟踪链中。 智能合约会生成有关成功添加区块的事件。 事务将从池中删除。

4.接收到有关块提交事件节点开始应用添加到该块的事务。

5.在某个时候,令牌的所有者(或非所有者)想从Plasma Cash提取令牌。 为此,他调用了“ startExit”函数,将令牌上最近2次交易的信息传递到该函数中,以确认他是令牌的所有者。 使用Merkle哈希的智能合约以块为单位检查交易并将令牌发送到输出,这将在两周内完成。

6.如果令牌撤回操作发生违规(令牌是在撤回过程开始后花掉的,或者令牌已经是撤回的陌生人),则令牌的所有者可以在两周内驳回撤回请求。



隐私可以通过两种方式实现。


1.根链对子链内部形成和转发的交易一无所知。 仍保留有关谁启动和从以太坊现金中提取以太坊的信息。

2.子链允许您使用zk-SNARK组织匿名交易。

技术栈


  • 节点JS
  • 雷迪斯
  • 以太坊
  • 弄脏了

测试中


在开发Plasma Cash时,我们测试了系统的速度并获得了以下结果:

  • 每秒最多增加35,000个事务;
  • 该区块最多可以存储1,000,000个交易。

在以下3台服务器上进行了测试:

1.英特尔酷睿i7-6700四核Skylake(含)。 NVMe SSD-512 GB,64 GB DDR4 RAM
提出了3个经过验证的“等离子现金”节点。

2. AMD Ryzen 7 1700X八核“ Summit Ridge”(Zen),SATA SSD-500 GB,64 GB DDR4 RAM
提出了Ropsten testnet ETH节点。
提出了3个经过验证的“等离子现金”节点。

3.英特尔酷睿i9-9900K八核(含)。 NVMe SSD-1 TB,64 GB DDR4 RAM
1提交等离子现金节点被提出。
提出了3个经过验证的“等离子现金”节点。
已启动测试以将交易添加到Plasma Cash网络。

总计:专用网络中有10个Plasma Cash节点。

测试1


每个区块最多只能有100万笔交易。 因此,一百万笔交易分为2个块​​(因为系统设法处理了部分交易并在发送时进行提交)。


初始状态:最后一个块#7; 数据库中存储了100万笔交易和令牌。

00:00-启动交易生成脚本
01:37-创建了100万笔交易,并开始向节点发送
01:46-提交节点从池中提取了24万笔交易,并形成了块#8。 我们还看到在10秒内将320k事务添加到池中
01:58-#8块已签名并发送以进行验证
02:03-验证了区块#8,并调用了带有Merkle哈希和区块编号的智能合约的submitBlock函数
02:10-演示脚本完成工作,在32秒内发送了100万笔交易
02:33-节点开始接收将块8添加到根链的信息,并开始执行24万笔交易
02:40-从池中删除了24万笔交易,这些交易已在第8块中
02:56-提交节点从池中提取了剩余的760k交易,并开始计算Merkle哈希和符号块9
03:20-所有节点都包含1百万个240k交易和令牌
03:35-9号块已签名并发送给其他节点进行验证
03:41-发生网络错误
04:40-超时,等待9号块确认的等待已停止
04:54-提交节点从池中提取了剩余的760k事务,并开始计算Merkle哈希和符号块9
05:32-9号块已签名并发送给其他节点进行验证
05:53-9号区块已验证并发送到根链
06:17-节点开始接收有关块9已添加到根链并开始执行760k事务的信息
06:47-清除池中第9块中的事务
09:06-所有节点都包含200万笔交易和令牌

测试2


每个块限制为350k。 结果,我们有3个区块。


初始状态:最后一个块#9; 数据库中存储了200万笔交易和令牌

00:00-交易生成脚本已在运行
00:44-创建了100万笔交易,并开始向节点发送
00:56-提交节点从池中提取了320k笔交易,并形成了块#10。 我们还看到在10秒内将320k事务添加到池中
01:12-#10块已签名并发送到其他节点进行验证
01:18-演示脚本完成工作,在34秒内发送了100万笔交易
01:20-区块#10经过验证并发送到根链
01:51-所有节点都从根链收到了已添加第10块的信息,并且它们开始应用320k事务
02:01-已清除池中的320k事务,这些事务已添加到第10块
02:15-提交节点从池中获取了35万笔交易,并形成了11号区块
02:34-区块#11已签名并发送到其他节点进行验证
02:51-区块#11经过验证并发送到根链
02:55-最后一个节点执行了块10中的事务
10:59-在根链中很长的时间里,执行了带有第9块提交的事务,但是该事务已完成,并且所有节点都接收到有关此事务的信息并开始执行35万笔事务
11:05-已清除池中的320k事务,这些事务已添加到11号区块
12:10-所有节点包含一百万个670k交易和令牌
12:17-提交节点从池中获取了330k笔交易,并形成了第12个区块
12:32-块#12已签名并发送到其他节点进行验证
12:39-验证了#12块并将其发送到根链
13:44-所有节点都从根链收到了已添加第12块信息,并开始应用330k事务
14:50-所有节点都包含200万笔交易和令牌

测试3


在第一台和第二台服务器中,一个验证节点被替换为提交节点。


初始状态:最后一个区块#84; 数据库中存储了0个交易和令牌

00:00-启动3个脚本,生成并发送1百万笔交易
01:38-创建了100万笔交易,并开始发送以提交3号节点
01:50-提交节点3从池中提取了33万笔交易,并形成了模块85(f21)。 我们还看到在10秒内将35万笔交易添加到池中
01:53-创建了100万笔交易,并开始发送以提交节点1
01:50-提交节点3从池中提取了33万笔交易,并形成了模块85(f21)。 我们还看到在10秒内将35万笔交易添加到池中
02:01-提交节点1从池中接收了25万笔交易,并形成了区块85(65e)
02:06-块#85(f21)已签名并发送到其他节点进行验证
02:08-3号服务器演示脚本已完成工作,在30秒内发送了100万笔交易
02:14-验证块#85(f21)并将其发送到根链
02:19-区块#85(65e)已签名并发送到其他节点进行验证
02:22-创建了100万笔交易,并开始发送以提交节点2
02:27-验证区块#85(65e)并将其发送到根链
02:29-提交节点2从池111855中进行交易,并形成模块85(256)。
02:36-区块#85(256)已签名并发送到其他节点进行验证
02:36-1号服务器的演示脚本完成工作,在42.5秒内发送了100万笔交易
02:38-验证区块#85(256)并将其发送到根链
03:08-服务器脚本2(在47秒内发送了100万笔交易)完成工作
03:38-所有节点均从根链接收到以下信息:已添加块#85(f21),#86(65e),#87(256)并开始应用330k,250k,111855事务
03:49-清除了池中的330k,250k,111855个事务,这些事务已添加到#85(f21),#86(65e),#87(256)块中
03:59-提交节点#1从池888145事务和表单块88(214)中获取,提交节点#2从池750k事务和表单块88(50a)中获取,提交节点#3从池670k事务中获取和表格#88(d3b)
04:44-块#88(d3b)已签名并发送到其他节点进行验证
04:58-区块#88(214)已签名并发送到其他节点进行验证
05:11-#88(50a)块已签名并发送到其他节点进行验证
05:11-验证区块#85(d3b)并将其发送到根链
05:36-验证区块#85(214)并将其发送到根链
05:43-所有节点都从根链接收到信息,该信息已添加了区块#88(d3b)和#89(214),并开始应用670k,750k交易
06:50-由于断开连接,未验证块#85(50a)
06:55-提交节点2从池中提取了888145笔交易,并形成了模块90(50a)
08:14-#90(50a)块已签名并发送到其他节点进行验证
09:04-验证#90(50a)块并将其发送到根链
11:23-所有节点都从根链收到了添加了块#90(50a)的信息,并且开始应用888145事务。 同时,服务器#3长时间应用了来自块#88(d3b),#89(214)的事务
12:11-所有池都是空的
13:41-所有服务器节点3包含300万笔交易和令牌
14:35-所有服务器节点1包含300万笔交易和令牌
19:24-所有2号服务器节点都包含300万笔交易和令牌

障碍物


在开发Plasma Cash的过程中,我们遇到了以下问题,这些问题已逐步解决并正在解决:

1.系统各种功能相互作用的冲突。 例如,将事务添加到池中的功能阻止了块的提交和验证,反之亦然,这导致速度下降。

2.目前尚不清楚如何发送大量交易,同时如何将数据传输成本降至最低。

3.尚不清楚如何以及在何处存储数据以取得较高的结果。

4.目前尚不清楚如何在节点之间组织网络,因为具有100万个事务的块大小大约需要100 MB。

5.在进行长计算时(例如,构建Merkle树并计算其哈希),以单线程模式工作会断开节点之间的连接。

我们如何处理所有这一切?


Plasma Cash节点的第一个版本是一种可以同时进行所有操作的组合:接受交易,提交和验证区块,提供用于访问数据的API。 由于NodeJS最初是单线程的,因此繁重的Merkle树计算功能阻止了添加事务功能。 我们看到了两种解决此问题的方法:

1.运行多个NodeJS进程,每个进程执行某些功能。

2.使用worker_threads并将代码的执行放入线程中。

结果,我们同时使用了这两个选项:在逻辑上将一个节点划分为3个部分,这些部分可以分别工作,但同时可以同步工作

1.将一个接受事务的节点提交到池中并创建块。

2.验证节点,以验证节点的有效性。

3.节点API-提供用于访问数据的API。

同时,您可以使用cli通过unix套接字连接到每个节点。

我们在单独的流中进行了繁重的操作,例如Merkle树的计算。

因此,我们同时实现了所有Plasma Cash功能的正常运行,而没有出现故障。

系统正常运行后,我们开始测试速度,但不幸的是,结果令人不满意:每秒5,000个事务,每个区块最多50,000个事务。 我必须找出未正确执行的内容。

首先,我们开始测试与Plasma Cash的通信机制,以找出系统的峰值能力。 早些时候,我们写道Plasma Cash节点提供了unix套接字接口。 它最初是文字的。 JSON对象是使用JSON.parse()和JSON.stringify()发送的。

```json { "action": "sendTransaction", "payload":{ "prevHash": "0x8a88cc4217745fd0b4eb161f6923235da10593be66b841d47da86b9cd95d93e0", "prevBlock": 41, "tokenId": "57570139642005649136210751546585740989890521125187435281313126554130572876445", "newOwner": "0x200eabe5b26e547446ae5821622892291632d4f4", "type": "pay", "data": "", "signature": "0xd1107d0c6df15e01e168e631a386363c72206cb75b233f8f3cf883134854967e1cd9b3306cc5c0ce58f0a7397ae9b2487501b56695fe3a3c90ec0f61c7ea4a721c" } } ``` 

我们测量了此类物体的传输速度,每秒收到约130k的信号。 他们试图用json替换标准函数,但是性能没有提高。 必须为这些操作优化V8引擎。

交易,代币,区块的工作是通过类进行的。 创建此类时,性能下降了2倍,这表明:OOP不适合我们。 我必须用纯功能方法重写所有内容。

写入数据库


最初,Redis被选作数据存储,是满足我们要求的最高效的解决方案之一:键值存储,使用哈希表等。 我们启动了redis-benchmark,在1种流水线模式下每秒可进行约80k次操作。

为了获得高性能,我们对Redis进行了更精细的调整:

  • 建立了unix套接字连接。
  • 禁用状态保存到磁盘的功能(出于可靠性考虑,您可以配置副本并在单独的Redis中保存到磁盘)。

在Redis中,池是一个哈希表,因为我们需要能够接收一个请求中的所有事务并逐个删除事务的能力。 我们尝试使用常规列表,但是在卸载整个列表时,它的工作速度较慢。

使用标准的NodeJS库,Redis库每秒可实现18k事务处理。 速度下降了9倍。

自从基准向我们清楚地向我们展示了5倍的可能性之后,它们就开始进行优化。 我们将库更改为ioredis,并获得了每秒25k的性能。 我们使用“ hset”命令一个接一个地添加了事务。 因此,我们在Redis中产生了许多请求。 有一种想法可以将事务合并到捆绑包中,然后使用一个hmset命令发送它们。 结果是每秒32k。

由于以下几个原因,我们将使用`Buffer`处理数据,事实证明,如果在写入之前将其转换为文本(`buffer.toString('hex')`),则可以获得更高的性能。 因此,速度增加到每秒35k。 目前,我们决定暂停进一步的优化。

我们必须切换到二进制协议,因为:

1.系统通常会计算哈希,签名等,为此,它需要将数据存储在“缓冲区”中。

2.在服务之间传输时,二进制数据的权重小于文本。 例如,当发送具有一百万个事务的块时,文本中的数据可能会占用300兆字节以上。

3.连续数据转换会影响性能。

因此,我们在奇妙的二进制数据库的基础上开发了自己的二进制协议,用于存储和传输数据。

结果,我们具有以下数据结构:

-交易


  ```json { prevHash: BD.types.buffer(20), prevBlock: BD.types.uint24le, tokenId: BD.types.string(null), type: BD.types.uint8, newOwner: BD.types.buffer(20), dataLength: BD.types.uint24le, data: BD.types.buffer(({current}) => current.dataLength), signature: BD.types.buffer(65), hash: BD.types.buffer(32), blockNumber: BD.types.uint24le, timestamp: BD.types.uint48le, } ``` 

-代币


  ```json { id: BD.types.string(null), owner: BD.types.buffer(20), block: BD.types.uint24le, amount: BD.types.string(null), } ``` 

-块


  ```json { number: BD.types.uint24le, merkleRootHash: BD.types.buffer(32), signature: BD.types.buffer(65), countTx: BD.types.uint24le, transactions: BD.types.array(Transaction.Protocol, ({current}) => current.countTx), timestamp: BD.types.uint48le, } ``` 

通过通常的命令“ BD.encode(块,协议).slice();”和“ BD.decode(缓冲区,协议)”,我们将数据转换为“ Buffer”以将其保存到Redis或发送另一个节点并取回数据。

我们还有2个二进制协议,用于在服务之间传输数据:

-通过unix套接字与Plasma Node交互的协议

  ```json { type: BD.types.uint8, messageId: BD.types.uint24le, error: BD.types.uint8, length: BD.types.uint24le, payload: BD.types.buffer(({node}) => node.length) } ``` 

其中:

  • type-要执行的动作,例如1-sendTransaction,2-getTransaction;
  • 有效载荷 -要传输到相应功能的数据;
  • `messageId`-消息ID,以便可以识别响应。

-节点之间的交互协议

  ```json { code: BD.types.uint8, versionProtocol: BD.types.uint24le, seq: BD.types.uint8, countChunk: BD.types.uint24le, chunkNumber: BD.types.uint24le, length: BD.types.uint24le, payload: BD.types.buffer(({node}) => node.length) } ``` 

其中:

  • 代码-消息代码,例如6-PREPARE_NEW_BLOCK,7-BLOCK_VALID,8-BLOCK_COMMIT;
  • `versionProtocol`-协议版本,因为具有不同版本的节点可以在网络上引发,并且它们可以以不同的方式工作;
  • seq-消息标识符;
  • 需要`countChunk`和` chunkNumber`来拆分大消息;
  • `length`和` payload`长度和数据本身。

由于我们事先输入了数据,因此最终系统比以太坊的`rlp`库快得多。 不幸的是,我们尚未拒绝它,因为有必要最终敲定我们计划在未来执行的智能合约。

如果我们设法达到每秒35,000个事务的速度,我们还需要在最佳时间内处理它们。 由于该块的大致形成时间需要30秒,因此我们需要在该块中包括1,000,000个事务,这意味着发送100 mb以上的数据。

最初,我们使用“ ethereumjs-devp2p”库来通信节点,但是它不能处理那么多数据。 结果,我们使用了ws库并在websocket上设置了二进制数据传输。 当然,在发送大数据包时,我们也遇到了问题,但是我们将它们分为多个块,现在没有此类问题。

同样,Merkle树的形成和1,000,000个事务的哈希计算需要大约10秒的连续计算。 在这段时间内,与所有节点的连接设法断开。 决定将此计算转移到单独的线程。

结论:


实际上,我们的发现并不是什么新发现,但是由于某些原因,许多专家在开发过程中都忘记了它们。

  • 使用函数式编程而不是面向对象的编程可提高性能。
  • 对于NodeJS上的生产系统而言,整体而言比服务体系更糟糕。
  • 使用`worker_threads`进行大量计算可以改善系统的响应能力,尤其是在使用I / O操作时。
  • unix套接字比http请求更稳定,更快。
  • 如果需要通过网络快速传输大量数据,最好使用websocket并发送分解成大块的二进制数据,如果无法到达则可以转发,然后合并为一条消息。

我们邀请您访问GitHub项目: https//github.com/opporty-com/Plasma-Cash/tree/new-version

这篇文章是由Clever Solution Inc.的高级开发人员Alexander Nashivan共同撰写的

Source: https://habr.com/ru/post/zh-CN471096/


All Articles