Skip to content

Commit 1261e4b

Browse files
authored
Merge pull request #148 from buffalojoec/core-bpf-support
Support conformance testing between BPF and builtins
2 parents 0b471fe + eb8049a commit 1261e4b

File tree

4 files changed

+153
-6
lines changed

4 files changed

+153
-6
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,8 @@ solana-program = { git = "https://github.com/firedancer-io/agave", rev = "b641c6
5050
solana-zk-token-sdk = { git = "https://github.com/firedancer-io/agave", rev = "b641c6e050d759e41dd7d794c3d0a12638c11800" }
5151

5252
[features]
53+
# This feature is used to compile a target with a builtin replaced by a BPF program.
54+
# Requires the `CORE_BPF_PROGRAM_ID` and `CORE_BPF_TARGET` environment variables.
55+
core-bpf = []
5356
# This feature is used to stub out certain parts of the agave runtime for fuzzing
5457
stub-agave = ["solana-program-runtime/stub-proc-instr"]

macro/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,35 @@ pub fn load_core_bpf_program(_: TokenStream) -> TokenStream {
8686

8787
quote!().into()
8888
}
89+
90+
/// Initializes a constant - `CORE_BPF_DEFAULT_COMPUTE_UNITS` - for the target
91+
/// to use when compiled with the `core-bpf` feature enabled.
92+
///
93+
/// This constant allows the harness to handle compute units compared to the
94+
/// builtin version of a program, since BPF compute units will rarely match the
95+
/// `DEFAULT_COMPUTE_UNITS` value declared by a builtin.
96+
#[proc_macro]
97+
pub fn declare_core_bpf_default_compute_units(_: TokenStream) -> TokenStream {
98+
let mut tokens = quote!();
99+
100+
if let Ok(program_id_str) = std::env::var("CORE_BPF_PROGRAM_ID") {
101+
let program_id = Pubkey::from_str(&program_id_str).expect("Invalid address");
102+
if !SUPPORTED_BUILTINS.contains(&program_id) {
103+
panic!("Unsupported program id: {}", program_id);
104+
}
105+
106+
if program_id == solana_sdk::address_lookup_table::program::id() {
107+
tokens = quote! {
108+
#[cfg(feature = "core-bpf")]
109+
const CORE_BPF_DEFAULT_COMPUTE_UNITS: u64 = solana_address_lookup_table_program::processor::DEFAULT_COMPUTE_UNITS;
110+
}
111+
} else if program_id == solana_sdk::config::program::id() {
112+
tokens = quote! {
113+
#[cfg(feature = "core-bpf")]
114+
const CORE_BPF_DEFAULT_COMPUTE_UNITS: u64 = solana_config_program::config_processor::DEFAULT_COMPUTE_UNITS;
115+
}
116+
}
117+
}
118+
119+
tokens.into()
120+
}

scripts/build_core_bpf.sh

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ set_core_bpf_vars "$1"
2929

3030
CORE_BPF_PROGRAM_ID=$CORE_BPF_PROGRAM_ID CORE_BPF_TARGET=$CORE_BPF_TARGET FORCE_RECOMPILE=true $CARGO build \
3131
--target x86_64-unknown-linux-gnu \
32+
--features core-bpf \
3233
--lib \
3334
--release

src/lib.rs

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ use solana_timings::ExecuteTimings;
4242
use crate::utils::err_map::instr_err_to_num;
4343
use crate::utils::feature_u64;
4444
use solana_svm::transaction_processing_callback::TransactionProcessingCallback;
45-
use solfuzz_agave_macro::load_core_bpf_program;
45+
use solfuzz_agave_macro::{declare_core_bpf_default_compute_units, load_core_bpf_program};
4646
use std::collections::HashSet;
4747
use std::env;
4848
use std::ffi::c_int;
4949
use std::sync::Arc;
5050
use thiserror::Error;
5151

52+
#[cfg(feature = "core-bpf")]
53+
use solana_sdk::account::WritableAccount;
54+
5255
// macro to rewrite &[IDENTIFIER, ...] to &[feature_u64(IDENTIFIER::id()), ...]
5356
#[macro_export]
5457
macro_rules! feature_list {
@@ -253,6 +256,13 @@ static SUPPORTED_FEATURES: &[u64] = feature_list![
253256
move_stake_and_move_lamports_ixs,
254257
];
255258

259+
// If the `CORE_BPF_PROGRAM_ID` variable is set, declares the default compute
260+
// units used by the program's builtin version.
261+
//
262+
// This constant is used to stub-out compute unit conformance checks, since the
263+
// BPF version will use different amounts of CUs.
264+
declare_core_bpf_default_compute_units!();
265+
256266
pub mod proto {
257267
include!(concat!(env!("OUT_DIR"), "/org.solana.sealevel.v1.rs"));
258268
}
@@ -562,7 +572,23 @@ fn load_builtins(cache: &mut ProgramCacheForTxBatch) -> HashSet<Pubkey> {
562572
}
563573

564574
fn execute_instr(mut input: InstrContext) -> Option<InstrEffects> {
565-
// TODO this shouldn't be default
575+
#[cfg(feature = "core-bpf")]
576+
// If the fixture declares `cu_avail` to be less than the builtin version's
577+
// `DEFAULT_COMPUTE_UNITS`, the program should fail on compute meter
578+
// exhaustion.
579+
//
580+
// If the builtin version would otherwise _not_ exhuast the CU meter, give
581+
// the BPF version the default budget for BPF programs (200k), to avoid any
582+
// mismatches from the BPF program exhuasting the meter when the builtin
583+
// did not.
584+
let compute_budget = {
585+
let mut budget = ComputeBudget::default();
586+
if input.cu_avail <= CORE_BPF_DEFAULT_COMPUTE_UNITS {
587+
budget.compute_unit_limit = 0; // Ensures CU meter exhaustion.
588+
}
589+
budget
590+
};
591+
#[cfg(not(feature = "core-bpf"))]
566592
let compute_budget = ComputeBudget {
567593
compute_unit_limit: input.cu_avail,
568594
..ComputeBudget::default()
@@ -623,7 +649,25 @@ fn execute_instr(mut input: InstrContext) -> Option<InstrEffects> {
623649
input
624650
.accounts
625651
.iter()
626-
.map(|(pubkey, account)| (*pubkey, AccountSharedData::from(account.clone())))
652+
.map(|(pubkey, account)| {
653+
#[cfg(feature = "core-bpf")]
654+
// Fixtures provide the program account as a builtin (owned by
655+
// native loader), but the program-runtime will expect the account
656+
// owner to match the cache entry.
657+
//
658+
// Since we loaded the provided ELF into the cache under loader v3,
659+
// stub out the program account here.
660+
//
661+
// Note: Agave does this during transaction account loading.
662+
// https://github.com/anza-xyz/agave/blob/6d74d13749829d463fabccebd8203edf0cf4c500/svm/src/account_loader.rs#L246-L249
663+
if *pubkey == input.instruction.program_id {
664+
let mut stubbed_out_program_account = AccountSharedData::default();
665+
stubbed_out_program_account.set_owner(solana_sdk::bpf_loader_upgradeable::id());
666+
stubbed_out_program_account.set_executable(true);
667+
return (*pubkey, stubbed_out_program_account);
668+
}
669+
(*pubkey, AccountSharedData::from(account.clone()))
670+
})
627671
.for_each(|x| transaction_accounts.push(x));
628672

629673
let program_idx = transaction_accounts
@@ -683,6 +727,15 @@ fn execute_instr(mut input: InstrContext) -> Option<InstrEffects> {
683727
let mut newly_loaded_programs = HashSet::<Pubkey>::new();
684728

685729
for acc in &input.accounts {
730+
#[cfg(feature = "core-bpf")]
731+
// The Core BPF program's ELF has already been added to the cache.
732+
// Its transaction account was stubbed out, so it can't be loaded via
733+
// callback (inputs), since the account doesn't contain the ELF.
734+
// Skip it here.
735+
if acc.0 == input.instruction.program_id {
736+
continue;
737+
}
738+
686739
// FD rejects duplicate account loads
687740
if !newly_loaded_programs.insert(acc.0) {
688741
return None;
@@ -806,6 +859,16 @@ fn execute_instr(mut input: InstrContext) -> Option<InstrEffects> {
806859
&mut timings,
807860
);
808861

862+
#[cfg(feature = "core-bpf")]
863+
// To keep alignment with a builtin run, deduct only the CUs the builtin
864+
// version would have consumed, so the fixture realizes the same CU
865+
// deduction across both BPF and builtin in its effects.
866+
let cu_avail = input
867+
.cu_avail
868+
.saturating_sub(CORE_BPF_DEFAULT_COMPUTE_UNITS);
869+
#[cfg(not(feature = "core-bpf"))]
870+
let cu_avail = input.cu_avail - compute_units_consumed;
871+
809872
let return_data = transaction_context.get_return_data().1.to_vec();
810873

811874
Some(InstrEffects {
@@ -814,15 +877,63 @@ fn execute_instr(mut input: InstrContext) -> Option<InstrEffects> {
814877
} else {
815878
None
816879
},
817-
result: result.err(),
880+
#[allow(clippy::map_identity)]
881+
result: result.err().map(|err| {
882+
#[cfg(feature = "core-bpf")]
883+
// Some errors don't directly map between builtins and their BPF
884+
// versions.
885+
//
886+
// For example, when a builtin program exceeds the compute budget,
887+
// the builtin's `DEFAULT_COMPUTE_UNITS` are deducted from the
888+
// meter, and if the meter is exhuasted, the invoke context will
889+
// throw `InstructionError::ComputationalBudgetExceeded`.
890+
// https://github.com/anza-xyz/agave/blob/6d74d13749829d463fabccebd8203edf0cf4c500/program-runtime/src/invoke_context.rs#L73
891+
// https://github.com/anza-xyz/agave/blob/6d74d13749829d463fabccebd8203edf0cf4c500/program-runtime/src/invoke_context.rs#L574
892+
//
893+
// However, for a BPF program, if the compute meter is exhausted,
894+
// the error comes from the VM, and is converted to
895+
// `InstructionError::ProgramFailedToComplete`.
896+
// https://github.com/solana-labs/rbpf/blob/69a52ec6a341bb7374d387173b5e6dc56218fe0c/src/error.rs#L44
897+
// https://github.com/anza-xyz/agave/blob/6d74d13749829d463fabccebd8203edf0cf4c500/program-runtime/src/invoke_context.rs#L547
898+
//
899+
// Therefore, some errors require reconciliation when testing a BPF
900+
// program against its builtin implementation.
901+
if err == InstructionError::ProgramFailedToComplete
902+
&& (input.cu_avail <= CORE_BPF_DEFAULT_COMPUTE_UNITS
903+
|| compute_units_consumed >= input.cu_avail)
904+
{
905+
return InstructionError::ComputationalBudgetExceeded;
906+
}
907+
err
908+
}),
818909
modified_accounts: transaction_context
819910
.deconstruct_without_keys()
820911
.unwrap()
821912
.into_iter()
822913
.enumerate()
823-
.map(|(index, data)| (transaction_accounts[index].0, data.into()))
914+
.map(|(index, data)| {
915+
#[cfg(feature = "core-bpf")]
916+
// Fixtures provide the program account as a builtin account
917+
// (owned by native loader).
918+
//
919+
// When we built out the transaction accounts, we stubbed out
920+
// the program account to be owned by loader v3.
921+
//
922+
// We need to swap back in the original here to avoid a
923+
// mismatch.
924+
if index == program_idx {
925+
if let Some(program_account) = input
926+
.accounts
927+
.iter()
928+
.find(|(pubkey, _)| *pubkey == input.instruction.program_id)
929+
{
930+
return (program_account.0, program_account.1.clone());
931+
}
932+
}
933+
(transaction_accounts[index].0, data.into())
934+
})
824935
.collect(),
825-
cu_avail: input.cu_avail - compute_units_consumed,
936+
cu_avail,
826937
return_data,
827938
})
828939
}

0 commit comments

Comments
 (0)