塊鏈開(kāi)發(fā)框架入門(mén):從核心概念到實(shí)戰(zhàn)踩坑指南)
1. 從“substrate”這個(gè)詞說(shuō)起它到底指什么第一次看到“substrate”這個(gè)標(biāo)題很多人會(huì)愣一下。這個(gè)詞在英文里的本意是“底層、基底、培養(yǎng)基”但在不同圈子里它指向的東西完全不一樣。做區(qū)塊鏈的人第一反應(yīng)是 Parity 那套區(qū)塊鏈框架做材料或化學(xué)的人想到的是反應(yīng)基底做電子工程的人想到的是芯片襯底做生物實(shí)驗(yàn)的人想到的是培養(yǎng)基。所以拿到這個(gè)標(biāo)題第一件事不是急著動(dòng)手而是先判斷它落在哪個(gè)語(yǔ)境里。我這次要聊的是它在區(qū)塊鏈開(kāi)發(fā)框架這個(gè)語(yǔ)境下的含義也就是那套用來(lái)搭建自定義區(qū)塊鏈的底層框架。之所以值得單獨(dú)寫(xiě)一篇是因?yàn)樗汀爸苯幽靡粭l現(xiàn)成鏈來(lái)用”是完全不同的思路現(xiàn)成鏈?zhǔn)悄闳ミm應(yīng)它的規(guī)則而 substrate 是你自己定義規(guī)則。這個(gè)差別決定了它的學(xué)習(xí)曲線、適用人群和踩坑方式都和普通鏈上開(kāi)發(fā)不一樣。如果你屬于下面這幾類(lèi)人這篇內(nèi)容會(huì)對(duì)你有直接幫助想搞清楚 substrate 到底解決什么問(wèn)題、值不值得投入時(shí)間學(xué)已經(jīng)決定上手但被一堆概念和工具鏈繞暈或者已經(jīng)跑通了模板但不知道下一步該往哪走。我會(huì)盡量把“為什么這么設(shè)計(jì)”講透而不是只丟一堆命令給你。需要先說(shuō)明一點(diǎn)substrate 本身迭代很快版本之間的 API 和工具名會(huì)有變化。我下面講的是基于常見(jiàn)實(shí)踐的思路和結(jié)構(gòu)具體命令和字段名請(qǐng)以你所用版本的官方文檔為準(zhǔn)。這不是偷懶而是這個(gè)領(lǐng)域變化太快任何寫(xiě)死的命令過(guò)幾個(gè)月都可能失效思路才是能長(zhǎng)期復(fù)用的東西。2. 為什么是“框架”而不是“一條鏈”2.1 現(xiàn)成鏈和框架的本質(zhì)區(qū)別大多數(shù)人接觸區(qū)塊鏈?zhǔn)菑囊粭l已經(jīng)跑起來(lái)的鏈開(kāi)始的。你拿到地址、拿到 RPC 接口、拿到 SDK然后寫(xiě)合約或者發(fā)交易。這種模式下你是在別人定好的規(guī)則里做事出塊時(shí)間、手續(xù)費(fèi)模型、賬戶體系、治理方式全都是鏈定好的你只能接受。substrate 走的是另一條路。它提供的是一套可組裝的組件共識(shí)、賬戶、治理、資產(chǎn)、智能合約執(zhí)行環(huán)境這些都被拆成獨(dú)立的模塊在 substrate 里叫 pallet你可以挑需要的裝進(jìn)去不需要的就不裝。裝完之后編譯出來(lái)的是一條屬于你自己的鏈規(guī)則由你定。打個(gè)比方現(xiàn)成鏈像是租一間裝修好的房子家具位置都固定了你只能往里搬東西substrate 像是給你一套樂(lè)高積木加一份圖紙你可以照著搭也可以改圖紙甚至自己造零件。自由度高了代價(jià)就是你要對(duì)“房子怎么蓋”有基本認(rèn)知否則連門(mén)都裝不上。2.2 什么場(chǎng)景才真的需要自己搭鏈不是所有項(xiàng)目都需要 substrate。如果你的需求只是發(fā)個(gè)代幣、做個(gè) NFT、寫(xiě)個(gè) DeFi 協(xié)議那用現(xiàn)成的智能合約平臺(tái)完全夠用成本低、生態(tài)成熟、用戶現(xiàn)成。硬上 substrate 屬于殺雞用牛刀還會(huì)把自己拖進(jìn)一堆底層問(wèn)題里。真正適合 substrate 的場(chǎng)景通常有這幾個(gè)特征一是你需要自定義手續(xù)費(fèi)模型或賬戶體系比如免手續(xù)費(fèi)、按資源計(jì)費(fèi)、或者賬戶不是傳統(tǒng)的公私鑰結(jié)構(gòu)二是你需要鏈級(jí)別的治理和升級(jí)能力比如鏈上投票直接升級(jí)運(yùn)行時(shí)不用硬分叉三是你需要特定的共識(shí)或出塊規(guī)則現(xiàn)成鏈滿足不了四是你要做應(yīng)用鏈也就是為單一應(yīng)用專(zhuān)門(mén)優(yōu)化的一條鏈不想和其他應(yīng)用搶區(qū)塊空間。我見(jiàn)過(guò)不少人一上來(lái)就想用 substrate理由是“聽(tīng)起來(lái)更底層更厲害”。這是個(gè)誤區(qū)。底層不等于更好只等于更自由、更貴、更慢。先想清楚你的需求是不是真的被現(xiàn)成方案卡住了再?zèng)Q定要不要上框架。2.3 框架帶來(lái)的三個(gè)真實(shí)成本決定用 substrate 之前得先接受三個(gè)成本。第一是編譯成本substrate 項(xiàng)目編譯一次動(dòng)輒十幾分鐘到半小時(shí)機(jī)器配置差一點(diǎn)更久這會(huì)顯著拖慢你的迭代節(jié)奏。第二是學(xué)習(xí)成本Rust、宏、運(yùn)行時(shí)、存儲(chǔ)、權(quán)重這一套概念和普通合約開(kāi)發(fā)完全不是一個(gè)量級(jí)。第三是運(yùn)維成本自己搭的鏈要自己維護(hù)節(jié)點(diǎn)、自己處理升級(jí)、自己保證網(wǎng)絡(luò)穩(wěn)定沒(méi)有現(xiàn)成的生態(tài)幫你兜底。這三個(gè)成本不是勸退而是讓你心里有數(shù)。如果你只是想做個(gè)小工具驗(yàn)證想法用合約平臺(tái)一兩天就能出結(jié)果用 substrate 可能一周還在配環(huán)境。選型的時(shí)候把時(shí)間成本算進(jìn)去比事后后悔強(qiáng)。3. 上手前必須搞懂的幾個(gè)核心概念3.1 運(yùn)行時(shí)鏈的“操作系統(tǒng)”substrate 里最重要的概念是運(yùn)行時(shí)Runtime。你可以把它理解成鏈的操作系統(tǒng)它定義了這條鏈能做什么、狀態(tài)怎么存、交易怎么被處理。運(yùn)行時(shí)的代碼是編譯進(jìn)鏈的升級(jí)運(yùn)行時(shí)就等于升級(jí)鏈的邏輯而且這個(gè)過(guò)程可以通過(guò)鏈上治理完成不需要停鏈或硬分叉。這一點(diǎn)和智能合約平臺(tái)有本質(zhì)區(qū)別。在合約平臺(tái)上邏輯寫(xiě)在合約里合約和鏈?zhǔn)欠蛛x的在 substrate 里邏輯就是鏈本身的一部分。好處是性能更好、更靈活代價(jià)是每次改邏輯都要重新編譯、走升級(jí)流程不能像改合約那樣隨時(shí)部署。理解運(yùn)行時(shí)是理解 substrate 的分水嶺。很多人卡住就是因?yàn)檫€在用“合約思維”看它總想著“我寫(xiě)個(gè)邏輯部署上去”但 substrate 的思路是“我改運(yùn)行時(shí)代碼然后升級(jí)鏈”。3.2 Pallet可插拔的功能模塊Pallet是 substrate 的模塊化單位。一個(gè) pallet 通常包含一組相關(guān)的功能比如資產(chǎn)、治理、質(zhì)押、身份。每個(gè) pallet 有自己的存儲(chǔ)、自己的交易在 substrate 里叫 extrinsic、自己的鉤子函數(shù)。用 pallet 的好處是復(fù)用。你不需要從零寫(xiě)賬戶系統(tǒng)、寫(xiě)治理邏輯直接用官方或社區(qū)維護(hù)的 pallet 就行。需要定制的時(shí)候可以改現(xiàn)成 pallet也可以自己寫(xiě)一個(gè)。這種“組裝 定制”的模式是 substrate 效率的核心來(lái)源。但要注意pallet 之間不是完全獨(dú)立的。它們共享存儲(chǔ)、共享權(quán)重預(yù)算、共享運(yùn)行時(shí)上下文。裝太多 pallet 會(huì)讓運(yùn)行時(shí)變重編譯變慢還可能引入依賴(lài)沖突。所以選 pallet 的原則是只裝真正需要的能復(fù)用就不自己寫(xiě)但也要警惕引入一堆用不上的依賴(lài)。3.3 存儲(chǔ)、權(quán)重與費(fèi)用三個(gè)繞不開(kāi)的約束存儲(chǔ)在 substrate 里是要花錢(qián)的因?yàn)殒溕蠣顟B(tài)是所有節(jié)點(diǎn)都要保存的。所以它的存儲(chǔ)設(shè)計(jì)強(qiáng)調(diào)“用多少付多少”存一個(gè)值要交押金刪掉可以退。這和很多合約平臺(tái)“存了就不管”的習(xí)慣很不一樣寫(xiě)邏輯時(shí)必須考慮存儲(chǔ)的經(jīng)濟(jì)性。權(quán)重Weight是 substrate 用來(lái)衡量計(jì)算量的單位。每筆交易在執(zhí)行前要先估算權(quán)重執(zhí)行后要報(bào)告實(shí)際消耗。權(quán)重決定了區(qū)塊能裝多少交易也決定了手續(xù)費(fèi)怎么算。寫(xiě) pallet 的時(shí)候每個(gè) extrinsic 都要標(biāo)注權(quán)重標(biāo)錯(cuò)了要么浪費(fèi)區(qū)塊空間要么被惡意交易拖垮網(wǎng)絡(luò)。費(fèi)用則是在權(quán)重基礎(chǔ)上疊加的經(jīng)濟(jì)模型。substrate 默認(rèn)有一套按權(quán)重收費(fèi)的機(jī)制但你可以改。比如做應(yīng)用鏈的時(shí)候可以讓用戶免手續(xù)費(fèi)由應(yīng)用方補(bǔ)貼也可以按存儲(chǔ)、按計(jì)算、按調(diào)用次數(shù)分別計(jì)費(fèi)。這套靈活性是優(yōu)勢(shì)但也意味著你得自己想清楚經(jīng)濟(jì)模型不能指望默認(rèn)配置適配所有場(chǎng)景。4. 從零跑通一條鏈的完整路徑4.1 環(huán)境準(zhǔn)備里最容易翻車(chē)的環(huán)節(jié)搭 substrate 開(kāi)發(fā)環(huán)境最容易出問(wèn)題的不是代碼而是工具鏈版本。Rust 的版本、substrate 的版本、依賴(lài)庫(kù)的版本三者必須匹配否則編譯報(bào)錯(cuò)能讓你懷疑人生。我的習(xí)慣是先看官方文檔推薦的 Rust 版本用 rustup 裝指定版本然后嚴(yán)格按模板項(xiàng)目的依賴(lài)版本走不隨意升級(jí)。另一個(gè)坑是編譯緩存。substrate 項(xiàng)目第一次編譯會(huì)下載大量依賴(lài)并編譯耗時(shí)很長(zhǎng)。如果你在 CI 或容器里跑每次都是冷編譯時(shí)間成本極高。解決辦法是緩存 target 目錄和 cargo 的 registry能省下大量重復(fù)編譯時(shí)間。還有磁盤(pán)空間。substrate 的編譯產(chǎn)物和依賴(lài)緩存加起來(lái)能占幾十 GB磁盤(pán)不夠會(huì)在編譯中途失敗報(bào)錯(cuò)還不一定直白。開(kāi)始之前先確認(rèn)磁盤(pán)余量比事后排查省事得多。4.2 用模板起步而不是從空目錄開(kāi)始substrate 官方提供了模板項(xiàng)目通常叫 node-template 或類(lèi)似名字里面已經(jīng)配好了一條能跑的最小鏈有賬戶、有余額、有治理的基礎(chǔ)結(jié)構(gòu)。正確的做法是從這個(gè)模板起步先把它跑起來(lái)理解每個(gè)部分的作用再逐步替換成自己的邏輯。從空目錄開(kāi)始是新手最容易犯的錯(cuò)。你會(huì)被一堆配置文件、依賴(lài)聲明、構(gòu)建腳本淹沒(méi)還沒(méi)寫(xiě)到業(yè)務(wù)邏輯就放棄了。模板的價(jià)值在于它把“能跑起來(lái)”這件事先解決了你可以在一個(gè)工作的基礎(chǔ)上做增量修改每次改一點(diǎn)、驗(yàn)證一點(diǎn)。跑通模板的標(biāo)志是能本地啟動(dòng)節(jié)點(diǎn)、能出塊、能通過(guò)界面或命令行發(fā)起交易、能看到余額變化。這四個(gè)都做到了說(shuō)明環(huán)境沒(méi)問(wèn)題可以開(kāi)始改邏輯了。4.3 第一次改運(yùn)行時(shí)的正確姿勢(shì)改運(yùn)行時(shí)的第一步通常是加一個(gè)自己的 pallet。不要一上來(lái)就改核心 pallet那樣出問(wèn)題很難定位。正確做法是新建一個(gè)最簡(jiǎn)單的 pallet只做一件事比如存一個(gè)數(shù)字、讀一個(gè)數(shù)字然后把它注冊(cè)到運(yùn)行時(shí)里編譯、啟動(dòng)、調(diào)用確認(rèn)整條鏈路通了。這個(gè)“最小閉環(huán)”非常重要。它驗(yàn)證了你的 pallet 能被運(yùn)行時(shí)識(shí)別、能被交易調(diào)用、能正確讀寫(xiě)存儲(chǔ)。很多人跳過(guò)這一步直接寫(xiě)復(fù)雜邏輯結(jié)果編譯過(guò)了但調(diào)用失敗排查起來(lái)要從頭查起。注冊(cè) pallet 的時(shí)候要注意幾個(gè)地方在運(yùn)行時(shí)的構(gòu)造里加進(jìn)去、配置它的關(guān)聯(lián)類(lèi)型、給它分配存儲(chǔ)前綴。這幾步任何一步漏了都會(huì)導(dǎo)致編譯錯(cuò)誤或運(yùn)行時(shí) panic。報(bào)錯(cuò)信息通常比較晦澀所以建議一次只加一個(gè) pallet加完立刻編譯驗(yàn)證。4.4 本地跑通之后下一步該做什么模板跑通、自己的 pallet 也加進(jìn)去了接下來(lái)通常會(huì)面臨“然后呢”的問(wèn)題。這時(shí)候有幾個(gè)方向可以走一是完善業(yè)務(wù)邏輯把 pallet 從玩具變成真正有用的功能二是加治理和升級(jí)讓鏈能通過(guò)投票升級(jí)運(yùn)行時(shí)三是接前端用 polkadot.js 之類(lèi)的工具讓用戶能操作四是搭測(cè)試網(wǎng)拉幾個(gè)節(jié)點(diǎn)驗(yàn)證多節(jié)點(diǎn)環(huán)境下的行為。我的建議是先做測(cè)試再做前端。因?yàn)檫壿嫑](méi)穩(wěn)定之前前端改了也白改。測(cè)試要覆蓋正常路徑和邊界情況尤其是存儲(chǔ)的讀寫(xiě)、權(quán)重的估算、權(quán)限的校驗(yàn)。substrate 提供了測(cè)試工具可以在不啟動(dòng)節(jié)點(diǎn)的情況下測(cè) pallet 邏輯速度比手動(dòng)點(diǎn)界面快得多。5. 那些文檔里不會(huì)寫(xiě)的踩坑經(jīng)驗(yàn)5.1 編譯報(bào)錯(cuò)先看版本再看代碼substrate 的編譯報(bào)錯(cuò)經(jīng)常指向一些莫名其妙的宏展開(kāi)或類(lèi)型不匹配新手容易一頭扎進(jìn)代碼里找問(wèn)題。但根據(jù)我的經(jīng)驗(yàn)八成以上的編譯錯(cuò)誤根源是版本不匹配Rust 版本不對(duì)、依賴(lài)版本沖突、或者某個(gè) crate 的 API 變了。所以遇到編譯錯(cuò)誤第一反應(yīng)應(yīng)該是檢查版本而不是改代碼。具體做法確認(rèn) rust-toolchain 文件指定的版本、確認(rèn) Cargo.lock 里的依賴(lài)版本、確認(rèn)你參考的文檔對(duì)應(yīng)的 substrate 版本。版本對(duì)齊了很多“詭異”的錯(cuò)誤會(huì)自己消失。5.2 權(quán)重標(biāo)錯(cuò)不會(huì)立刻報(bào)錯(cuò)但會(huì)埋雷寫(xiě) pallet 的時(shí)候權(quán)重是最容易被敷衍的部分。很多人隨便填個(gè)數(shù)字編譯能過(guò)、測(cè)試能跑就以為沒(méi)事了。但權(quán)重標(biāo)錯(cuò)的影響是延遲顯現(xiàn)的標(biāo)低了惡意用戶可以用一筆交易消耗大量計(jì)算拖垮節(jié)點(diǎn)標(biāo)高了區(qū)塊空間被浪費(fèi)吞吐下降。正確的做法是對(duì)每個(gè) extrinsic分析它最壞情況下的計(jì)算量和存儲(chǔ)操作給出一個(gè)合理的上界。substrate 提供了基準(zhǔn)測(cè)試工具可以自動(dòng)測(cè)算權(quán)重但工具本身也要配置正確才有意義。如果暫時(shí)沒(méi)條件做基準(zhǔn)測(cè)試至少要在注釋里寫(xiě)清楚你的估算依據(jù)方便后續(xù)修正。5.3 存儲(chǔ)設(shè)計(jì)決定后期改造成本存儲(chǔ)是 substrate 里最需要提前想清楚的部分。因?yàn)榇鎯?chǔ)結(jié)構(gòu)一旦上線改起來(lái)很麻煩要么寫(xiě)遷移邏輯要么接受舊數(shù)據(jù)和新邏輯不兼容。我見(jiàn)過(guò)不少項(xiàng)目前期隨便存后期想加個(gè)索引或改個(gè)結(jié)構(gòu)結(jié)果要寫(xiě)一大堆遷移代碼。我的經(jīng)驗(yàn)是設(shè)計(jì)存儲(chǔ)時(shí)多問(wèn)幾個(gè)問(wèn)題。這個(gè)數(shù)據(jù)需要鏈上存嗎還是可以放鏈下需要按什么維度查詢(xún)要不要建索引數(shù)據(jù)會(huì)增長(zhǎng)到多大存儲(chǔ)成本能不能接受會(huì)不會(huì)需要按 key 遍歷遍歷的規(guī)??煽貑徇@些問(wèn)題想清楚了后期改造會(huì)少很多。5.4 本地能跑不等于多節(jié)點(diǎn)能跑本地單節(jié)點(diǎn)跑通只是萬(wàn)里長(zhǎng)征第一步。多節(jié)點(diǎn)環(huán)境下會(huì)遇到一堆單節(jié)點(diǎn)不會(huì)出現(xiàn)的問(wèn)題節(jié)點(diǎn)之間同步慢、共識(shí)出問(wèn)題、交易廣播不到、時(shí)間不同步導(dǎo)致出塊異常。這些問(wèn)題在本地是復(fù)現(xiàn)不出來(lái)的。所以邏輯穩(wěn)定之后盡早搭一個(gè)多節(jié)點(diǎn)的本地測(cè)試網(wǎng)。哪怕只有兩三個(gè)節(jié)點(diǎn)也能暴露很多問(wèn)題。搭測(cè)試網(wǎng)的時(shí)候注意每個(gè)節(jié)點(diǎn)的配置要區(qū)分開(kāi)端口、數(shù)據(jù)目錄、密鑰、啟動(dòng)參數(shù)都不能沖突。這些細(xì)節(jié)在單節(jié)點(diǎn)時(shí)無(wú)所謂多節(jié)點(diǎn)時(shí)全是坑。6. 關(guān)于 substrate 的幾個(gè)常見(jiàn)誤解6.1 “學(xué)了 substrate 就能做所有鏈”這是個(gè)很常見(jiàn)的誤解。substrate 是一個(gè)框架不是萬(wàn)能鑰匙。它能幫你快速搭出一條鏈但鏈能不能跑起來(lái)、能不能被用起來(lái)取決于共識(shí)設(shè)計(jì)、經(jīng)濟(jì)模型、生態(tài)建設(shè)這些框架之外的東西。框架解決的是“怎么搭”不解決“搭什么”和“誰(shuí)來(lái)用”。而且 substrate 也不是唯一選擇。不同的框架有不同的取舍有的更輕量有的更專(zhuān)注特定場(chǎng)景。選框架要看你的具體需求而不是看哪個(gè)聽(tīng)起來(lái)更厲害。6.2 “運(yùn)行時(shí)升級(jí)很危險(xiǎn)最好別動(dòng)”運(yùn)行時(shí)升級(jí)確實(shí)是 substrate 的一個(gè)強(qiáng)能力但很多人因?yàn)榕鲁鍪聦幵覆桓?。這其實(shí)是浪費(fèi)了框架的核心優(yōu)勢(shì)。正確的態(tài)度是把升級(jí)當(dāng)成常規(guī)操作來(lái)設(shè)計(jì)提前做好測(cè)試、灰度、回滾方案而不是當(dāng)成一次性的冒險(xiǎn)。具體做法包括升級(jí)前在測(cè)試網(wǎng)充分驗(yàn)證、升級(jí)時(shí)保留舊版本以便回滾、升級(jí)后密切監(jiān)控關(guān)鍵指標(biāo)。把這些流程化之后升級(jí)就從“危險(xiǎn)動(dòng)作”變成了“日常操作”。6.3 “pallet 越多功能越強(qiáng)”裝 pallet 確實(shí)能快速獲得功能但每個(gè) pallet 都有成本編譯時(shí)間、運(yùn)行時(shí)體積、依賴(lài)復(fù)雜度、潛在的安全面。裝一堆用不上的 pallet只會(huì)讓項(xiàng)目變重、變慢、變難維護(hù)。我的原則是能用簡(jiǎn)單邏輯自己實(shí)現(xiàn)的就不引入整個(gè) pallet必須用的 pallet也要定期檢查是否有更輕量的替代。功能強(qiáng)不強(qiáng)不取決于裝了多少模塊而取決于這些模塊是不是真的被用好了。7. 給不同階段的人的一點(diǎn)實(shí)在建議如果你還在觀望階段我的建議是先用現(xiàn)成平臺(tái)驗(yàn)證你的想法。等確認(rèn)需求真的被卡住了再考慮 substrate。不要為了“底層”而底層時(shí)間是最貴的成本。如果你已經(jīng)決定上手那就從模板開(kāi)始跑通最小閉環(huán)再逐步加功能。不要一上來(lái)就啃源碼那樣容易勸退。先把“能跑、能改、能驗(yàn)證”這條鏈路走順再深入細(xì)節(jié)。如果你已經(jīng)跑通了模板下一步的重點(diǎn)是測(cè)試和多節(jié)點(diǎn)驗(yàn)證。本地單節(jié)點(diǎn)的成功會(huì)給人虛假的信心多節(jié)點(diǎn)環(huán)境才是真正的試金石。把測(cè)試做扎實(shí)比急著做前端更有價(jià)值。最后分享一個(gè)我自己的習(xí)慣每次改運(yùn)行時(shí)之前先想清楚“如果這次改錯(cuò)了我怎么回滾”。把回滾方案想好再動(dòng)手心態(tài)會(huì)穩(wěn)很多出問(wèn)題也不慌。這個(gè)習(xí)慣幫我省過(guò)好幾次大麻煩推薦你也試試。