Blokcheyn 是一项创新技术,有望改善人类生活的许多领域。 它将真实的流程和产品转移到数字空间中,确保金融交易的速度和可靠性,降低成本,还允许您在去中心化网络中使用智能合约创建现代 DAPP 应用程序。
考虑到区块链的诸多好处和多样化的应用,这项有前途的技术尚未进入每个行业似乎令人惊讶。 问题在于现代去中心化区块链缺乏可扩展性。 以太坊每秒处理约 20 笔交易,这不足以满足当今动态业务的需求。 与此同时,由于以太坊对黑客攻击和网络故障的高度保护,使用区块链技术的公司对于放弃以太坊犹豫不决。
为了确保区块链的去中心化、安全性和可扩展性,从而解决可扩展性三角难题,开发团队
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上 项目: