介绍# 一、

整体流程是DApp客户端将自定义的指令数据序列化到data里面,然后将账号信息和data发到链上,

这里介绍一些SDK中提供的主要数据结构。

1. Pubkey

#[repr(transparent)] #[derive(Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash, AbiExample)] pub struct Pubkey([u8; 32]);]

Pubkey实际是就是32个字符表示的base58的Account地址,在上面的Instruction中,我们看到的ProgramId 就是这样的类型,因为Program本身其实一个文件,也就是Account,只是是可执行的文件。

2. AccountInfo

/// Account information #[derive(Clone)] pub struct AccountInfo<'a> {     /// Public key of the account     pub key: &'a Pubkey,     /// Was the transaction signed by this account's public key?     pub is_signer: bool,     /// Is the account writable?     pub is_writable: bool,     /// The lamports in the account.  Modifiable by programs.     pub lamports: Rc>,     /// The data held in this account.  Modifiable by programs.     pub data: Rc>,     /// Program that owns this account     pub owner: &'a Pubkey,     /// This account's data contains a loaded program (and is now read-only)     pub executable: bool,     /// The epoch at which this account will next owe rent     pub rent_epoch: Epoch, }

AccountInfo就是一个Account在链上的表达形式,可以认为是一个文件的属性,想象一些state函数列出 的文件属性。其中,key表示文件名,也就是base58的地址。而文件大小可以认为是lamports,这里区别与我们操作系统里面的文件,操作系统里面的文件的大小是可以为0的,且文件存在,而

ProgramResult实际上类型为ProgramError的Result对象,而ProgramError是

第 1 行代码将 borsh::BorshDeserializeborsh::BorshSerialize 引入本地作用域,用于序列化和反序列化数据。第 2~9 行代码将

如果要从头创建一个solanan合约,使用命令生成项目目录及目录下的Cargo.toml文件

% cargo new onchain_program

同时在这个目录下增加Xargo.toml

[target.bpfel-unknown-unknown.dependencies.std] features = []

用于增加bpf的跨平台编译支持。接着编写合约内容,在src/lib.rs里面设置entrypoint:

// 声明是程序的主入口 entrypoint!(process_instruction);

然后在lib.rs编写合约内容:

//! Program entrypoint  use solana_program::{      account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey, }; use std::str::from_utf8;entrypoint!(process_instruction);  fn process_instruction(     _program_id: &Pubkey,     _accounts: &[AccountInfo],     instruction_data: &[u8], ) -> ProgramResult {     Ok(()) }

这个合约的内容,是不做任何处理。直接返回成功。

函数process_instruction是整个合约入口,传入的是一个instruction结构。他包含了用于执行指令的程序账户地址:_program_id,所要执行使用的账户集:_accounts,以及序列化之后的 instruction_data部分。当认为执行成功时,通过Ok(())返回成功,否则用Err(error)返回出错。

6、从头创建合约项目

创建工程,指定–lib代表为库文件。

% cargo new  helloworld --lib Created library `helloworld` package

生成的Cargo.toml文件。

[package] name = "helloworld" version = "0.1.0" edition = "2021"  [dependencies]

对其进行修改,features 里面增加了no-entrypoint特性,dependencies里面增加了

同时在Cargo.toml同级目录创建文件”Xargo.toml” 用于跨平台生成BPF目标文件格式。内容为:

[target.bpfel-unknown-unknown.dependencies.std] features = []

目的是

根据这个逻辑结构,我们依次创建如下几个文件:

  • instruction.rs : 解析由runtime传过来的instruction
  • processor.rs : 针对instruction的合约逻辑
  • state.rs : 将需要存储的内容进行打包存储

同时为了方便程序书写,我们创建:

  • error.rs: 出错处理,定义各种错误
  • entrypoint.rs : 结合“entrypoint”特性,封装合约入口
├── Cargo.lock ├── Cargo.toml ├── src │   ├── entrypoint.rs │   ├── error.rs │   ├── instruction.rs │   ├── lib.rs │   ├── processsor.rs │   └── state.rs └── Xargo.toml

1. entrypoint:合约入口

entrypoint是所有合约的入口,是一个处理函数,原型为:

entrypoint!(process_instruction); fn process_instruction(     program_id: &Pubkey,     accounts: &[AccountInfo],     instruction_data: &[u8], ) -> ProgramResult

通过entrypoint特性指定入口函数的函数名,而函数定义为接受三个参数并返回ProgramResult类型的函数,三个参数依次是合约的地址program_idinstruction里面keys经过runtime解析得到的账号信息数组accounts,以及instruction里面的data部分。

这里将对instrcution封装到了process里面,因此这里直接调用process的函数:

