公開測試:以太坊上的隱私和可擴展性解決方案

區塊鏈 是一項創新技術,有望改善人類生活的許多領域。 它將真實的流程和產品轉移到數位空間,確保金融交易的速度和可靠性,降低成本,還允許您在去中心化網路中使用智慧合約創建現代 DAPP 應用程式。

考慮到區塊鏈的許多好處和多樣化的應用,這項有前途的技術尚未進入每個行業似乎令人驚訝。 問題在於現代去中心化區塊鏈缺乏可擴展性。 以太坊每秒處理約 20 筆交易,不足以滿足當今動態業務的需求。 同時,由於以太坊對駭客攻擊和網路故障的高度保護,使用區塊鏈技術的公司對於放棄以太坊猶豫不決。

為了確保區塊鏈的去中心化、安全性和可擴展性,從而解決可擴展性三角難題,開發團隊 機會 創建了 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

這篇文章的共同作者是 亞歷山大·納西萬, 高級開發人員 聰明解決方案公司.

來源: www.habr.com

添加評論