@@ -42,13 +42,16 @@ use solana_timings::ExecuteTimings;
4242use crate :: utils:: err_map:: instr_err_to_num;
4343use crate :: utils:: feature_u64;
4444use 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} ;
4646use std:: collections:: HashSet ;
4747use std:: env;
4848use std:: ffi:: c_int;
4949use std:: sync:: Arc ;
5050use 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]
5457macro_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+
256266pub 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
564574fn 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