entrypoint!(process_instruction); fn process_instruction(     program_id: &Pubkey,     accounts: &[AccountInfo],     instruction_data: &[u8], ) -> ProgramResult {     if let Err(error) = Processor::process(program_id, accounts, instruction_data) {         // catch the error so we can print it         error.print::();         return Err(error);     }     Ok(()) }

注意这里增加了错误时候的捕捉:

error.print::();

当出错的时候,会在日志和RPC调用里面返回出错信息。这里HelloWorldError就是error.rs里面定义的程序错误。

2. error:处理错误

error的定义主要是用于收敛程序中的错误,并给出具体的错误消息,如果对应的错误出现,在RPC调用时,会明确给出错误提示:

Custom Error: 0x02

对应错误的枚举值。

/// Errors that may be returned by the hello-world program. #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum HelloWorldError {     /// Invalid instruction     #[error("Invalid instruction")]     InvalidInstruction, }  impl From for ProgramError {     fn from(e: HelloWorldError) -> Self {         ProgramError::Custom(e as u32)     } }  impl DecodeError for HelloWorldError {     fn type_of() -> &'static str {         "HelloWorldError"     } }  impl PrintProgramError for HelloWorldError {     fn print(&self)     where         E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive,     {         match self {             RegistryError::InvalidInstruction => info!("Invalid instruction"),         }     } }

这里为了使得Error可以打印,用了几个辅助库,所以在Cargo.toml的dependence里面增加:

num-derive = "0.3" thiserror = "1.0" num-traits = "0.2" arrayref = "0.3.6" num_enum = "0.5.1"

HelloWorldError即为定义的错误枚举,然后为枚举实现了”From”、”DecodeError” 以及”PrintProgramError” 等traits。

3. instruction:反序列化指令

/// Instructions supported by the hello-world program. #[repr(C)] #[derive(Clone, Debug, PartialEq)] pub enum HelloWorldInstruction {     /// Hello print hello to an Account file     Hello{         /// message for hello         message: String,     },     /// Erase free the hello account     Erase , }

定义了2个指令,一个是带有一个String类型参数的 “Hello” 另一个是删除文件的不带参数的 “Erase”。定义好结构后,需要为其 书写反序列化函数,对于instruction真正工作的其实只有反序列化函数,比如这里叫unpack,而序列化是在客户端请求做的,因此pack函数不是必须的,但是如果使用单元测试的时候,可能需要通过pack来构建hook内容。

对于序列化的格式,采用了固定长度的二进制堆叠法:

+-----------------------------------+ Hello:      |   0    |       message            |             +-----------------------------------+ Erase:      |   1    |             +--------+

如上图,第一个字节表示消息的类型,对于Hello消息,消息内容紧随其后。

所以解析代码可以这样写:

impl HelloWorldInstruction {     /// Unpacks a byte buffer into a [HelloWorldInstruction](enum.HelloWorldInstruction.html).     pub fn unpack(input: &[u8]) -> Result {         use HelloWorldError::InvalidInstruction;                let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?;         Ok(match tag { //HelloWorld              0 => {                  let message= String::from(from_utf8(rest).unwrap());                  Self::Hello{                       message,                  }             },             1 => Self::Erase,  _ => return Err(HelloWorldError::InvalidInstruction.into()),         }) }

4. state:存储数据的格式定义

state是用来将内容存储到对应的文件时,存储格式的定义,类似一个ORM或者所谓的MVC中Model层。 因此首先定义Model:

/// HelloWorld data. #[repr(C)] #[derive(Clone, Debug, Default, PartialEq)] pub struct HelloWorldState {     /// account     pub account_key: Pubkey,     /// message     pub message: String }

这里定义了谁:account 说了什么:message。然后定义了Model层操作文件的方法,这里通过

这里采用mut_array_refs预先给几个要存储的元素分配好地址,然后使用copy_from_slice复制32字节的key,用as u8转换长度,copy_from_slice copy字符串内容。

5. process:处理合约逻辑

最关键的处理程序来了。首先我们将runtime给过来要处理的instruction进行反序列化操作:

/// Processes an [Instruction](enum.Instruction.html). pub fn process(_program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {     let instruction = HelloWorldInstruction::unpack(input)?;      match instruction {         HelloWorldInstruction::Hello {         message,     } => {         msg!(&format!("hello-world: HelloWorld msg:{}", message));         Self::process_hello(accounts, message)     }     HelloWorldInstruction::Erase=>{         msg!("hello-world: Erase");         Self::process_erase(accounts)     } }

对于Hello,处理就是将消息内容和谁发的信息,进行记录:

/// Processes an [Hello](enum.HelloWorldInstruction.html) instruction. fn process_hello(     accounts: &[AccountInfo],     message: String, ) -> ProgramResult {         let account_info_iter = &mut accounts.iter();     let client_info = next_account_info(account_info_iter)?;     let message_info = next_account_info(account_info_iter)?;    // check permission     if !client_info.is_signer || !message_info.is_signer{         return Err(ProgramError::MissingRequiredSignature);     }      msg!("before unpack hello");     let mut state = HelloWorldState::unpack_unchecked(&message_info.data.borrow())?;     msg!("after unpack hello");     state.account_key = *client_info.key;     state.message = message;      msg!("before pack hello");     HelloWorldState::pack(state, &mut message_info.data.borrow_mut())?;     msg!("after pack hello");         Ok(()) }

用户传递instruction里面的keys数组就对应这里的accounts数组,用户将其创建的消息账号通过这个数组传递过来,通过next_account_info进行获取,分别获取了用户账号client_info和消息账号message_info 。这里通过判断!client_info.is_signer来判断,用户构建transaction是否用了自己的进行签名, 如果是的话,runtime会进行校验,因此只要判断,他是否是被校验的单元就好了,无需自己去调用鉴权接口。

接着就是Model里面先解析Model对象,然后进行修改后,在写回Model对象。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注