Solana開發學習筆記(一)——從Hello World出發

 2024-08-13  阅读 8  评论 0

摘要:筆者註:因近期筆者工作需要,開始接觸 Solana 鏈上程序開發。本系列文章是筆者的學習筆記,既是為瞭備忘,也是希望得到 Solana 開發者的指點與交流。本系列文章將默認讀者已經掌握 Rust 的基礎語法,故不涉及對 Rust 語法細節的解釋。如果讀者對 Rust 基礎語法還不熟練的話,

筆者註:因近期筆者工作需要,開始接觸 Solana 鏈上程序開發。本系列文章是筆者的學習筆記,既是為瞭備忘,也是希望得到 Solana 開發者的指點與交流。本系列文章將默認讀者已經掌握 Rust 的基礎語法,故不涉及對 Rust 語法細節的解釋。如果讀者對 Rust 基礎語法還不熟練的話,本文下方推薦的 Rust 入門書籍《Rust 編程入門、實戰與進階》學習。

1.1 Solana 簡介

Solana 是一個高性能、無許可的底層公鏈,專註於在不犧牲去中心化或安全性的前提下提供可擴展性。Solana 主網於 2023 年一季度上線,目前主網的全球節點超過 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_epochSolana鏈上程序的部署是按其賬戶大小進行定期收費的,如果賬戶無法支付租金,系統將清除該賬號

我們知道以太坊上每個智能合約的代碼和狀態都存儲在同一個賬戶中,而 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 最新穩定版的下載與安裝。

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

如果安裝成功,會出現以下內容。

downloading stable installer  ✨ stable commit e9bef425 initializedAdding export PATH="~/.local/share/solana/install/active_release/bin:$PATH" to ~/.profileAdding export PATH="~/.local/share/solana/install/active_release/bin:$PATH" to ~/.bash_profileClose and reopen your terminal to apply the PATH changes or run the following in your existing shell:  export PATH="~/.local/share/solana/install/active_release/bin:$PATH"

Solana CLI 的所有命令行工具都安裝在 ~/.local/share/solana/install/active_release/bin 中,並會自動將該路徑加入 ~/.profile 和 ~/.bash_profile 文件的 PATH 環境變量。

運行以下命令,檢查 PATH 環境變量是否已正確設置。

solana --version// solana-cli 1.7.18 (src:e9bef425; feat:140464022)

如果能顯示 solana-cli 的版本號、版本哈希等信息,代表環境變量設置成功。如果未看到這些信息,請檢查相關文件中 PATH 環境變量設置的路徑是否正確。

如果已安裝過 Solana CLI,想升級到最新版本,可在終端運行以下命令。

solana-install update

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。

運行以下命令,根據實際需要來選擇集群。

// 選擇localhost集群solana config set --url localhost// 選擇devnet集群solana config set --url devnet

2. 創建賬戶

如果是第一次使用 Solana CLI,需要先創建一個賬戶。運行以下命令,根據操作提示可以設置一個 BIP39 規范的密碼,此密碼用來增強助記詞的安全性,當然也可以為空。生成新的賬戶後,密鑰對會被自動寫入 ~/.config/solana/id.json 文件中。需要註意的是,這種存儲密鑰對的方式是不安全的,僅限開發測試使用。

solana-keygen new

要查看當前這個賬戶的公鑰,運行以下命令。

solana-keygen pubkey

當前如果是在 devnet 集群,該賬戶的餘額為 0 SOL,可以運行以下命令查詢餘額。

solana balance

在 devnet 上申請 SOL 空投,運行以下命令後再次查詢當前賬戶的餘額,會發現餘額為 2 SOL。

solana airdrop 2
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 目錄下是客戶端的源代碼。

example-helloworld|+-- src|  ||  +-- client|  |  ||  |  +-- hello_world.ts|  |  ||  |  +-- main.ts|  |  ||  |  +-- utils.ts|  ||  +-- program-rust|  |  ||  |  +-- src|  |  |  ||  |  |  +-- lib.rs|  |  ||  |  +-- tests|  |  |  ||  |  |  +-- lib.rs|  |  ||  |  +-- Cargo.toml|  |  ||  |  +-- Xargo.toml|+-- .gitignore|+-- package.json|+-- tsconfig.json

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 類型的定義如下所示。

pub type ProgramResult = Result<(), ProgramError>;

