Skip to content

Commit eb5fea3

Browse files
authored
Support registering custom syscalls (#126)
1 parent 9e965de commit eb5fea3

File tree

7 files changed

+175
-26
lines changed

7 files changed

+175
-26
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ audit:
2929
# Build test programs
3030
build-test-programs:
3131
@cargo build-sbf --manifest-path test-programs/cpi-target/Cargo.toml
32+
@cargo build-sbf --manifest-path test-programs/custom-syscall/Cargo.toml
3233
@cargo build-sbf --manifest-path test-programs/primary/Cargo.toml
3334

3435
# Pre-publish checks
@@ -84,6 +85,7 @@ check-features:
8485

8586
# Run tests
8687
test:
88+
@$(MAKE) build-test-programs
8789
@cargo test --all-features
8890

8991
# Run all checks in sequence

harness/src/lib.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ impl Default for Mollusk {
503503
solana_runtime::message_processor=debug,\
504504
solana_runtime::system_instruction_processor=trace",
505505
);
506+
let compute_budget = ComputeBudget::default();
506507
#[cfg(feature = "fuzz")]
507508
let feature_set = {
508509
// Omit "test features" (they have the same u64 ID).
@@ -515,12 +516,13 @@ impl Default for Mollusk {
515516
};
516517
#[cfg(not(feature = "fuzz"))]
517518
let feature_set = FeatureSet::all_enabled();
519+
let program_cache = ProgramCache::new(&feature_set, &compute_budget);
518520
Self {
519521
config: Config::default(),
520-
compute_budget: ComputeBudget::default(),
522+
compute_budget,
521523
feature_set,
522524
logger: None,
523-
program_cache: ProgramCache::default(),
525+
program_cache,
524526
sysvars: Sysvars::default(),
525527
#[cfg(feature = "fuzz-fd")]
526528
slot: 0,
@@ -573,13 +575,7 @@ impl Mollusk {
573575
elf: &[u8],
574576
loader_key: &Pubkey,
575577
) {
576-
self.program_cache.add_program(
577-
program_id,
578-
loader_key,
579-
elf,
580-
&self.compute_budget,
581-
&self.feature_set,
582-
);
578+
self.program_cache.add_program(program_id, loader_key, elf);
583579
}
584580

585581
/// Warp the test environment to a slot by updating sysvars.

harness/src/program.rs

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ use {
88
solana_loader_v3_interface::state::UpgradeableLoaderState,
99
solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status},
1010
solana_program_runtime::{
11-
invoke_context::BuiltinFunctionWithContext,
11+
invoke_context::{BuiltinFunctionWithContext, InvokeContext},
1212
loaded_programs::{LoadProgramMetrics, ProgramCacheEntry, ProgramCacheForTxBatch},
13+
solana_sbpf::{
14+
elf::ElfError,
15+
program::{BuiltinFunction, BuiltinProgram},
16+
},
1317
},
1418
solana_pubkey::Pubkey,
1519
solana_rent::Rent,
@@ -57,13 +61,23 @@ pub struct ProgramCache {
5761
//
5862
// K: program ID, V: loader key
5963
entries_cache: Rc<RefCell<HashMap<Pubkey, Pubkey>>>,
64+
// The function registry (syscalls) to use for verifying and loading
65+
// program ELFs.
66+
program_runtime_environment: BuiltinProgram<InvokeContext<'static>>,
6067
}
6168

62-
impl Default for ProgramCache {
63-
fn default() -> Self {
69+
impl ProgramCache {
70+
pub fn new(feature_set: &FeatureSet, compute_budget: &ComputeBudget) -> Self {
6471
let me = Self {
6572
cache: Rc::new(RefCell::new(ProgramCacheForTxBatch::default())),
6673
entries_cache: Rc::new(RefCell::new(HashMap::new())),
74+
program_runtime_environment: create_program_runtime_environment_v1(
75+
feature_set,
76+
compute_budget,
77+
/* reject_deployment_of_broken_elfs */ false,
78+
/* debugging_features */ false,
79+
)
80+
.unwrap(),
6781
};
6882
BUILTINS.iter().for_each(|builtin| {
6983
let program_id = builtin.program_id;
@@ -72,9 +86,7 @@ impl Default for ProgramCache {
7286
});
7387
me
7488
}
75-
}
7689

77-
impl ProgramCache {
7890
pub(crate) fn cache(&self) -> RefMut<ProgramCacheForTxBatch> {
7991
self.cache.borrow_mut()
8092
}
@@ -94,18 +106,24 @@ impl ProgramCache {
94106
}
95107

96108
/// Add a program to the cache.
97-
pub fn add_program(
98-
&mut self,
99-
program_id: &Pubkey,
100-
loader_key: &Pubkey,
101-
elf: &[u8],
102-
compute_budget: &ComputeBudget,
103-
feature_set: &FeatureSet,
104-
) {
105-
let environment = Arc::new(
106-
create_program_runtime_environment_v1(feature_set, compute_budget, false, false)
107-
.unwrap(),
108-
);
109+
pub fn add_program(&mut self, program_id: &Pubkey, loader_key: &Pubkey, elf: &[u8]) {
110+
// This might look rough, but it's actually functionally the same as
111+
// calling `create_program_runtime_environment_v1` on every addition.
112+
let environment = {
113+
let config = self.program_runtime_environment.get_config().clone();
114+
let mut loader = BuiltinProgram::new_loader(config);
115+
116+
for (_key, (name, value)) in self
117+
.program_runtime_environment
118+
.get_function_registry()
119+
.iter()
120+
{
121+
let name = std::str::from_utf8(name).unwrap();
122+
loader.register_function(name, value).unwrap();
123+
}
124+
125+
Arc::new(loader)
126+
};
109127
self.replenish(
110128
*program_id,
111129
Arc::new(
@@ -149,6 +167,19 @@ impl ProgramCache {
149167
})
150168
.collect()
151169
}
170+
171+
/// Register a new function (syscall) with the program runtime environment.
172+
///
173+
/// **Important**: You should register all custom syscalls BEFORE adding any
174+
/// programs to the store that will use them.
175+
pub fn register_function(
176+
&mut self,
177+
name: &str,
178+
value: BuiltinFunction<InvokeContext<'static>>,
179+
) -> Result<(), ElfError> {
180+
self.program_runtime_environment
181+
.register_function(name, value)
182+
}
152183
}
153184

154185
pub struct Builtin {

harness/tests/custom_syscall.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use {
2+
mollusk_svm::{result::Check, Mollusk},
3+
solana_instruction::Instruction,
4+
solana_program_runtime::{
5+
invoke_context::InvokeContext,
6+
solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping},
7+
},
8+
solana_pubkey::Pubkey,
9+
};
10+
11+
declare_builtin_function!(
12+
/// A custom syscall to burn CUs.
13+
SyscallBurnCus,
14+
fn rust(
15+
invoke_context: &mut InvokeContext,
16+
to_burn: u64,
17+
_arg2: u64,
18+
_arg3: u64,
19+
_arg4: u64,
20+
_arg5: u64,
21+
_memory_mapping: &mut MemoryMapping,
22+
) -> Result<u64, Box<dyn std::error::Error>> {
23+
invoke_context.consume_checked(to_burn)?;
24+
Ok(0)
25+
}
26+
);
27+
28+
fn instruction_burn_cus(program_id: &Pubkey, to_burn: u64) -> Instruction {
29+
Instruction::new_with_bytes(*program_id, &to_burn.to_le_bytes(), vec![])
30+
}
31+
32+
#[test]
33+
fn test_custom_syscall() {
34+
std::env::set_var("SBF_OUT_DIR", "../target/deploy");
35+
36+
let program_id = Pubkey::new_unique();
37+
38+
let mollusk = {
39+
let mut mollusk = Mollusk::default();
40+
mollusk
41+
.program_cache
42+
.register_function("sol_burn_cus", SyscallBurnCus::vm)
43+
.unwrap();
44+
mollusk.add_program(
45+
&program_id,
46+
"custom_syscall_program",
47+
&mollusk_svm::program::loader_keys::LOADER_V3,
48+
);
49+
mollusk
50+
};
51+
52+
let base_cus = mollusk
53+
.process_and_validate_instruction(
54+
&instruction_burn_cus(&program_id, 0),
55+
&[],
56+
&[Check::success()],
57+
)
58+
.compute_units_consumed;
59+
60+
for to_burn in [100, 1_000, 10_000] {
61+
mollusk.process_and_validate_instruction(
62+
&instruction_burn_cus(&program_id, to_burn),
63+
&[],
64+
&[Check::success(), Check::compute_units(base_cus + to_burn)],
65+
);
66+
}
67+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "custom-syscall-program"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
solana-account-info = { workspace = true }
8+
solana-program-entrypoint = { workspace = true }
9+
solana-program-error = { workspace = true }
10+
solana-pubkey = { workspace = true }
11+
12+
[lib]
13+
crate-type = ["cdylib", "lib"]
14+
15+
[lints]
16+
workspace = true
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use {solana_account_info::AccountInfo, solana_program_error::ProgramError, solana_pubkey::Pubkey};
2+
3+
// Declare the custom syscall that we expect to be registered.
4+
// This matches the `sol_burn_cus` syscall from the test.
5+
extern "C" {
6+
fn sol_burn_cus(to_burn: u64) -> u64;
7+
}
8+
9+
solana_program_entrypoint::entrypoint!(process_instruction);
10+
11+
fn process_instruction(
12+
_program_id: &Pubkey,
13+
_accounts: &[AccountInfo],
14+
input: &[u8],
15+
) -> Result<(), ProgramError> {
16+
let to_burn = input
17+
.get(0..8)
18+
.and_then(|bytes| bytes.try_into().map(u64::from_le_bytes).ok())
19+
.ok_or(ProgramError::InvalidInstructionData)?;
20+
21+
// Call the custom syscall to burn CUs.
22+
unsafe {
23+
sol_burn_cus(to_burn);
24+
}
25+
26+
Ok(())
27+
}

0 commit comments

Comments
 (0)