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

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

考虑到区块链的诸多好处和多样化的应用,这项有前途的技术尚未进入每个行业似乎令人惊讶。 问题在于现代去中心化区块链缺乏可扩展性。 以太坊每秒处理约 20 笔交易,这不足以满足当今动态业务的需求。 与此同时,由于以太坊对黑客攻击和网络故障的高度保护,使用区块链技术的公司对于放弃以太坊犹豫不决。

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

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

Plasma Cash 的关键流程

1. 用户调用智能合约函数“deposit”,将他想要存入 Plasma Cash 代币的 ETH 数量传递给它。 智能合约功能创建一个令牌并生成一个有关它的事件。

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

3. 特殊的 Plasma Cash 节点会定期从池中获取所有交易(最多 1 万笔),并从中形成一个区块,计算 Merkle 树,并相应地计算哈希值。 该区块被发送到其他节点进行验证。 节点检查 Merkle 哈希是否有效以及交易是否有效(例如,代币的发送者是否是其所有者)。 验证区块后,节点调用智能合约的“submitBlock”函数,该函数将区块号和 Merkle 哈希保存到边缘链上。 智能合约会生成一个事件,指示成功添加块。 交易将从池中删除。

4. 接收到区块提交事件的节点开始应用添加到区块中的交易。

5. 在某些时候,代币的所有者(或非所有者)想要从 Plasma Cash 中提取它。 为此,他调用“startExit”函数,向其中传递有关令牌的最后 2 笔交易的信息,这确认他是令牌的所有者。 智能合约使用 Merkle 哈希检查区块中是否存在交易,并发送令牌以进行提现,这将在两周内发生。

6. 如果提现操作存在违规情况(提现程序开始后代币已被花掉或提现前代币已被他人使用),代币所有者可以在两周内反驳提现。

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

隐私通过两种方式实现

1. 根链对子链内生成和转发的交易一无所知。 有关谁从 Plasma Cash 存入和提取 ETH 的信息仍然是公开的。

2. 子链允许使用 zk-SNARK 进行匿名交易。

技术栈

  • 的NodeJS
  • Redis的
  • Etherium
  • 实芯

测试

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

  • 每秒最多可将 35 个交易添加到池中;
  • 一个区块中最多可以存储 1 笔交易。

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

1. Intel Core i7-6700 四核 Skylake,包括。 NVMe 固态硬盘 – 512 GB、64 GB DDR4 内存
筹集了 3 个验证 Plasma Cash 节点。

2. AMD Ryzen 7 1700X 八核“Summit Ridge”(Zen),SATA SSD – 500 GB,64 GB DDR4 RAM
Ropsten 测试网 ETH 节点已启动。
筹集了 3 个验证 Plasma Cash 节点。

3. 英特尔酷睿 i9-9900K 八核,包括。 NVMe SSD – 1 TB、64 GB DDR4 RAM
1 个 Plasma Cash 提交节点被发起。
筹集了 3 个验证 Plasma Cash 节点。
启动了向 Plasma Cash 网络添加交易的测试。

合计: 专用网络中的 10 个 Plasma Cash 节点。

测试 1

每个区块的交易限额为 1 万笔。 因此,1 万笔交易分为 2 个区块(因为系统设法获取部分交易并在发送时提交)。


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

00:00 — 交易生成脚本开始
01:37 - 创建了 1 万笔交易并开始发送到节点
01:46 — 提交节点从池中取出 240 万笔交易并形成区块#8。 我们还看到 320 秒内有 10k 笔交易添加到池中
01:58 — 区块 #8 已签名并发送进行验证
02:03 — 区块 #8 得到验证,并使用 Merkle 哈希值和区块编号调用智能合约的“submitBlock”函数
02:10 — 演示脚本完成工作,在 1 秒内发送了 32 万笔交易
02:33 - 节点开始接收区块#8被添加到根链的信息,并开始执行240k交易
02:40 - 240k 笔交易已从池中删除,这些交易已经在区块 #8 中
02:56 — 提交节点从池中取出剩余的 760 万笔交易并开始计算 Merkle 哈希并签署区块 #9
03:20 - 所有节点包含 1 万 240k 交易和代币
03:35 — 区块 #9 被签名并发送到其他节点进行验证
03:41 - 发生网络错误
04:40 — 等待区块 #9 验证已超时
04:54 — 提交节点从池中取出剩余的 760 万笔交易并开始计算 Merkle 哈希并签署区块 #9
05:32 — 区块 #9 被签名并发送到其他节点进行验证
05:53 — 区块 #9 被验证并发送到根链
06:17 - 节点开始收到区块 #9 已添加到根链并开始执行 760k 交易的信息
06:47 — 矿池已清除区块 #9 中的交易
09:06 - 所有节点包含 2 万笔交易和代币