當程序的邏輯執行成功時返回 Ok(()),否則將 ProgramError 錯誤返回。ProgramError 是自定義錯誤的枚舉類型,其中包含程序可能失敗的各種原因。

第 27 行代碼使用 msg! 宏將字符串輸出到日志中,方便觀察業務的執行邏輯和調試信息。第 30 行代碼通過 iter 方法將賬戶列表轉換為迭代器,以安全的方式獲取賬戶地址。第 33 行代碼使用瞭 ? 操作符,如果迭代器中有賬戶地址,會將賬戶地址與變量 account 綁定。如果迭代器中沒有賬戶地址,? 操作符會讓程序執行失敗。

第 36~39 行代碼判斷存儲狀態的賬戶所有者是否是當前程序。隻有賬戶所有者才能修改數據,否則輸出日志並返回。

第 42~44 行代碼先對賬戶中的數據進行反序列化操作,再將 counter 加一,最後將其序列化後存儲到賬戶中。

代碼清單 1-1 helloworld 鏈上程序

use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{    account_info::{next_account_info, AccountInfo},    entrypoint,    entrypoint::ProgramResult,    msg,    program_error::ProgramError,    pubkey::Pubkey,};/// Define the type of state stored in accounts#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct GreetingAccount {    /// number of greetings    pub counter: u32,}// Declare and export the program's entrypointentrypoint!(process_instruction);// Program entrypoint's implementationpub fn process_instruction(    program_id: &Pubkey, // Public key of the account the hello world program was loaded into    accounts: &[AccountInfo], // The account to say hello to    _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos) -> ProgramResult {    msg!("Hello World Rust program entrypoint");    // Iterating accounts is safer then indexing    let accounts_iter = &mut accounts.iter();    // Get the account to say hello to    let account = next_account_info(accounts_iter)?;    // The account must be owned by the program in order to modify its data    if account.owner != program_id {   msg!("Greeted account does not have the correct program id");   return Err(ProgramError::IncorrectProgramId);    }    // Increment and store the number of times the account has been greeted    let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;    greeting_account.counter += 1;    greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;    msg!("Greeted {} time(s)!", greeting_account.counter);    Ok(())}

2. 客戶端程序源碼解讀

要想測試鏈上程序,我們必須通過 Solana JSON RPC API 去和鏈上程序進行交互。example-helloworld 項目提供的客戶端用 Typescript 編寫,使用瞭 web3.js 庫這個 Solana JavaScript SDK。

在 client 目錄下,客戶端執行的入口是 main.ts 文件,它按特定的順序執行任務,每個任務的業務邏輯代碼在 hello_world.ts 文件。

首先,客戶端調用 establishConnection 函數與集群建立連接。

export async function establishConnection(): Promise<void> {  const rpcUrl = await getRpcUrl();  connection = new Connection(rpcUrl, 'confirmed');  const version = await connection.getVersion();  console.log('Connection to cluster established:', rpcUrl, version);}

接著,客戶端調用 establishPayer 函數來確保有一個有支付能力的賬戶。

