區塊鏈 是一項創新技術,有望改善人類生活的許多領域。 它將真實的流程和產品轉移到數位空間,確保金融交易的速度和可靠性,降低成本,還允許您在去中心化網路中使用智慧合約創建現代 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上 專案:
這篇文章的共同作者是 亞歷山大·納西萬, 高級開發人員
來源: www.habr.com