比特幣實現日誌
我八月的技術學習項目是用C語言從頭寫一個比特幣節點。在這裡記錄一下過程。
Repo:https://github.com/blaesus/ctbc
1. 日程規劃
計劃是用八月的4個週末和一些閒暇時間,寫一個比特幣節點出來,最高目標是功能完備,最低目標是比特幣核心機制(區塊驗證)。
我其實沒寫過C,K&R看到一半,連指針都沒用過。這次選C來寫有強迫自己學習的意思。
底層網絡編程(比如拼TCP包)其實我也沒做過,所以也要學。
所以要做得事情還挺多的。
爲什麼覺得一個月業餘真的能寫出點東西了?
第一,我很熟悉JavaScript和TypeScript,畢竟語法都是C系的,C語言只要學特性就好了。
第二,我對比特幣的白皮書讀得比較認真(見本文上游), 概念理解不會又大問題,只需要把實現細節補全就好了。
第三,我統計了中本聰第一版的比特幣代碼,不少,有兩萬行,但是去掉次要的功能(UI、數據庫)和工具庫(運算工具等),其實核心就6000行左右。如果把邊界檢查、錯誤處理、註釋等等去掉,估計真正要寫的就3000行,一個月寫3000行是完全可能的。
第四,無聊和無關原理的事情就用現成的庫,比如SHA256計算等。
2. 開發進程
比特幣協議文檔看這裏:https://en.bitcoin.it/wiki/Protocol_documentation
一開始我用高級語言的思路,先定義transaction、block的數據結構,然後考慮怎麼組合。
不久之後發現,Bitcoin的通訊協議非常底層,C語言也很底層,根本不是瀏覽器裏一句JSON.parse(await fetch(URL)),數據就來了。
所以改變了策略,從節點功能的角度開始實現。
一、找節點
第一步是找其他節點,不然自己跟自己玩,也不知道做對沒有。
要找節點,概念上其實很簡單,先用DNS解析幾個寫死的域名,獲取第一批節點,然後問這些節點,要他們知道的節點。
只是,這個DNS,Node.js就是一句話的事,我寫了一整天,因爲真是非常麻煩。
首先IPv4和IPv6各有一套API(我決定這次只處理IPv4)。
第二,光是IP地址就有幾種形態(系統給你的是二進制uint16,比特幣協議裏用uint8[16]表示,還要寫成可讀形式(1:2:3:4),不然自己都不知道算對了沒有)。IP地址還有endian問題,網絡通訊一般用大頭,Intel和AMD的CPU都是小頭,比特幣傳輸一般是小頭,IP地址和端口又用大頭……方向弄錯了,出來的結果看上去都是對的,就是連不上去,還要自己一個個字節比較。
反正一番折騰,總算找到了另外一百多個節點。
二、通信
找到其他節點,就是要跟他們說話了。
一開始,網絡通訊用的是Unix原生的那一套,socket、connect、listen、bind、accept什麼的,好鬼麻煩。而且,默認是阻塞的。想要異步?自己開進程加鎖吧。
掙扎了一天,決定放棄,網絡編程不作重點研究了,跳過select、epoll什麼的,直接上Node.js的基礎庫libuv。
啊,還是熟悉的配方,還是熟悉的味道,還是熟悉的callback hell。還附帶各種小工具,要是早上libuv,DNS也不用寫一天了。
三、握手
好,能跟節點說話,也能聽到他們說話了,下一步是協議握手(陌生人見面要打招呼,陌生節點見面要握手,才能談正事)。
首先,我要發一個叫version的消息包過去,告訴它我是誰,我玩哪一套規則,然後對面如果覺得大家能玩到一起,就發一個verack包回來。
這個過程,又花了兩個晚上……
爲什麼呢?
要感謝Amir Taaki和Patrick Strateman兩位仁兄。
中本聰設計的version是很簡單的,46個字節,講清楚四件事情:1. 我的版本,2. 我能幹什麼, 3. 幾點發的,4. 消息發給誰。就可以了。
定長的消息最好處理了,struct一定義,memcpy出來,直接發過去,就可以等回信了。
但是這兩位仁兄偏說,version消息要加一個user-agent的數據,節點不止要說自己的版本,還要說清楚自己具體是哪個客戶端和小版本,比如/Satoshi:5.64/bitcoin-qt:0.4/。
這兩個人是做前端出身的吧。
加這個數據就算了,這個數據還要設成不定長字符串。換句話說,整個version消息就是不定長的了,那就要另外找個地方存它的長度。
這還沒完。
這個「不定長字符串」,它的的「長度本身」的長度,也是會變的,可能是1、3、5、9字節。
這是個「不定不定長字符串」。
就爲了這個破user-agent。
反正各種折騰,各種指針倒來倒去,與errno 11鬥志鬥勇。
總算,剛纔,收到其他節點的回信了(就是我發了個version,它們發來verack和version)。
真的很高興(所以纔有這篇文章)。
在地球不知道什麼地方,有一羣機器,對我的機器說:我們聽得懂你的語言。
在互聯網普及、協議成熟的今天,握手成功,太正常了。
可是,用Unix的創始語言,實現一個小衆的協議,然後在茫茫機海之中,找到語言共通的機器,然後確認雙方的對話意願。
感覺回到了80年代。
那個互聯網還是小衆玩意的年代,那個要努力才能連上線的年代,那個經常有人發明通訊協議的年代。
比特幣果然挺好玩的。
之後的有新的進展會再跟大家報告。
8月12日更新
對面發message,有時候會整個發過來,有時候會只發一個頭,再發個payload過來,所以代碼也要分開處理,不能假設對方一定會全部發過來。
8月19日更新
tx和block是message裏面的大頭,比較複雜,變化也相對多一點,文檔也略差。要自己鑽研一下。
8月29日更新
一個月過去了,實現的經驗已經寫到了:https://github.com/blaesus/ctbc/wiki/Bitcoin-from-Scratch
完成功能:找節點、連接節點、 握手、收區塊鏈(header)、驗證「工作證明」(Proof-of-work)。
部分完成:挖礦(只實現了給定header找nonce)
未完成:交易驗證(尤其是Script實現)、分叉處理、錢包(生成私鑰、公鑰)、JSON-RPC、UI
這個進度我是非常滿意的。
中間卡殼兩次,一次是getheaders請求發出去,怎麼都收不到headers的回覆。後來發現是payload的checksum算錯了(因爲輸入數組的長度算錯了)。第二次是PoW的target跟官方數據有千分之幾的誤差,到三萬多的區塊之後,誤差就大到無法兼容了。後來發現問題是過度矯正了中本聰的2015 off-by-one bug。本來應該是矯正到2015,我矯正多了一位到2014。最後直接看中本聰的實現,一行一行翻譯過來,終於對上了。(中間還犯了另一個錯,把目標設高了8倍,導致本來不應該接受的區塊也接受了,這是測試挖礦功能的時候發現的。)
9月我計劃繼續實現比特幣協議,把主要功能都實現一次。
這個月用C寫比特幣,感覺自己的工程能力又上了一個檔次,真是受益匪淺。
用C寫,就要建立內存模型,處理segment fault什麼的,記住每個變量在棧上還是在堆上,對於我這個寫JS出身的程序員是非常有益的。比特幣使用大量網絡編程技術:TCP連接,發包收包,序列化反序列化。甚至還有二叉樹(算merkle root時有用)。共時庫上了libuv,對Node.js的底層也有了一點接觸。