export async function establishPayer(): Promise<void> {  let fees = 0;  if (!payer) {    const {feeCalculator} = await connection.getRecentBlockhash();    // Calculate the cost to fund the greeter account    fees += await connection.getMinimumBalanceForRentExemption(GREETING_SIZE);    // Calculate the cost of sending transactions    fees += feeCalculator.lamportsPerSignature * 100; // wag    try { // Get payer from cli config payer = await getPayer();    } catch (err) { // Fund a new payer via airdrop payer = await newAccountWithLamports(connection, fees);    }  }  const lamports = await connection.getBalance(payer.publicKey);  if (lamports < fees) {    // This should only happen when using cli config keypair    const sig = await connection.requestAirdrop( payer.publicKey, fees - lamports,    );    await connection.confirmTransaction(sig);  }  console.log(    'Using account',    payer.publicKey.toBase58(),    'containing',    lamports / LAMPORTS_PER_SOL,    'SOL to pay for fees',  );}

然後,客戶端調用 checkProgram 函數從 src/program-rust/target/deploy/helloworld-keypair.json 中加載已部署程序的密鑰對(此操作前需先構建鏈上程序,詳見 1.3.2 節),並使用密鑰對的公鑰來獲取程序賬戶。如果程序不存在,客戶端會報錯並停止執行。如果程序存在,將創建一個新賬戶來存儲狀態,並以該程序作為新賬戶所有者。這裡新賬戶存儲的狀態,就是程序被調用的次數。

export async function checkProgram(): Promise<void> {  // Read program id from keypair file  try {    const programKeypair = await createKeypairFromFile(PROGRAM_KEYPAIR_PATH);    programId = programKeypair.publicKey;  } catch (err) {    const errMsg = (err as Error).message;    throw new Error( `Failed to read program keypair at '${PROGRAM_KEYPAIR_PATH}' due to error: ${errMsg}.`,    );  }  // Check if the program has been deployed  const programInfo = await connection.getAccountInfo(programId);  if (programInfo === null) {    if (fs.existsSync(PROGRAM_SO_PATH)) { throw new Error(   'Program needs to be deployed with `solana program deploy dist/program/helloworld.so`', );    } else { throw new Error('Program needs to be built and deployed');    }  } else if (!programInfo.executable) {    throw new Error(`Program is not executable`);  }  console.log(`Using program ${programId.toBase58()}`);  // Derive the address (public key) of a greeting account from the program so that it's easy to find later.  const GREETING_SEED = 'hello';  greetedPubkey = await PublicKey.createWithSeed(    payer.publicKey,    GREETING_SEED,    programId,  );  // Check if the greeting account has already been created  const greetedAccount = await connection.getAccountInfo(greetedPubkey);  if (greetedAccount === null) {    console.log( 'Creating account', greetedPubkey.toBase58(), 'to say hello to',    );    const lamports = await connection.getMinimumBalanceForRentExemption( GREETING_SIZE,    );    const transaction = new Transaction().add( SystemProgram.createAccountWithSeed({   fromPubkey: payer.publicKey,   basePubkey: payer.publicKey,   seed: GREETING_SEED,   newAccountPubkey: greetedPubkey,   lamports,   space: GREETING_SIZE,   programId, }),    );    await sendAndConfirmTransaction(connection, transaction, [payer]);  }}

客戶端再調用 sayHello 函數向鏈上程序發送交易。一個交易可以包含一個或多個不同的指令,當前該交易包含瞭一個指令,指令中帶有要調用鏈上程序的 Program Id 以及客戶端要交互的賬戶地址。需要註意的是,如果交易中包含多個不同的指令,其中有一個指令執行失敗,那麼所有指令所做的操作都會被還原。

export async function sayHello(): Promise<void> {  console.log('Saying hello to', greetedPubkey.toBase58());  const instruction = new TransactionInstruction({    keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}],    programId,    data: Buffer.alloc(0), // All instructions are hellos  });  await sendAndConfirmTransaction(    connection,    new Transaction().add(instruction),    [payer],  );}

最後,客戶端調用 reportGreetings 函數訪問賬戶數據,查詢鏈上程序被有效調用的次數。

export async function reportGreetings(): Promise<void> {  const accountInfo = await connection.getAccountInfo(greetedPubkey);  if (accountInfo === null) {    throw 'Error: cannot find the greeted account';  }  const greeting = borsh.deserialize(    GreetingSchema,    GreetingAccount,    accountInfo.data,  );  console.log(    greetedPubkey.toBase58(),    'has been greeted',    greeting.counter,    'time(s)',  );}

1.3.2 Hello World 構建與部署

1. 創建項目

使用 git clone 命令下載 example-helloworld 項目。

git clone https://github.com/solana-labs/example-helloworld.gitcd example-helloworld

2. 構建鏈上程序

運行以下命令,在 program-rust 目錄下構建鏈上程序。

cd src/program-rust/cargo build-bpf

構建完成後,src/program-rust/target/deploy 目錄下的 helloworld.so 就是可在 Solana 集群部署的鏈上程序的 BPF 字節碼文件。

3. 啟動本地集群

當前項目在本地集群部署運行,因此首先選擇 localhost 集群,運行以下命令。

solana config set --url localhost

本地集群設置成功,會出現以下內容。

Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed

再運行以下命令,啟動 localhost 集群。

solana-test-validator

看到以下內容,代表本地集群已成功啟動。

Ledger location: test-ledgerLog: test-ledger/validator.logIdentity: A4HuRgmABNCe94epY2mU7q6WqEHCo2B9iBFE5Yphiw5uGenesis Hash: 96TF9n1uuyFv4rAKECffA61jLrgYjMjNRZ3hJpP6HSr7Version: 1.7.18Shred Version: 13390Gossip Address: 127.0.0.1:1024TPU Address: 127.0.0.1:1027JSON RPC URL: http://127.0.0.1:8899⠉ 00:00:42 | Processed Slot: 45430 | Confirmed Slot: 45430 | Finalized Slot: 45398 | Snapshot Slot: 45300 | Transactions: 45452 | ◎499.772930000

4. 部署鏈上程序

運行以下命令,在 localhost 集群部署鏈上程序。

solana program deploy target/deploy/helloworld.so// Program Id: 6AArMEBpFhhtU2mBnEMEPeEH7xkhfUwPseUeG4fhLYto

鏈上程序部署成功會返回 Program Id,它類似於以太坊智能合約的地址。

5. 調用鏈上程序

helloworld 已成功部署,可以與它進行交互瞭!example-helloworld 項目提供瞭一個簡單的客戶端,在運行客戶端之前先安裝依賴軟件包。

npm install

由於我們調整瞭鏈上程序的構建方式,沒有使用該項目默認的 npm run build:program-rust 命令,因此需要修改 client 目錄下的 hello_world.ts 文件,將第 48 行代碼定義的變量 PROGRAM_PATH 的路徑由“../../dist/program”改為“../program-rust/target/deploy”。 再運行以下命令,啟動客戶端去調用鏈上程序。

npm run start

客戶端成功調用鏈上程序,輸出內容如下所示。如果再次運行客戶端,第 10 行所顯示的次數會加一。至此,我們已經成功在 Solana 集群部署鏈上程序並與之交互瞭。

> [email protected] start> ts-node src/client/main.tsLet's say hello to a Solana account...Connection to cluster established: http://localhost:8899 { 'feature-set': 3179062686, 'solana-core': '1.6.23' }Using account 4xRm2FYmRB8WdxJk6nXicVMgsPnsxChEnpQwFDGwdcSS containing 499999999.93435186 SOL to pay for feesUsing program 6AArMEBpFhhtU2mBnEMEPeEH7xkhfUwPseUeG4fhLYtoCreating account Eq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94Zvy to say hello toSaying hello to Eq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94ZvyEq7bcsg5p6AaYiPnfiia99ESsuq4B4jYpVbWZhQ94Zvy has been greeted 1 time(s)Success

如果沒有輸出期望值,請首先確認是否已正確啟動瞭本地集群,構建並部署好瞭鏈上程序。此外,可以運行以下命令查看程序日志,日志包括程序日志消息以及程序失敗信息。

solana logs

包含程序失敗信息的日志如下所示,檢查日志找出程序失敗的原因。

<img src="https://learnblockchain.cn/css/default/copy.svg" /><code>Transaction executed in slot 5621:Signature: 4pya5iyvNfAZj9sVWHzByrxdKB84uA5sCxLceBwr9UyuETX2QwnKg56MgBKWSM4breVRzHmpb1EZQXFPPmJnEtsJStatus: Error processing Instruction 0: Program failed to completeLog Messages:  Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA invoke   Program log: Hello World Rust program entrypoint  Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA consumed 200000 of 200000 compute units  Program failed to complete: exceeded maximum number of instructions allowed (200000) at instruction #334  Program G5bbS1ipWzqQhekkiCLn6u7Y1jJdnGK85ceSYLx2kKbA failed: Program failed to complete
1.4 本章小節

本章對 Solana 區塊鏈的基本概念進行瞭簡要介紹,Solana 的智能合約叫做鏈上程序。在開始 Solana 鏈上程序開發之前,需要先安裝和配置相關的編程環境,我們著重介紹瞭 Solana CLI 的安裝和配置。

Hello World 是一個官方演示項目,通過對這個項目源碼的解讀,我們瞭解瞭如何使用 Rust 開發鏈上程序,並使用 Solana CLI 來構建與部署,以及使用 Solana JavaScript SDK 與鏈上程序進行交互。

Solana

发表评论:

管理员

  • 内容64764
  • 积分0
  • 金币0
关于我们
区块链:筆者註:因近期筆者工作需要,開始接觸 Solana 鏈上程序開發。本系列文章是筆者的學習筆記,既是為瞭備忘,也是希望得到 Solana 開發者的指點與交流。本系列文章將默認讀者已經掌握 Rust 的基礎語法,故不涉及對 Rust 語法細節的解釋。如果讀者對 Rust 基礎語法還不熟練的話,
快捷菜单
TRX钱包 网站地图
联系方式
电话:

Copyright © 2020-2024 区块链 Inc. 保留所有权利。