同时在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_id
、instruction
里面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
对象。