bokken_runtime/
executor.rs

1use std::{mem::size_of, collections::HashMap, sync::Arc, thread};
2
3
4use bytemuck::{Zeroable, Pod};
5use solana_program::{
6	entrypoint::MAX_PERMITTED_DATA_INCREASE,
7	pubkey::Pubkey,
8	program_error::ProgramError, instruction::AccountMeta
9};
10use tokio::{sync::{Mutex, RwLock, mpsc}};
11
12use crate::{debug_env::{BokkenAccountData, BokkenRuntimeMessage}, ipc_comm::IPCComm, sol_syscalls::BokkenSyscallMsg};
13
14/// Raw header data for the `SolanaAccountsBlob`
15#[derive(PartialEq, Eq, Debug, Clone, Copy, Zeroable, Pod)]
16#[repr(C)]
17pub(crate) struct AccountInfoHeader {
18	_0xff: u8,
19	is_signer: u8, // bool
20	is_writable: u8, // bool
21	executable: u8, // bool
22	pub original_data_len: u32,
23	pub pubkey: Pubkey,
24	pub owner: Pubkey,
25	pub lamports: u64,
26	pub data_len: u64
27}
28impl AccountInfoHeader {
29	pub fn is_signer(&self) -> bool {
30		self.is_signer > 0
31	}
32	pub fn is_writable(&self) -> bool {
33		self.is_writable > 0
34	}
35	pub fn executable(&self) -> bool {
36		self.executable > 0
37	}
38}
39
40/// An instance of multiple Solana `AccountInfo`s, structured in a manner which the `solana_program`'s entrypoint
41/// parser expects.
42#[derive(Debug)]
43pub(crate) struct SolanaAccountsBlob {
44	pub account_offsets: HashMap<Pubkey, usize>,
45	pub bytes: Vec<u8>
46}
47impl SolanaAccountsBlob {
48	/// Creates a new instance of solana account data with the information provided
49	pub fn new(
50		program_id: Pubkey,
51		instruction: Vec<u8>,
52		account_metas: Vec<AccountMeta>,
53		mut account_datas: HashMap<Pubkey, BokkenAccountData>
54	) -> Self {
55		let mut blob: Vec<u8> = Vec::with_capacity(
56			account_metas.len() * 20480 + // this value is arbitrary
57			size_of::<u64>() + 
58			instruction.len() +
59			size_of::<Pubkey>()
60		);
61		blob.extend((account_metas.len() as u64).to_le_bytes());
62
63		let mut account_indices: HashMap<Pubkey, usize> = HashMap::new();
64		let mut account_offsets: HashMap<Pubkey, usize> = HashMap::new();
65		for (index, account_meta) in account_metas.iter().enumerate() {
66			if let Some(entry_index) = account_indices.get(&account_meta.pubkey) {
67				blob.extend((*entry_index as u64).to_le_bytes());
68			}else{
69				let account_data = account_datas.remove(&account_meta.pubkey)
70					.expect("The account metas should reference accounts in the account datas");
71				account_indices.insert(account_meta.pubkey, index);
72				account_offsets.insert(account_meta.pubkey, blob.len());
73
74				blob.push(u8::MAX);
75				blob.push(account_meta.is_signer as u8);
76				blob.push(account_meta.is_writable as u8);
77				blob.push(account_data.executable as u8);
78				blob.extend((account_data.data.len() as u32).to_le_bytes()); // "Original data length" (immediatly overwritten?)
79				blob.extend(account_meta.pubkey.as_ref());
80				blob.extend(account_data.owner.as_ref());
81				blob.extend((account_data.lamports).to_le_bytes());
82				blob.extend((account_data.data.len() as u64).to_le_bytes());
83				blob.extend(account_data.data);
84				blob.extend(vec![0; MAX_PERMITTED_DATA_INCREASE]);
85				blob.extend(vec![0; blob.len() % 8]);
86				blob.extend(account_data.rent_epoch.to_le_bytes());		
87			}
88		}
89		blob.extend((instruction.len() as u64).to_le_bytes());
90		blob.extend(instruction);
91		blob.extend(program_id.as_ref());
92		Self {
93			bytes: blob,
94			account_offsets
95		}
96	}
97
98	/// Returns a copy of the account info associated with the specified pubkey
99	/// 
100	/// Returns None if the account doesn't exist in this context.
101	pub fn get_account_data(&self, pubkey: &Pubkey) -> Option<BokkenAccountData> {
102		if let Some(account_offset) = self.account_offsets.get(pubkey) {
103			let account_data_offset = *account_offset + std::mem::size_of::<AccountInfoHeader>();
104			let account_header = bytemuck::from_bytes::<AccountInfoHeader>(
105				&self.bytes[*account_offset..account_data_offset]
106			);
107			let rent_epoch_offset =
108				account_data_offset +
109				account_header.original_data_len as usize +
110				MAX_PERMITTED_DATA_INCREASE +
111				(account_header.original_data_len as usize % 8);
112			
113			Some( BokkenAccountData {
114				lamports: account_header.lamports,
115				data: self.bytes[account_data_offset..{account_data_offset + account_header.data_len as usize}].to_vec(),
116				owner: account_header.owner,
117				executable: account_header.executable > 0,
118				rent_epoch: u64::from_le_bytes(self.bytes[rent_epoch_offset..{rent_epoch_offset + 8}].try_into().unwrap())
119			})
120		}else{
121			None
122		}
123	}
124
125	/// Edits the account data accessible by the solana program with the data provided
126	pub fn set_account_data(&mut self, pubkey: &Pubkey, account_data: BokkenAccountData) -> Result<(), ProgramError> {
127		if let Some(account_offset) = self.account_offsets.get(pubkey) {
128			let account_data_offset = *account_offset + std::mem::size_of::<AccountInfoHeader>();
129			let account_header = bytemuck::from_bytes_mut::<AccountInfoHeader>(
130				&mut self.bytes[*account_offset..account_data_offset]
131			);
132			if account_data.data.len() > account_header.original_data_len as usize + MAX_PERMITTED_DATA_INCREASE {
133				println!("Debug runtime: set_account_data: {} was grown too much", pubkey);
134				return Err(ProgramError::InvalidRealloc);
135			}
136			account_header.data_len = account_data.data.len() as u64;
137			account_header.lamports = account_data.lamports;
138			account_header.owner = account_data.owner;
139			self.bytes[account_data_offset..{account_data_offset + account_data.data.len()}].copy_from_slice(&account_data.data);
140			Ok(())
141		}else{
142			println!(
143				"Debug runtime: set_account_data called with {} but we have no idea what that account is",
144				pubkey
145			);
146			Err(ProgramError::UninitializedAccount)
147		}
148	}
149
150	/// Retrieves a reference to the account data header
151	pub fn get_account_data_header(&self, pubkey: &Pubkey) -> Option<&AccountInfoHeader> {
152		if let Some(account_offset) = self.account_offsets.get(pubkey) {
153			let account_data_offset = *account_offset + std::mem::size_of::<AccountInfoHeader>();
154			let account_header = bytemuck::from_bytes::<AccountInfoHeader>(
155				&self.bytes[*account_offset..account_data_offset]
156			);
157			Some(account_header)
158		}else{
159			None
160		}
161	}
162
163	/// Whether or not the specified account is writable
164	pub fn is_writable(&self, pubkey: &Pubkey) -> bool {
165		if let Some(account_header) = self.get_account_data_header(pubkey) {
166			account_header.is_writable() && !account_header.executable()
167		}else{
168			false
169		}
170	}
171
172	/// Whether or not the specified account is a signer
173	pub fn is_signer(&self, pubkey: &Pubkey) -> bool {
174		if let Some(account_header) = self.get_account_data_header(pubkey) {
175			account_header.is_signer()
176		}else{
177			false
178		}
179	}
180
181	/// Gets all the account information stored in this context
182	pub fn get_account_datas(&self) -> HashMap<Pubkey, BokkenAccountData> {
183		let mut result = HashMap::new();
184		for pubkey in self.account_offsets.keys() {
185			result.insert(
186				*pubkey,
187				self.get_account_data(pubkey).expect("the value of the keys we are iterating over")
188			);
189		}
190		result
191	}
192}
193
194/// Execution context used for `BokkenSyscalls`
195#[derive(Debug)]
196pub(crate) struct BokkenSolanaContext {
197	// executed: bool,
198	pub blob: Arc<RwLock<SolanaAccountsBlob>>,
199	nonce: u64,
200	cpi_height: u8
201}
202impl BokkenSolanaContext {
203	pub fn new(
204		program_id: Pubkey,
205		instruction: Vec<u8>,
206		account_metas: Vec<AccountMeta>,
207		account_datas: HashMap<Pubkey, BokkenAccountData>,
208		nonce: u64,
209		cpi_height: u8,
210	) -> Self {
211		
212		Self {
213			// executed: false,
214			blob: Arc::new(RwLock::new(
215				SolanaAccountsBlob::new(
216					program_id,
217					instruction,
218					account_metas,
219					account_datas
220				)
221			)),
222			nonce,
223			cpi_height
224		}
225	}
226	pub fn get_account_data(&self, pubkey: &Pubkey) -> Option<BokkenAccountData> {
227		self.blob.blocking_read().get_account_data(pubkey)
228	}
229	pub fn is_writable(&self, pubkey: &Pubkey) -> bool {
230		self.blob.blocking_read().is_writable(pubkey)
231	}
232	pub fn is_signer(&self, pubkey: &Pubkey) -> bool {
233		self.blob.blocking_read().is_signer(pubkey)
234	}
235
236	pub fn get_account_datas(&self) -> HashMap<Pubkey, BokkenAccountData> {
237		self.blob.blocking_read().get_account_datas()
238	}
239	pub fn cpi_height(&self) -> u8 {
240		self.cpi_height
241	}
242	pub fn nonce(&self) -> u64 {
243		self.nonce
244	}
245}
246
247/// Spawns a new thread to execute the Solana program in.
248/// 
249/// Does not await until the new thread is finished, await is only used to properly use the RwLock
250/// After the program execution has finished, `comm` is used to notify the main process of the results, and
251/// `context_drop_notifier` is used to notify `BokkenSyscalls` to pop the context.
252pub(crate) async fn execute_sol_program_thread(
253	nonce: u64,
254	blob: Arc<RwLock<SolanaAccountsBlob>>,
255	comm: Arc<Mutex<IPCComm>>,
256	context_drop_notifier: mpsc::Sender<BokkenSyscallMsg>
257) {
258		// This is "unsafe", but we cannot write-lock the blob during the entire SOL program's execution.
259		// This is because we need to update the account data as a result of a CPI. If we locked it here, then we'd
260		// deadlock ourselves as we'd never be able to update the account data.
261		let blob_ptr = {
262			// And so, we're bypassing the RwLock to make that happen.
263			blob.read().await.bytes.as_ptr() as usize
264		};
265		
266		// All Solana syscalls methods, including invoke, log, are all blocking. So we spawn another thread in order
267		// to avoid deadlocking ourselves.
268		thread::spawn(move || {
269			// Solana programs might panic for any reason. So we spawn yet another thread in order to catch any
270			// potential panics.
271			let result = thread::spawn(move || {
272				extern "C" {
273					// The entrypoint macro provided by `solana_program` simply exports a C function called
274					// `entrypoint`. This is how we call upon the provided solana program.
275					fn entrypoint(input: *mut u8) -> u64;
276				}
277				let result = unsafe {
278					entrypoint(blob_ptr as *mut u8)
279				};
280				result
281			}).join();
282			let mut comm = comm.blocking_lock();
283			context_drop_notifier.blocking_send(
284				BokkenSyscallMsg::PopContext
285			).expect("mpsc::Sender to not fail");
286			let account_datas = blob.blocking_read().get_account_datas();
287			match result {
288				Ok(return_code) => {
289					comm.blocking_send_msg(
290						BokkenRuntimeMessage::Executed{
291							nonce,
292							return_code,
293							account_datas
294						}
295					).expect("encoding to not fail");
296				},
297				Err(err) => {
298					let panic_msg = match err.downcast_ref::<&str>() {
299						Some(str) => str.to_string(),
300						None => {
301							match err.downcast_ref::<String>() {
302								Some(str) => str.clone(),
303								None => String::from("<Unknown panic message>")
304							}
305						},
306					};
307					comm.blocking_send_msg(
308						BokkenRuntimeMessage::Log{
309							nonce,
310							message: format!("Program panicked: {}", panic_msg)
311						}
312					).expect("encoding to not fail");
313					comm.blocking_send_msg(
314						// TODO: Treat panics differently
315						BokkenRuntimeMessage::Executed{
316							nonce,
317							return_code: ProgramError::Custom(0).into(),
318							account_datas
319						}
320					).expect("encoding to not fail");
321				},
322			}
323		});
324}