测试 2

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


初始状态:最后一个区块#9; 数据库中存储了 2 万笔交易和代币

00:00 — 交易生成脚本已经启动
00:44 - 创建了 1 万笔交易并开始发送到节点
00:56 — 提交节点从池中取出 320 万笔交易并形成区块#10。 我们还看到 320 秒内有 10k 笔交易添加到池中
01:12 — 区块 #10 被签名并发送到其他节点进行验证
01:18 — 演示脚本完成工作,在 1 秒内发送了 34 万笔交易
01:20 — 区块 #10 被验证并发送到根链
01:51 - 所有节点从根链收到添加了区块 #10 的信息并开始应用 320k 交易
02:01 - 矿池已清除添加到区块 #320 的 10 万笔交易
02:15 — 提交节点从池中取出 350 万笔交易并形成区块 #11
02:34 — 区块 #11 被签名并发送到其他节点进行验证
02:51 — 区块 #11 被验证并发送到根链
02:55 — 最后一个节点完成了区块 #10 的交易
10:59 — 提交区块 #9 的交易在根链中花费了很长时间,但它已经完成,所有节点都收到了有关它的信息并开始执行 350k 交易
11:05 - 矿池已清除添加到区块 #320 的 11 万笔交易
12:10 - 所有节点包含 1 万个 670k 交易和代币
12:17 — 提交节点从池中取出 330 万笔交易并形成区块 #12
12:32 — 区块 #12 被签名并发送到其他节点进行验证
12:39 — 区块 #12 被验证并发送到根链
13:44 - 所有节点从根链收到添加了区块 #12 的信息并开始应用 330k 交易
14:50 - 所有节点包含 2 万笔交易和代币

测试 3

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


初始状态:最后一个区块#84; 0 笔交易和代币保存在数据库中

00:00 — 已启动 3 个脚本,每个脚本生成并发送 1 万笔交易
01:38 — 创建了 1 万笔交易并开始发送到提交节点 #3
01:50 — 提交节点 #3 从池中获取 330 万笔交易并形成区块 #85 (f21)。 我们还看到 350 秒内有 10 万笔交易添加到池中
01:53 — 创建了 1 万笔交易并开始发送到提交节点 #1
01:50 — 提交节点 #3 从池中获取 330 万笔交易并形成区块 #85 (f21)。 我们还看到 350 秒内有 10 万笔交易添加到池中
02:01 — 提交节点 #1 从池中获取 250 万笔交易并形成区块 #85 (65e)
02:06 — 区块 #85 (f21) 被签名并发送到其他节点进行验证
02:08 — 服务器 #3 的演示脚本在 1 秒内发送了 30 万笔交易,完成工作
02:14 — 区块 #85 (f21) 被验证并发送到根链
02:19 — 区块 #85 (65e) 被签名并发送到其他节点进行验证
02:22 — 创建了 1 万笔交易并开始发送到提交节点 #2
02:27 — 区块 #85 (65e) 得到验证并发送到根链
02:29 — 提交节点 #2 从池中获取 111855 笔交易并形成区块 #85 (256)。
02:36 — 区块 #85 (256) 被签名并发送到其他节点进行验证
02:36 — 服务器 #1 的演示脚本在 1 秒内发送了 42.5 万笔交易,完成工作
02:38 — 区块 #85 (256) 被验证并发送到根链
03:08 — 服务器 #2 脚本完成工作,在 1 秒内发送了 47 万笔交易
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 从池中获取 750 万笔交易并形成区块 #88 (50a),提交节点 #3 从区块中获取 670 万笔交易池和形成块 #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 的所有节点包含 3 万笔交易和代币
14:35 — 服务器 #1 的所有节点包含 3 万笔交易和代币
19:24 — 服务器 #2 的所有节点包含 3 万笔交易和代币

