diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 1d49764ea7..2df278a720 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -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" diff --git a/programs/bpf/Cargo.toml b/programs/bpf/Cargo.toml index 06afd12e6f..e6a0c88da4 100644 --- a/programs/bpf/Cargo.toml +++ b/programs/bpf/Cargo.toml @@ -38,6 +38,7 @@ members = [ "rust/alloc", "rust/dep_crate", "rust/dup_accounts", + "rust/error_handling", "rust/external_spend", "rust/iter", "rust/many_args", diff --git a/programs/bpf/build.rs b/programs/bpf/build.rs index 3f90938ad6..8b48fbad58 100644 --- a/programs/bpf/build.rs +++ b/programs/bpf/build.rs @@ -69,9 +69,10 @@ fn main() { "alloc", "dep_crate", "dup_accounts", + "error_handling", + "external_spend", "iter", "many_args", - "external_spend", "noop", "panic", "param_passing", diff --git a/programs/bpf/c/src/bench_alu/test_bench_alu.c b/programs/bpf/c/src/bench_alu/test_bench_alu.c index 7219d977e8..6f36bd27e9 100644 --- a/programs/bpf/c/src/bench_alu/test_bench_alu.c +++ b/programs/bpf/c/src/bench_alu/test_bench_alu.c @@ -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); diff --git a/programs/bpf/c/src/dup_accounts/dup_accounts.c b/programs/bpf/c/src/dup_accounts/dup_accounts.c index cce0a7195b..4fabcb4866 100644 --- a/programs/bpf/c/src/dup_accounts/dup_accounts.c +++ b/programs/bpf/c/src/dup_accounts/dup_accounts.c @@ -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, ¶ms, 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; } diff --git a/programs/bpf/c/src/error_handling/error_handling.c b/programs/bpf/c/src/error_handling/error_handling.c new file mode 100644 index 0000000000..ce131eda88 --- /dev/null +++ b/programs/bpf/c/src/error_handling/error_handling.c @@ -0,0 +1,37 @@ +/** + * @brief Example C-based BPF program that exercises duplicate keyed ka + * passed to it + */ +#include + +/** + * 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, ¶ms, 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; +} diff --git a/programs/bpf/c/src/move_funds/move_funds.c b/programs/bpf/c/src/move_funds/move_funds.c index 4201f770b3..d38325ba03 100644 --- a/programs/bpf/c/src/move_funds/move_funds.c +++ b/programs/bpf/c/src/move_funds/move_funds.c @@ -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, ¶ms, 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; diff --git a/programs/bpf/c/src/noop++/noop++.cc b/programs/bpf/c/src/noop++/noop++.cc index 0f62db39da..2ad78b4e49 100644 --- a/programs/bpf/c/src/noop++/noop++.cc +++ b/programs/bpf/c/src/noop++/noop++.cc @@ -16,7 +16,7 @@ extern uint32_t entrypoint(const uint8_t *input) { sol_log(__FILE__); if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) { - return INVALID_INPUT; + return INVALID_ARGUMENT; } // Log the provided input parameters. In the case of the no-op diff --git a/programs/bpf/c/src/noop/noop.c b/programs/bpf/c/src/noop/noop.c index 1f03047f1f..31985ed4bc 100644 --- a/programs/bpf/c/src/noop/noop.c +++ b/programs/bpf/c/src/noop/noop.c @@ -4,11 +4,6 @@ */ #include -/** - * 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, ¶ms, SOL_ARRAY_SIZE(ka))) { - return INVALID_INPUT; + return INVALID_ARGUMENT; } // Log the provided input parameters. In the case of the no-op diff --git a/programs/bpf/rust/dup_accounts/src/lib.rs b/programs/bpf/rust/dup_accounts/src/lib.rs index 744a73afbd..c51384e7f8 100644 --- a/programs/bpf/rust/dup_accounts/src/lib.rs +++ b/programs/bpf/rust/dup_accounts/src/lib.rs @@ -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(()) } diff --git a/programs/bpf/rust/error_handling/Cargo.toml b/programs/bpf/rust/error_handling/Cargo.toml new file mode 100644 index 0000000000..8ddbdb4181 --- /dev/null +++ b/programs/bpf/rust/error_handling/Cargo.toml @@ -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 "] +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"] diff --git a/programs/bpf/rust/error_handling/Xargo.toml b/programs/bpf/rust/error_handling/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/programs/bpf/rust/error_handling/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/programs/bpf/rust/error_handling/src/lib.rs b/programs/bpf/rust/error_handling/src/lib.rs new file mode 100644 index 0000000000..31ea3e4af6 --- /dev/null +++ b/programs/bpf/rust/error_handling/src/lib.rs @@ -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 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) + } + } +} diff --git a/programs/bpf/rust/external_spend/src/lib.rs b/programs/bpf/rust/external_spend/src/lib.rs index 73bbf5d491..a77098aaa5 100644 --- a/programs/bpf/rust/external_spend/src/lib.rs +++ b/programs/bpf/rust/external_spend/src/lib.rs @@ -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(()) } diff --git a/programs/bpf/rust/noop/src/lib.rs b/programs/bpf/rust/noop/src/lib.rs index c5898e80ae..f3bec99608 100644 --- a/programs/bpf/rust/noop/src/lib.rs +++ b/programs/bpf/rust/noop/src/lib.rs @@ -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)] diff --git a/programs/bpf/rust/sysval/src/lib.rs b/programs/bpf/rust/sysval/src/lib.rs index 57c8802261..d57429b1c7 100644 --- a/programs/bpf/rust/sysval/src/lib.rs +++ b/programs/bpf/rust/sysval/src/lib.rs @@ -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(()) } diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 3676f204f8..775b606eae 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -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) + ); + } } } diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index d989540298..1196b08ec7 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -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); diff --git a/programs/bpf_loader/test_elfs/noop.so b/programs/bpf_loader/test_elfs/noop.so index 6081fb9252..6790a77e77 100755 Binary files a/programs/bpf_loader/test_elfs/noop.so and b/programs/bpf_loader/test_elfs/noop.so differ diff --git a/sdk/bpf/c/inc/solana_sdk.h b/sdk/bpf/c/inc/solana_sdk.h index 4b107d8e0a..f4851a6d26 100644 --- a/sdk/bpf/c/inc/solana_sdk.h +++ b/sdk/bpf/c/inc/solana_sdk.h @@ -56,6 +56,36 @@ static_assert(sizeof(uint64_t) == 8); */ #define SUCCESS 0 +/** + * Builtin program error return values have the 31st bit set. Programs + * may define their own values but their 31st and 30th bit must be unset + * to avoid conflicting with the builtin errors + */ +#define BUILTIN_ERROR_START 0x80000000 + +/** The arguments provided to a program instruction where invalid */ +#define INVALID_ARGUMENT (BUILTIN_ERROR_START + 0) +/** An instruction's data contents was invalid */ +#define INVALID_INSTRUCTION_DATA (BUILTIN_ERROR_START + 1) +/** An account's data contents was invalid */ +#define INVALID_ACCOUNT_DATA (BUILTIN_ERROR_START + 2) +/** An account's data was too small */ +#define ACCOUNT_DATA_TOO_SMALL (BUILTIN_ERROR_START + 3) +/** An account's balance was too small to complete the instruction */ +#define INSUFFICIENT_FUNDS (BUILTIN_ERROR_START + 4) +/** The account did not have the expected program id */ +#define INCORRECT_PROGRAM_ID (BUILTIN_ERROR_START + 5) +/** A signature was required but not found */ +#define MISSING_REQUIRED_SIGNATURES (BUILTIN_ERROR_START + 6) +/** An initialize instruction was sent to an account that has already been initialized */ +#define ACCOUNT_ALREADY_INITIALIZED (BUILTIN_ERROR_START + 7) +/** An attempt to operate on an account that hasn't been initialized */ +#define UNINITIALIZED_ACCOUNT (BUILTIN_ERROR_START + 8) +/** The instruction expected additional account keys */ +#define NOT_ENOUGH_ACCOUNT_KEYS (BUILTIN_ERROR_START + 9) +/** Note: Not applicable to program written in C */ +#define ACCOUNT_BORROW_FAILED (BUILTIN_ERROR_START + 10) + /** * Boolean type */ diff --git a/sdk/src/entrypoint.rs b/sdk/src/entrypoint.rs index 74e5614115..4b0962cc3f 100644 --- a/sdk/src/entrypoint.rs +++ b/sdk/src/entrypoint.rs @@ -1,9 +1,7 @@ //! @brief Solana Rust-based BPF program entry point and its parameter types -#![cfg(feature = "program")] - extern crate alloc; -use crate::{account_info::AccountInfo, pubkey::Pubkey}; +use crate::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use alloc::vec::Vec; use std::{ cell::RefCell, @@ -17,8 +15,11 @@ use std::{ /// program_id: Program ID of the currently executing program /// accounts: Accounts passed as part of the instruction /// instruction_data: Instruction data -pub type ProcessInstruction = - fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> u32; +pub type ProcessInstruction = fn( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError>; /// Programs indicate success with a return value of 0 pub const SUCCESS: u32 = 0; @@ -35,10 +36,11 @@ macro_rules! entrypoint { /// # Safety #[no_mangle] pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u32 { - unsafe { - let (program_id, accounts, instruction_data) = - $crate::entrypoint::deserialize(input); - $process_instruction(&program_id, &accounts, &instruction_data) + let (program_id, accounts, instruction_data) = + unsafe { $crate::entrypoint::deserialize(input) }; + match $process_instruction(&program_id, &accounts, &instruction_data) { + Ok(()) => $crate::entrypoint::SUCCESS, + Err(error) => error.into(), } } }; diff --git a/sdk/src/instruction.rs b/sdk/src/instruction.rs index f995e842eb..2b7311016b 100644 --- a/sdk/src/instruction.rs +++ b/sdk/src/instruction.rs @@ -93,6 +93,11 @@ pub enum InstructionError { /// NOTE: u64 requires special serialization to avoid the loss of precision in JS clients and /// so is not used for now. CustomError(u32), + + /// Like CustomError but the return value from the program conflicted with + /// a builtin error. The value held by this variant is the u32 error code + /// returned by the program but with the 30th bit cleared. + ConflictingError(u32), } impl InstructionError { diff --git a/sdk/src/instruction_processor_utils.rs b/sdk/src/instruction_processor_utils.rs index 05ae3d3415..a76af5ea1b 100644 --- a/sdk/src/instruction_processor_utils.rs +++ b/sdk/src/instruction_processor_utils.rs @@ -1,7 +1,7 @@ use crate::{account::KeyedAccount, instruction::InstructionError, pubkey::Pubkey}; -use num_traits::{FromPrimitive, ToPrimitive}; +use num_traits::FromPrimitive; -// Native program ENTRYPOINT prototype +// Prototype of a native program entry point pub type Entrypoint = unsafe extern "C" fn( program_id: &Pubkey, keyed_accounts: &[KeyedAccount], @@ -99,15 +99,6 @@ macro_rules! declare_program( ) ); -impl From for InstructionError -where - T: ToPrimitive, -{ - fn from(error: T) -> Self { - InstructionError::CustomError(error.to_u32().unwrap_or(0xbad_c0de)) - } -} - /// Return the next KeyedAccount or a NotEnoughAccountKeys instruction error pub fn next_keyed_account(iter: &mut I) -> Result { iter.next().ok_or(InstructionError::NotEnoughAccountKeys) diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 79980b6f9e..88804f4764 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -59,6 +59,7 @@ pub use solana_sdk_macro::declare_id; pub mod account_info; pub mod entrypoint; pub mod log; +pub mod program_error; // Modules not usable by on-chain programs #[cfg(not(feature = "program"))] diff --git a/sdk/src/program_error.rs b/sdk/src/program_error.rs new file mode 100644 index 0000000000..122aeaeb0f --- /dev/null +++ b/sdk/src/program_error.rs @@ -0,0 +1,129 @@ +use crate::instruction::InstructionError; +use num_traits::ToPrimitive; + +/// Reasons the program may fail +pub enum ProgramError { + /// CustomError allows programs to implement program-specific error types and see + /// them returned by the Solana runtime. A CustomError may be any type that is represented + /// as or serialized to a u32 integer. + /// + /// NOTE: u64 requires special serialization to avoid the loss of precision in JS clients and + /// so is not used for now. + CustomError(u32), + /// The arguments provided to a program instruction where invalid + InvalidArgument, + /// An instruction's data contents was invalid + InvalidInstructionData, + /// An account's data contents was invalid + InvalidAccountData, + /// An account's data was too small + AccountDataTooSmall, + /// An account's balance was too small to complete the instruction + InsufficientFunds, + /// The account did not have the expected program id + IncorrectProgramId, + /// A signature was required but not found + MissingRequiredSignature, + /// An initialize instruction was sent to an account that has already been initialized. + AccountAlreadyInitialized, + /// An attempt to operate on an account that hasn't been initialized. + UninitializedAccount, + /// The instruction expected additional account keys + NotEnoughAccountKeys, + /// Failed to borrow a reference to an account, already borrowed + AccountBorrowFailed, +} + +/// 32bit representations of builtin program errors returned by the entry point +const BUILTIN_ERROR_START: u32 = 0x8000_0000; // 31st bit set +const INVALID_ARGUMENT: u32 = BUILTIN_ERROR_START; +const INVALID_INSTRUCTION_DATA: u32 = BUILTIN_ERROR_START + 1; +const INVALID_ACCOUNT_DATA: u32 = BUILTIN_ERROR_START + 2; +const ACCOUNT_DATA_TOO_SMALL: u32 = BUILTIN_ERROR_START + 3; +const INSUFFICIENT_FUNDS: u32 = BUILTIN_ERROR_START + 4; +const INCORRECT_PROGRAM_ID: u32 = BUILTIN_ERROR_START + 5; +const MISSING_REQUIRED_SIGNATURES: u32 = BUILTIN_ERROR_START + 6; +const ACCOUNT_ALREADY_INITIALIZED: u32 = BUILTIN_ERROR_START + 7; +const UNINITIALIZED_ACCOUNT: u32 = BUILTIN_ERROR_START + 8; +const NOT_ENOUGH_ACCOUNT_KEYS: u32 = BUILTIN_ERROR_START + 9; +const ACCOUNT_BORROW_FAILED: u32 = BUILTIN_ERROR_START + 10; + +/// Is this a builtin error? (is 31th bit set?) +fn is_builtin(error: u32) -> bool { + (error & BUILTIN_ERROR_START) != 0 +} + +/// If a program defined error conflicts with a builtin error +/// its 30th bit is set before returning to distinguish it. +/// The side effect is that the original error's 30th bit +/// value is lost, be aware. +const CONFLICTING_ERROR_MARK: u32 = 0x4000_0000; // 30st bit set + +/// Is this error marked as conflicting? (is 30th bit set?) +fn is_marked_conflicting(error: u32) -> bool { + (error & CONFLICTING_ERROR_MARK) != 0 +} + +/// Mark as a conflicting error +fn mark_conflicting(error: u32) -> u32 { + error | CONFLICTING_ERROR_MARK +} + +/// Unmark as a conflicting error +fn unmark_conflicting(error: u32) -> u32 { + error & !CONFLICTING_ERROR_MARK +} + +impl From for u32 { + fn from(error: ProgramError) -> Self { + match error { + ProgramError::InvalidArgument => INVALID_ARGUMENT, + ProgramError::InvalidInstructionData => INVALID_INSTRUCTION_DATA, + ProgramError::InvalidAccountData => INVALID_ACCOUNT_DATA, + ProgramError::AccountDataTooSmall => ACCOUNT_DATA_TOO_SMALL, + ProgramError::InsufficientFunds => INSUFFICIENT_FUNDS, + ProgramError::IncorrectProgramId => INCORRECT_PROGRAM_ID, + ProgramError::MissingRequiredSignature => MISSING_REQUIRED_SIGNATURES, + ProgramError::AccountAlreadyInitialized => ACCOUNT_ALREADY_INITIALIZED, + ProgramError::UninitializedAccount => UNINITIALIZED_ACCOUNT, + ProgramError::NotEnoughAccountKeys => NOT_ENOUGH_ACCOUNT_KEYS, + ProgramError::AccountBorrowFailed => ACCOUNT_BORROW_FAILED, + ProgramError::CustomError(error) => { + if error == 0 || is_builtin(error) { + mark_conflicting(error) + } else { + error + } + } + } + } +} + +impl From for InstructionError +where + T: ToPrimitive, +{ + fn from(error: T) -> Self { + let error = error.to_u32().unwrap_or(0xbad_c0de); + match error { + INVALID_ARGUMENT => InstructionError::InvalidArgument, + INVALID_INSTRUCTION_DATA => InstructionError::InvalidInstructionData, + INVALID_ACCOUNT_DATA => InstructionError::InvalidAccountData, + ACCOUNT_DATA_TOO_SMALL => InstructionError::AccountDataTooSmall, + INSUFFICIENT_FUNDS => InstructionError::InsufficientFunds, + INCORRECT_PROGRAM_ID => InstructionError::IncorrectProgramId, + MISSING_REQUIRED_SIGNATURES => InstructionError::MissingRequiredSignature, + ACCOUNT_ALREADY_INITIALIZED => InstructionError::AccountAlreadyInitialized, + UNINITIALIZED_ACCOUNT => InstructionError::UninitializedAccount, + NOT_ENOUGH_ACCOUNT_KEYS => InstructionError::NotEnoughAccountKeys, + ACCOUNT_BORROW_FAILED => InstructionError::AccountBorrowFailed, + _ => { + if is_marked_conflicting(error) { + InstructionError::ConflictingError(unmark_conflicting(error)) + } else { + InstructionError::CustomError(error) + } + } + } + } +}