筆者注:因近期筆者工作需要,開始接觸 solana 鏈上程序開發。本系列文章是筆者的學習筆記,既是為了備忘,也是希望得到 solana 開發者的指點與交流。本系列文章將默認讀者已經掌握 rust 的基礎語法,故不涉及對 rust 語法細節的解釋。如果讀者對 rust 基礎語法還不熟練的話,本文下方推薦的 rust 入門書籍《rust 編程入門、實戰與進階》學習。
1.1 Solana 簡介
Solana 是一個高性能、無許可的底層公鏈,專注于在不犧牲去中心化或安全性的前提下提供可擴展性。Solana 主網于 2020 年一季度上線,目前主網的全球節點超過 800 個,TPS 最高可達 6.5 萬,出塊時間約 400 毫秒。
Solana 的共識算法采用 PoH(歷史證明),其核心是一個去中心化時鐘,該時鐘旨在解決缺乏單個可信賴時間源在分布式網絡中的時間問題。PoH 免除了在節點網絡中廣播時間戳的需求,從而提高整個網絡的效率。
1.1.1 鏈上程序
Solana 的智能合約叫做鏈上程序(On-chain Program),Solana 官方提供了 Rust 和 C 的 SDK 來支持開發鏈上程序。鏈上程序的開發工作流如圖 1-1 所示,開發者可以使用工具將程序編譯成 Berkley Packet Filter (BPF) 字節碼(文件以 .so 為擴展名),再部署到 Solana 鏈上,通過 Sealevel 并行智能合約運行時去執行智能合約的邏輯。此外,基于 Solana JSON RPC API,官方提供了諸多 SDK 用于客戶端與 Solana 鏈上數據交互。
圖 1-1 鏈上程序開發工作流
1.1.2 賬戶模型
與以太坊類似,Solana 也是基于賬戶模型的區塊鏈。通過將任意狀態存儲于鏈上賬戶并同步復制給集群中的所有節點,可以創建復雜而強大的去中心化應用程序。
Solana 提供了一套不同于以太坊的賬戶模型,賬戶定義的字段如表 1-1 所示。Solana 的賬戶可以分為可執行賬戶和不可執行賬戶。
- 可執行賬戶:存儲不可變的數據,主要用于存儲程序的 BPF 字節碼。
- 不可執行賬戶:存儲可變的數據,主要用于存儲程序的狀態。
表 1-1 賬戶定義字段
字段 | 描述 |
---|---|
lamports | 賬戶余額 |
owner | 賬戶所有者 |
executable | 是否為可執行賬戶 |
data | 賬戶存儲的數據 |
rent_epoch | Solana鏈上程序的部署是按其賬戶大小進行定期收費的,如果賬戶無法支付租金,系統將清除該賬號 |
我們知道以太坊上每個智能合約的代碼和狀態都存儲在同一個賬戶中,而 Solana 鏈上程序是只讀或無狀態的,即程序的賬戶(可執行賬戶)只存儲 BPF 字節碼,不存儲任何狀態,程序會把狀態存儲在其他獨立的賬戶(不可執行賬戶)中。為了區分某個賬戶是用作哪個程序的狀態存儲,每個賬戶都指定了一個程序作為其所有者。程序可以讀取其不作為所有者的賬戶中的狀態,但只有作為所有者的程序才能修改賬戶中的狀態,任何其他程序所做的修改都會被還原并導致交易失敗。
更多關于賬戶模型的資料可以參見官方文檔:https://solana.wiki/zh-cn/docs/account-model/
1.2 搭建編程環境
在開始 Solana 鏈上程序開發之前,需要先安裝和配置相關的編程環境。首先請正確安裝 Node、NPM 和 Rust 的最新穩定版本,下面來安裝 Solana CLI 并配置相關環境。
1.2.1 安裝 Solana CLI
Solana CLI 是與 Solana 集群進行交互的命令行管理工具,包含節點程序 solana-validator、密鑰對生成工具 solana-keygen,以及合約開發工具 cargo-build-bpf、cargo-test-bpf 等。
在終端運行以下命令,可完成 Solana CLI 最新穩定版的下載與安裝。
如果安裝成功,會出現以下內容。
Solana CLI 的所有命令行工具都安裝在 ~/.local/share/solana/install/active_release/bin 中,并會自動將該路徑加入 ~/.profile 和 ~/.bash_profile 文件的 PATH 環境變量。
運行以下命令,檢查 PATH 環境變量是否已正確設置。
如果能顯示 solana-cli 的版本號、版本哈希等信息,代表環境變量設置成功。如果未看到這些信息,請檢查相關文件中 PATH 環境變量設置的路徑是否正確。
如果已安裝過 Solana CLI,想升級到最新版本,可在終端運行以下命令。
1.2.2 配置 Solana CLI
1. 連接到集群
Solana 的集群有本地集群(localhost)和公開集群。根據不同的用途,公開集群又分為開發者網絡(devnet)、測試網(testnet)和主網(mainnet-beta)。
- devnet 是適用于開發者的集群,開發者可獲得 SOL token 的空投,但這個 SOL token 不具有真實價值,僅限測試使用。devnet 的 RPC 鏈接是https://api.devnet.solana.com。
- testnet 是用于測試最新功能的集群,如網絡性能、穩定性和驗證程序行為等。同樣可獲得 SOL token 的空投,但也僅限測試使用。testnet 的 RPC 鏈接是https://api.testnet.solana.com。
- mainnet-beta 是主網集群,在 Mainnet Beta 上發行的 SOL token 具有真實價值。mainnet-beta 的 RPC 鏈接是https://api.mainnet-beta.solana.com。
運行以下命令,根據實際需要來選擇集群。
2. 創建賬戶
如果是第一次使用 Solana CLI,需要先創建一個賬戶。運行以下命令,根據操作提示可以設置一個 BIP39 規范的密碼,此密碼用來增強助記詞的安全性,當然也可以為空。生成新的賬戶后,密鑰對會被自動寫入 ~/.config/solana/id.json 文件中。需要注意的是,這種存儲密鑰對的方式是不安全的,僅限開發測試使用。
要查看當前這個賬戶的公鑰,運行以下命令。
當前如果是在 devnet 集群,該賬戶的余額為 0 SOL,可以運行以下命令查詢余額。
在 devnet 上申請 SOL 空投,運行以下命令后再次查詢當前賬戶的余額,會發現余額為 2 SOL。
1.3 第一個 Solana 項目——Hello World
Hello World 是一個官方演示項目,展示了如何使用 Rust 和 C 開發鏈上程序,并使用 Solana CLI 來構建與部署,以及使用 Solana JavaScript SDK 與鏈上程序進行交互。
1.3.1 Hello World 源碼解讀
example-helloworld 項目的目錄結構如下所示,其中 program-rust 目錄下是 Rust 開發的程序源代碼,client 目錄下是客戶端的源代碼。
1. 鏈上程序源碼解讀
program-rust/src/lib.rs 是鏈上程序的核心代碼,如代碼清單 1-1 所示,實現了將程序被調用次數存儲在鏈上賬戶中。
第 1 行代碼將 borsh::BorshDeserialize 和 borsh::BorshSerialize 引入本地作用域,用于序列化和反序列化數據。第 2~9 行代碼將 Solana Rust SDK 的模塊引入本地作用域,使用 Rust 編寫程序都需要這個 SDK。
第 13~16 行代碼定義了 GreetingAccount 結構體作為存儲在賬戶中的狀態類型,里面有一個 u32 類型的字段 counter,用于記錄程序被有效調用的次數。
第 19 行代碼 entrypoint 聲明了 process_instruction 函數是程序入口,每個程序都有一個唯一的入口。第 22~26 行代碼是 process_instruction 函數簽名,它要接收 3 個參數:
- program_id:鏈上程序的部署地址,在這里也就是 helloworld 程序賬戶的公鑰。
- accounts:與程序交互的賬戶列表,當前程序會使用賬戶列表中的賬戶來存儲狀態或修改賬戶中的數據。如果當前程序不是某個賬戶的所有者,那就無法使用該賬戶存儲狀態或修改數據,當前交易會執行失敗。
- instruction_data:指令數據,比如要轉賬的代幣數量、轉賬地址等。
process_instruction 函數的返回值類型是 ProgramResult,ProgramResult 類型的定義如下所示。
當程序的邏輯執行成功時返回 Ok(()),否則將 ProgramError 錯誤返回。ProgramError 是自定義錯誤的枚舉類型,其中包含程序可能失敗的各種原因。
第 27 行代碼使用 msg! 宏將字符串輸出到日志中,方便觀察業務的執行邏輯和調試信息。第 30 行代碼通過 iter 方法將賬戶列表轉換為迭代器,以安全的方式獲取賬戶地址。第 33 行代碼使用了 ? 操作符,如果迭代器中有賬戶地址,會將賬戶地址與變量 account 綁定。如果迭代器中沒有賬戶地址,? 操作符會讓程序執行失敗。
第 36~39 行代碼判斷存儲狀態的賬戶所有者是否是當前程序。只有賬戶所有者才能修改數據,否則輸出日志并返回。
第 42~44 行代碼先對賬戶中的數據進行反序列化操作,再將 counter 加一,最后將其序列化后存儲到賬戶中。
代碼清單 1-1 helloworld 鏈上程序
2. 客戶端程序源碼解讀
要想測試鏈上程序,我們必須通過 Solana JSON RPC API 去和鏈上程序進行交互。example-helloworld 項目提供的客戶端用 Typescript 編寫,使用了 web3.js 庫這個 Solana JavaScript SDK。
在 client 目錄下,客戶端執行的入口是 main.ts 文件,它按特定的順序執行任務,每個任務的業務邏輯代碼在 hello_world.ts 文件。
首先,客戶端調用 establishConnection 函數與集群建立連接。
接著,客戶端調用 establishPayer 函數來確保有一個有支付能力的賬戶。
然后,客戶端調用 checkProgram 函數從 src/program-rust/target/deploy/helloworld-keypair.json 中加載已部署程序的密鑰對(此操作前需先構建鏈上程序,詳見 1.3.2 節),并使用密鑰對的公鑰來獲取程序賬戶。如果程序不存在,客戶端會報錯并停止執行。如果程序存在,將創建一個新賬戶來存儲狀態,并以該程序作為新賬戶所有者。這里新賬戶存儲的狀態,就是程序被調用的次數。
客戶端再調用 sayHello 函數向鏈上程序發送交易。一個交易可以包含一個或多個不同的指令,當前該交易包含了一個指令,指令中帶有要調用鏈上程序的 Program Id 以及客戶端要交互的賬戶地址。需要注意的是,如果交易中包含多個不同的指令,其中有一個指令執行失敗,那么所有指令所做的操作都會被還原。
最后,客戶端調用 reportGreetings 函數訪問賬戶數據,查詢鏈上程序被有效調用的次數。
1.3.2 Hello World 構建與部署
1. 創建項目
使用 git clone 命令下載 example-helloworld 項目。
2. 構建鏈上程序
運行以下命令,在 program-rust 目錄下構建鏈上程序。
構建完成后,src/program-rust/target/deploy 目錄下的 helloworld.so 就是可在 Solana 集群部署的鏈上程序的 BPF 字節碼文件。
3. 啟動本地集群
當前項目在本地集群部署運行,因此首先選擇 localhost 集群,運行以下命令。
本地集群設置成功,會出現以下內容。
再運行以下命令,啟動 localhost 集群。
看到以下內容,代表本地集群已成功啟動。
4. 部署鏈上程序
運行以下命令,在 localhost 集群部署鏈上程序。
鏈上程序部署成功會返回 Program Id,它類似于以太坊智能合約的地址。
5. 調用鏈上程序
helloworld 已成功部署,可以與它進行交互了!example-helloworld 項目提供了一個簡單的客戶端,在運行客戶端之前先安裝依賴軟件包。
由于我們調整了鏈上程序的構建方式,沒有使用該項目默認的 npm run build:program-rust 命令,因此需要修改 client 目錄下的 hello_world.ts 文件,將第 48 行代碼定義的變量 PROGRAM_PATH 的路徑由“../../dist/program”改為“../program-rust/target/deploy”。 再運行以下命令,啟動客戶端去調用鏈上程序。
客戶端成功調用鏈上程序,輸出內容如下所示。如果再次運行客戶端,第 10 行所顯示的次數會加一。至此,我們已經成功在 Solana 集群部署鏈上程序并與之交互了。
如果沒有輸出期望值,請首先確認是否已正確啟動了本地集群,構建并部署好了鏈上程序。此外,可以運行以下命令查看程序日志,日志包括程序日志消息以及程序失敗信息。
包含程序失敗信息的日志如下所示,檢查日志找出程序失敗的原因。
1.4 本章小節
本章對 Solana 區塊鏈的基本概念進行了簡要介紹,Solana 的智能合約叫做鏈上程序。在開始 Solana 鏈上程序開發之前,需要先安裝和配置相關的編程環境,我們著重介紹了 Solana CLI 的安裝和配置。
Hello World 是一個官方演示項目,通過對這個項目源碼的解讀,我們了解了如何使用 Rust 開發鏈上程序,并使用 Solana CLI 來構建與部署,以及使用 Solana JavaScript SDK 與鏈上程序進行交互。
以上就是Solana開發學習筆記(一)——從Hello World出發的詳細內容,更多請關注本站其它相關文章!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。