障碍

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

1. 各种系统功能交互中的冲突。 例如,向池中添加交易的功能会阻塞提交和验证区块的工作,反之亦然,从而导致速度下降。

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

3. 目前尚不清楚如何以及在何处存储数据才能获得高结果。

4. 目前尚不清楚如何在节点之间组织网络,因为包含 1 万笔交易的区块大小大约需要 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 笔交易。 我必须找出错误实施的地方。

首先,我们开始测试与 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,并在 80 管道模​​式下每秒获得约 1k 次操作。

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

  • UNIX 套接字连接已建立。
  • 我们禁用了将状态保存到磁盘(为了可靠性,您可以设置副本并保存到单独的 Redis 中的磁盘)。

在Redis中,池是一个哈希表,因为我们需要能够在一次查询中检索所有事务并逐一删除事务。 我们尝试使用常规列表,但卸载整个列表时速度较慢。

当使用标准 NodeJS 时,Redis 库实现了每秒 18k 事务的性能。 速度下降了9倍。

由于基准测试显示可能性明显提高了 5 倍,因此我们开始优化。 我们将库更改为 ioredis,并获得了每秒 25k 的性能。 我们使用“hset”命令一项一项地添加交易。 所以我们在 Redis 中生成了大量查询。 这一想法的出现是将交易合并为一批,并使用一个命令“hmset”发送它们。 结果是每秒 32k。

由于多种原因(我们将在下面描述),我们使用“Buffer”处理数据,事实证明,如果在写入之前将其转换为文本(“buffer.toString('hex')”),您可以获得额外的信息表现。 因此,速度提高到每秒35k。 目前,我们决定暂停进一步优化。

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

1. 系统经常计算哈希值、签名等,为此它需要 Buffer 中的数据。

2. 在服务之间发送时,二进制数据的重量小于文本。 例如,当发送一个包含 1 万笔交易的区块时,文本中的数据可能会占用超过 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(block, Protocol).slice();”和“BD.decode(buffer, Protocol)”,我们将数据转换为“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)
  }
  ```

其中:

  • `型` — 要执行的操作,例如 1 — sendTransaction,2 — getTransaction;
  • `有效负载` ——需要传递给适当函数的数据;
  • `消息ID` — 消息 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;
  • `版本协议` - 协议版本,因为不同版本的节点可以在网络上产生,并且它们可以不同地工作;
  • `序列` ——消息标识符;
  • `countChunk` и `块号` 分割大消息所必需的;
  • `长度` и `有效负载` 长度和数据本身。

由于我们预先输入了数据,最终的系统比以太坊的“rlp”库快得多。 不幸的是,我们还无法拒绝它,因为有必要敲定智能合约,我们计划在未来这样做。

如果我们能达到这个速度 35 000 每秒交易数,我们还需要在最佳时间处理它们。 由于区块形成时间大约需要 30 秒,因此我们需要将 1 000 000 交易,这意味着发送更多 100 MB 数据。

最初,我们使用“ethereumjs-devp2p”库在节点之间进行通信,但它无法处理如此多的数据。 因此,我们使用了“ws”库并配置了通过 websocket 发送二进制数据。 当然,我们在发送大数据包时也遇到了问题,但是我们将它们分成了块,现在这些问题都消失了。

同时形成默克尔树并计算哈希 1 000 000 交易需要大约 10 连续计算的秒数。 在此期间,与所有节点的连接都会断开。 决定将此计算移至单独的线程。

结论:

事实上,我们的发现并不新鲜,但由于某种原因,许多专家在开发时忘记了它们。

  • 使用函数式编程而不是面向对象编程可以提高生产力。
  • 对于高效的 NodeJS 系统来说,单体架构比服务架构更糟糕。
  • 使用“worker_threads”进行繁重的计算可以提高系统响应能力,尤其是在处理 I/O 操作时。
  • unix 套接字比 http 请求更稳定、更快。
  • 如果需要通过网络快速传输大数据,最好使用websocket,发送二进制数据,分成块,如果没有到达可以转发,然后组合成一条消息。

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

这篇文章的共同作者是 亚历山大·纳西万, 高级开发人员 聪明解决方案公司.

来源: habr.com

添加评论