Add support for idiomatic error handling to BPF instruction processors (#7968)

This commit is contained in:
Jack May
2020-01-30 09:47:22 -08:00
committed by GitHub
parent 0c55b37976
commit dd276138c2
25 changed files with 515 additions and 108 deletions

View File

@@ -1198,6 +1198,16 @@ dependencies = [
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-derive"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-derive"
version = "0.3.0"
@@ -1995,6 +2005,17 @@ dependencies = [
"solana-sdk-bpf-test 0.24.0",
]
[[package]]
name = "solana-bpf-rust-error-handling"
version = "0.24.0"
dependencies = [
"num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"solana-sdk 0.24.0",
"solana-sdk-bpf-test 0.24.0",
"thiserror 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "solana-bpf-rust-external-spend"
version = "0.24.0"
@@ -3156,6 +3177,7 @@ dependencies = [
"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
"checksum num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
"checksum num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0c8b15b261814f992e33760b1fca9fe8b693d8a65299f20c9901688636cfb746"
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"

View File

@@ -38,6 +38,7 @@ members = [
"rust/alloc",
"rust/dep_crate",
"rust/dup_accounts",
"rust/error_handling",
"rust/external_spend",
"rust/iter",
"rust/many_args",

View File

@@ -69,9 +69,10 @@ fn main() {
"alloc",
"dep_crate",
"dup_accounts",
"error_handling",
"external_spend",
"iter",
"many_args",
"external_spend",
"noop",
"panic",
"param_passing",

View File

@@ -4,7 +4,7 @@
Test(bench_alu, sanity) {
uint64_t input[] = {500, 0};
cr_assert_eq(entrypoint((uint8_t *) input), 0);
cr_assert_eq(entrypoint((uint8_t *) input), SUCCESS);
cr_assert_eq(input[0], 500);
cr_assert_eq(input[1], 5);

View File

@@ -9,49 +9,46 @@
*/
extern uint32_t entrypoint(const uint8_t *input) {
#define FAILURE 1
#define INVALID_INPUT 2
SolKeyedAccount ka[4];
SolParameters params = (SolParameters) { .ka = ka };
if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(ka))) {
return INVALID_INPUT;
return INVALID_ARGUMENT;
}
switch (params.data[0]) {
case(1):
sol_log("modify first account userdata");
ka[2].userdata[0] = 1;
break;
case(2):
sol_log("modify first account userdata");
ka[3].userdata[0] = 2;
break;
case(3):
sol_log("modify both account userdata");
ka[2].userdata[0] += 1;
ka[3].userdata[0] += 2;
break;
case(4):
sol_log("modify first account lamports");
*ka[1].lamports -= 1;
*ka[2].lamports += 1;
break;
case(5):
sol_log("modify first account lamports");
*ka[1].lamports -= 2;
*ka[3].lamports += 2;
break;
case(6):
sol_log("modify both account lamports");
*ka[1].lamports -= 3;
*ka[2].lamports += 1;
*ka[3].lamports += 2;
break;
default:
sol_log("Unrecognized command");
return FAILURE;
case(1):
sol_log("modify first account userdata");
ka[2].userdata[0] = 1;
break;
case(2):
sol_log("modify first account userdata");
ka[3].userdata[0] = 2;
break;
case(3):
sol_log("modify both account userdata");
ka[2].userdata[0] += 1;
ka[3].userdata[0] += 2;
break;
case(4):
sol_log("modify first account lamports");
*ka[1].lamports -= 1;
*ka[2].lamports += 1;
break;
case(5):
sol_log("modify first account lamports");
*ka[1].lamports -= 2;
*ka[3].lamports += 2;
break;
case(6):
sol_log("modify both account lamports");
*ka[1].lamports -= 3;
*ka[2].lamports += 1;
*ka[3].lamports += 2;
break;
default:
sol_log("Unrecognized command");
return INVALID_INSTRUCTION_DATA;
}
return SUCCESS;
}

View File

@@ -0,0 +1,37 @@
/**
* @brief Example C-based BPF program that exercises duplicate keyed ka
* passed to it
*/
#include <solana_sdk.h>
/**
* Custom error for when input serialization fails
*/
extern uint32_t entrypoint(const uint8_t *input) {
SolKeyedAccount ka[4];
SolParameters params = (SolParameters) { .ka = ka };
if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(ka))) {
return INVALID_ARGUMENT;
}
switch (params.data[0]) {
case(1):
sol_log("return success");
return SUCCESS;
case(2):
sol_log("return a builtin");
return INVALID_ACCOUNT_DATA;
case(3):
sol_log("return custom error");
return 42;
case(4):
sol_log("return error that conflicts with success");
return 0x40000000;
default:
sol_log("Unrecognized command");
return INVALID_INSTRUCTION_DATA;
}
return SUCCESS;
}

View File

@@ -11,27 +11,17 @@
*/
#define NUM_KA 3
/**
* Custom error for when input serialization fails
*/
#define INVALID_INPUT 1
/**
* Custom error for when transaction is not signed properly
*/
#define NOT_SIGNED 2
extern uint32_t entrypoint(const uint8_t *input) {
SolKeyedAccount ka[NUM_KA];
SolParameters params = (SolParameters) { .ka = ka };
if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(ka))) {
return INVALID_INPUT;
return INVALID_ARGUMENT;
}
if (!params.ka[0].is_signer) {
sol_log("Transaction not signed by key 0");
return NOT_SIGNED;
return MISSING_REQUIRED_SIGNATURES;
}
int64_t lamports = *(int64_t *)params.data;

View File

@@ -16,7 +16,7 @@ extern uint32_t entrypoint(const uint8_t *input) {
sol_log(__FILE__);
if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(ka))) {
return INVALID_INPUT;
return INVALID_ARGUMENT;
}
// Log the provided input parameters. In the case of the no-op

View File

@@ -4,11 +4,6 @@
*/
#include <solana_sdk.h>
/**
* Custom error for when input serialization fails
*/
#define INVALID_INPUT 1
extern uint32_t entrypoint(const uint8_t *input) {
SolKeyedAccount ka[1];
SolParameters params = (SolParameters) { .ka = ka };
@@ -16,7 +11,7 @@ extern uint32_t entrypoint(const uint8_t *input) {
sol_log(__FILE__);
if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(ka))) {
return INVALID_INPUT;
return INVALID_ARGUMENT;
}
// Log the provided input parameters. In the case of the no-op

View File

@@ -2,7 +2,7 @@
extern crate solana_sdk;
use solana_sdk::{
account_info::AccountInfo, entrypoint, entrypoint::SUCCESS, info, pubkey::Pubkey,
account_info::AccountInfo, entrypoint, info, program_error::ProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
@@ -10,9 +10,7 @@ fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> u32 {
const FAILURE: u32 = 1;
) -> Result<(), ProgramError> {
match instruction_data[0] {
1 => {
info!("modify first account data");
@@ -45,8 +43,8 @@ fn process_instruction(
}
_ => {
info!("Unrecognized command");
return FAILURE;
return Err(ProgramError::InvalidArgument);
}
}
SUCCESS
Ok(())
}

View File

@@ -0,0 +1,29 @@
# Note: This crate must be built using do.sh
[package]
name = "solana-bpf-rust-error-handling"
version = "0.24.0"
description = "Solana BPF test program written in Rust"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
num-derive = "0.2"
num-traits = "0.2"
solana-sdk = { path = "../../../../sdk/", version = "0.24.0", default-features = false }
thiserror = "1.0"
[dev_dependencies]
solana-sdk-bpf-test = { path = "../../../../sdk/bpf/rust/test", version = "0.24.0" }
[features]
program = ["solana-sdk/program"]
default = ["program"]
[lib]
name = "solana_bpf_rust_error_handling"
crate-type = ["cdylib"]

View File

@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@@ -0,0 +1,60 @@
//! @brief Example Rust-based BPF program that exercises error handling
extern crate solana_sdk;
use num_derive::FromPrimitive;
use solana_sdk::{
account_info::AccountInfo, entrypoint, info, program_error::ProgramError, pubkey::Pubkey,
};
use thiserror::Error;
/// Custom program errors
#[derive(Error, Debug, Clone, PartialEq, FromPrimitive)]
// Clippy compains about 0x8000_002d, but we don't care about C compatibility here
#[allow(clippy::enum_clike_unportable_variant)]
pub enum MyError {
#[error("The Answer")]
TheAnswer = 42,
#[error("Conflicting with success")]
ConflictingSuccess = 0,
#[error("Conflicting with builtin")]
ConflictingBuiltin = 0x8000_002d,
}
impl From<MyError> for ProgramError {
fn from(e: MyError) -> Self {
ProgramError::CustomError(e as u32)
}
}
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
match instruction_data[0] {
1 => {
info!("return success");
Ok(())
}
2 => {
info!("return a builtin");
Err(ProgramError::AccountBorrowFailed)
}
3 => {
info!("return custom error");
Err(MyError::TheAnswer.into())
}
4 => {
info!("return error that conflicts with success");
Err(MyError::ConflictingSuccess.into())
}
5 => {
info!("return error that conflicts with builtin");
Err(MyError::ConflictingBuiltin.into())
}
_ => {
info!("Unrecognized command");
Err(ProgramError::InvalidInstructionData)
}
}
}

View File

@@ -1,18 +1,20 @@
//! @brief Example Rust-based BPF program that moves a lamport from one account to another
extern crate solana_sdk;
use solana_sdk::{account_info::AccountInfo, entrypoint, entrypoint::SUCCESS, pubkey::Pubkey};
use solana_sdk::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> u32 {
) -> Result<(), ProgramError> {
// account 0 is the mint and not owned by this program, any debit of its lamports
// should result in a failed program execution. Test to ensure that this debit
// is seen by the runtime and fails as expected
**accounts[0].lamports.borrow_mut() -= 1;
SUCCESS
Ok(())
}

View File

@@ -3,8 +3,10 @@
#![allow(unreachable_code)]
extern crate solana_sdk;
use solana_sdk::{
account_info::AccountInfo, entrypoint, entrypoint::SUCCESS, info, log::*, pubkey::Pubkey,
account_info::AccountInfo, entrypoint, info, log::*, program_error::ProgramError,
pubkey::Pubkey,
};
#[derive(Debug, PartialEq)]
@@ -24,7 +26,7 @@ fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> u32 {
) -> Result<(), ProgramError> {
info!("Program identifier:");
program_id.log();
@@ -58,7 +60,7 @@ fn process_instruction(
panic!();
}
SUCCESS
Ok(())
}
#[cfg(test)]

View File

@@ -4,10 +4,9 @@ extern crate solana_sdk;
use solana_sdk::{
account_info::AccountInfo,
clock::{get_segment_from_slot, DEFAULT_SLOTS_PER_EPOCH, DEFAULT_SLOTS_PER_SEGMENT},
entrypoint,
entrypoint::SUCCESS,
info,
entrypoint, info,
log::Log,
program_error::ProgramError,
pubkey::Pubkey,
rent,
sysvar::{
@@ -21,7 +20,7 @@ fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> u32 {
) -> Result<(), ProgramError> {
// Clock
info!("Clock identifier:");
sysvar::clock::id().log();
@@ -66,5 +65,5 @@ fn process_instruction(
(0, true)
);
SUCCESS
Ok(())
}

View File

@@ -31,9 +31,10 @@ mod bpf {
account::Account,
bpf_loader,
client::SyncClient,
instruction::{AccountMeta, Instruction},
instruction::{AccountMeta, Instruction, InstructionError},
pubkey::Pubkey,
signature::KeypairUtil,
transaction::TransactionError,
};
use std::{io::Read, sync::Arc};
@@ -152,6 +153,59 @@ mod bpf {
assert!(result.is_ok());
assert_eq!(lamports, 13);
}
#[test]
fn test_program_bpf_c_error_handling() {
solana_logger::setup();
let filename = create_bpf_path("error_handling");
let mut file = File::open(filename).unwrap();
let mut elf = Vec::new();
file.read_to_end(&mut elf).unwrap();
let GenesisConfigInfo {
genesis_config,
mint_keypair,
..
} = create_genesis_config(50);
let bank = Bank::new(&genesis_config);
let bank_client = BankClient::new(bank);
let program_id = load_program(&bank_client, &mint_keypair, &bpf_loader::id(), elf);
let account_metas = vec![AccountMeta::new(mint_keypair.pubkey(), true)];
let instruction = Instruction::new(program_id, &1u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert!(result.is_ok());
let instruction = Instruction::new(program_id, &2u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
);
let instruction = Instruction::new(program_id, &3u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::CustomError(42))
);
let instruction = Instruction::new(program_id, &4u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::ConflictingError(0))
);
let instruction = Instruction::new(program_id, &6u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidInstructionData)
);
}
}
#[cfg(feature = "bpf_rust")]
@@ -162,10 +216,11 @@ mod bpf {
bpf_loader,
client::SyncClient,
clock::DEFAULT_SLOTS_PER_EPOCH,
instruction::{AccountMeta, Instruction},
instruction::{AccountMeta, Instruction, InstructionError},
pubkey::Pubkey,
signature::{Keypair, KeypairUtil},
sysvar::{clock, fees, rent, rewards, slot_hashes, stake_history},
transaction::TransactionError,
};
use std::io::Read;
use std::sync::Arc;
@@ -300,5 +355,68 @@ mod bpf {
assert!(result.is_ok());
assert_eq!(lamports, 13);
}
#[test]
fn test_program_bpf_rust_error_handling() {
solana_logger::setup();
let filename = create_bpf_path("solana_bpf_rust_error_handling");
let mut file = File::open(filename).unwrap();
let mut elf = Vec::new();
file.read_to_end(&mut elf).unwrap();
let GenesisConfigInfo {
genesis_config,
mint_keypair,
..
} = create_genesis_config(50);
let bank = Bank::new(&genesis_config);
let bank_client = BankClient::new(bank);
let program_id = load_program(&bank_client, &mint_keypair, &bpf_loader::id(), elf);
let account_metas = vec![AccountMeta::new(mint_keypair.pubkey(), true)];
let instruction = Instruction::new(program_id, &1u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert!(result.is_ok());
let instruction = Instruction::new(program_id, &2u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::AccountBorrowFailed)
);
let instruction = Instruction::new(program_id, &3u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::CustomError(42))
);
let instruction = Instruction::new(program_id, &4u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::ConflictingError(0))
);
let instruction = Instruction::new(program_id, &5u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(
0,
InstructionError::ConflictingError(0x8000_002d)
)
);
let instruction = Instruction::new(program_id, &6u8, account_metas.clone());
let result = bank_client.send_instruction(&mint_keypair, instruction);
assert_eq!(
result.unwrap_err().unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidInstructionData)
);
}
}
}

View File

@@ -15,7 +15,6 @@ use solana_sdk::{
sysvar::rent,
};
use std::{
convert::TryFrom,
io::{prelude::*, Error},
mem,
};
@@ -145,18 +144,15 @@ pub fn process_instruction(
info!("Call BPF program");
match vm.execute_program(parameter_bytes.as_slice(), &[], &[heap_region]) {
Ok(status) => match u32::try_from(status) {
Ok(status) => {
if status > 0 {
warn!("BPF program failed: {}", status);
return Err(InstructionError::CustomError(status));
}
Ok(status) => {
// ignore upper 32bits if any, programs only return lower 32bits
let status = status as u32;
if status != 0 {
let error: InstructionError = status.into();
warn!("BPF program failed: {:?}", error);
return Err(error);
}
Err(e) => {
warn!("BPF VM encountered invalid status: {}", e);
return Err(InstructionError::GenericError);
}
},
}
Err(e) => {
warn!("BPF VM failed to run program: {}", e);
return Err(InstructionError::GenericError);