1use {
2 crate::{
3 handshake::{ClientSession, ClientWorkerSession},
4 pubkeys_ptr::PubkeysPtr,
5 transaction_ptr::{TransactionPtr, TransactionPtrBatch},
6 },
7 agave_feature_set::FeatureSet,
8 agave_scheduler_bindings::{
9 MAX_TRANSACTIONS_PER_MESSAGE, PackToWorkerMessage, ProgressMessage,
10 SharableTransactionBatchRegion, SharableTransactionRegion, TpuToPackMessage,
11 WorkerToPackMessage, processed_codes, tpu_message_flags,
12 worker_message_types::{self, CheckResponse, ExecutionResponse},
13 },
14 agave_transaction_view::{
15 result::TransactionViewError, transaction_view::SanitizedTransactionView,
16 },
17 rts_alloc::Allocator,
18 slotmap::SlotMap,
19 solana_fee::FeeFeatures,
20 solana_pubkey::Pubkey,
21 solana_runtime_transaction::sanitize_config::sanitize_config,
22 std::ptr::NonNull,
23 thiserror::Error,
24};
25
26pub struct SchedulerBindingsBridge<M> {
27 allocator: Allocator,
28 tpu_to_pack: shaq::spsc::Consumer<TpuToPackMessage>,
29 progress_tracker: shaq::spsc::Consumer<ProgressMessage>,
30 workers: Vec<SchedulerWorker>,
31
32 progress: ProgressMessage,
33 runtime: RuntimeState,
34 state: SlotMap<TransactionKey, TransactionState>,
35
36 _marker: core::marker::PhantomData<M>,
37}
38
39type Batch<'a, M> = TransactionPtrBatch<'a, KeyedTransactionMeta<M>>;
40
41impl<M> SchedulerBindingsBridge<M>
42where
43 M: Copy,
44{
45 const TRANSACTION_BATCH_META_OFFSET: usize = Batch::<M>::TRANSACTION_META_START;
46 const TRANSACTION_BATCH_SIZE: usize = Batch::<M>::TRANSACTION_META_END;
47
48 #[must_use]
61 pub fn new(
62 ClientSession {
63 mut allocators,
64 tpu_to_pack,
65 progress_tracker,
66 workers,
67 }: ClientSession,
68 ) -> Self {
69 assert_eq!(allocators.len(), 1, "invalid number of allocators");
70
71 Self {
72 allocator: allocators.remove(0),
73 tpu_to_pack,
74 progress_tracker,
75 workers: workers.into_iter().map(SchedulerWorker).collect(),
76
77 progress: ProgressMessage {
78 leader_state: 0,
79 current_slot_progress: 0,
80 epoch: 0,
81 current_slot: 0,
82 next_leader_slot: u64::MAX,
83 leader_range_end: u64::MAX,
84 remaining_cost_units: 0,
85 latest_blockhash: [0; 32],
86 },
87 runtime: RuntimeState {
88 feature_set: FeatureSet::all_enabled(),
89 fee_features: FeeFeatures {},
90 lamports_per_signature: 5000,
91 burn_percent: 50,
92 },
93 state: SlotMap::default(),
94
95 _marker: core::marker::PhantomData,
96 }
97 }
98
99 #[cfg(feature = "dev-context-only-utils")]
100 pub fn allocator(&self) -> &Allocator {
101 &self.allocator
102 }
103
104 pub fn state(&self) -> &SlotMap<TransactionKey, TransactionState> {
105 &self.state
106 }
107
108 pub fn runtime(&self) -> &RuntimeState {
109 &self.runtime
110 }
111
112 pub fn progress(&self) -> &ProgressMessage {
113 &self.progress
114 }
115
116 pub fn worker_count(&self) -> usize {
117 self.workers.len()
118 }
119
120 pub fn worker(&mut self, id: usize) -> &mut SchedulerWorker {
126 &mut self.workers[id]
127 }
128
129 pub fn transaction(&self, key: TransactionKey) -> &TransactionState {
135 let tx = &self.state[key];
136 assert!(!tx.dead);
137
138 tx
139 }
140
141 pub fn insert_transaction(
147 &mut self,
148 tx: &[u8],
149 ) -> Result<TransactionKey, TransactionInsertError> {
150 assert!(tx.len() <= 4096);
152
153 let ptr = self
154 .allocator
155 .allocate(tx.len().try_into().expect("4096 fits in u32"))
156 .ok_or(TransactionInsertError::Allocate)?;
157 unsafe {
161 std::ptr::copy_nonoverlapping(tx.as_ptr(), ptr.as_ptr(), tx.len());
162 }
163 let tx = unsafe { TransactionPtr::from_raw_parts(ptr, tx.len()) };
166
167 match SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true)) {
169 Ok(tx) => {
170 let key = self.state.insert(TransactionState {
171 dead: false,
172 borrows: 0,
173 flags: 0,
174 data: tx,
175 keys: None,
176 });
177
178 Ok(key)
179 }
180 Err(err) => {
181 unsafe {
185 self.allocator.free(ptr);
186 }
187
188 Err(TransactionInsertError::ParseSanitize(err))
189 }
190 }
191 }
192
193 pub fn drop_transaction(&mut self, key: TransactionKey) {
194 match self.state[key].borrows {
197 0 => {
198 let state = self.state.remove(key).unwrap();
199
200 if let Some(keys) = state.keys {
201 unsafe {
204 keys.free(&self.allocator);
205 }
206 }
207
208 unsafe {
211 state.data.into_inner_data().free(&self.allocator);
212 }
213 }
214 _ => self.state[key].dead = true,
215 }
216 }
217
218 pub fn drain_progress(&mut self) -> Option<ProgressMessage> {
219 self.progress_tracker.sync();
220
221 let mut received = false;
222 while let Some(msg) = self.progress_tracker.try_read() {
223 self.progress = *msg;
224 received = true;
225 }
226 self.progress_tracker.finalize();
227
228 received.then_some(self.progress)
229 }
230
231 pub fn tpu_len(&mut self) -> usize {
232 self.tpu_to_pack.sync();
233
234 self.tpu_to_pack.len()
235 }
236
237 pub fn drain_tpu(
238 &mut self,
239 mut callback: impl FnMut(&mut Self, TransactionKey) -> TxDecision,
240 max_count: usize,
241 ) -> usize {
242 self.tpu_to_pack.sync();
243
244 let additional = std::cmp::min(self.tpu_to_pack.len(), max_count);
245 let mut sanitize_failures = 0usize;
246 for _ in 0..additional {
247 let msg = self.tpu_to_pack.try_read().expect("len checked above");
248
249 let tx = unsafe {
254 TransactionPtr::from_sharable_transaction_region(&msg.transaction, &self.allocator)
255 };
256
257 let Ok(tx) = SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true))
259 else {
260 unsafe {
264 self.allocator.free_offset(msg.transaction.offset);
265 }
266
267 sanitize_failures = sanitize_failures.wrapping_add(1);
268
269 continue;
270 };
271
272 let key = self.state.insert(TransactionState {
274 dead: false,
275 borrows: 0,
276 flags: msg.flags,
277 data: tx,
278 keys: None,
279 });
280
281 if callback(self, key) == TxDecision::Drop {
283 let state = self.state.remove(key).unwrap();
284 assert!(state.keys.is_none());
285 assert_eq!(state.borrows, 0);
286
287 unsafe { state.data.into_inner_data().free(&self.allocator) };
290 }
291 }
292
293 self.tpu_to_pack.finalize();
294
295 sanitize_failures
296 }
297
298 pub fn drain_worker(
299 &mut self,
300 worker: usize,
301 mut callback: impl FnMut(&mut Self, WorkerResponse<'_, M>) -> TxDecision,
302 max_count: usize,
303 ) {
304 self.workers[worker].0.worker_to_pack.sync();
305 for _ in 0..max_count {
306 let Some(rep) = self.workers[worker].0.worker_to_pack.try_read().copied() else {
307 break;
308 };
309 self.handle_worker_response(rep, &mut callback);
310 }
311 self.workers[worker].0.worker_to_pack.finalize();
312 }
313
314 pub fn schedule(
322 &mut self,
323 ScheduleBatch {
324 worker,
325 transactions: batch,
326 max_working_slot,
327 flags,
328 }: ScheduleBatch<&[KeyedTransactionMeta<M>]>,
329 ) -> Result<(), ScheduleError> {
330 let queue = &mut self.workers[worker].0.pack_to_worker;
331
332 queue.sync();
334 if queue.len() == queue.capacity() {
335 return Err(ScheduleError::Queue);
336 }
337
338 let batch = Self::collect_batch(&self.allocator, &mut self.state, batch)?;
340
341 queue
343 .try_write(PackToWorkerMessage {
344 flags,
345 max_working_slot,
346 batch,
347 })
348 .expect("space checked above");
349 queue.commit();
350
351 Ok(())
352 }
353
354 fn collect_batch(
355 allocator: &Allocator,
356 state: &mut SlotMap<TransactionKey, TransactionState>,
357 batch: &[KeyedTransactionMeta<M>],
358 ) -> Result<SharableTransactionBatchRegion, ScheduleError> {
359 assert!(batch.len() <= MAX_TRANSACTIONS_PER_MESSAGE);
360
361 let transactions = allocator
363 .allocate(Self::TRANSACTION_BATCH_SIZE as u32)
364 .ok_or(ScheduleError::Allocation)?;
365 let transactions_offset = unsafe { allocator.offset(transactions) };
366
367 let tx_ptr = unsafe {
369 allocator
370 .ptr_from_offset(transactions_offset)
371 .cast::<SharableTransactionRegion>()
372 };
373 let meta_ptr = unsafe {
377 allocator
378 .ptr_from_offset(transactions_offset)
379 .byte_add(Self::TRANSACTION_BATCH_META_OFFSET)
380 .cast::<KeyedTransactionMeta<M>>()
381 };
382
383 for (i, meta) in batch.iter().copied().enumerate() {
385 let tx = &mut state[meta.key];
386 assert!(!tx.dead);
387
388 tx.borrows = tx.borrows.checked_add(1).unwrap();
390
391 unsafe {
396 tx_ptr.add(i).write(
397 tx.data
398 .inner_data()
399 .to_sharable_transaction_region(allocator),
400 );
401 meta_ptr.add(i).write(meta);
402 };
403 }
404
405 Ok(SharableTransactionBatchRegion {
406 num_transactions: batch.len().try_into().unwrap(),
407 transactions_offset,
408 })
409 }
410
411 fn handle_worker_response(
412 &mut self,
413 rep: WorkerToPackMessage,
414 callback: &mut impl FnMut(&mut Self, WorkerResponse<'_, M>) -> TxDecision,
415 ) {
416 let transactions = unsafe {
418 self.allocator
419 .ptr_from_offset(rep.batch.transactions_offset)
420 .cast::<SharableTransactionRegion>()
421 };
422 let metas = unsafe {
425 transactions
426 .byte_add(Batch::<M>::TRANSACTION_META_START)
427 .cast()
428 };
429
430 let responses = match (rep.processed_code, rep.responses.tag) {
431 (processed_codes::PROCESSED, worker_message_types::EXECUTION_RESPONSE) => {
432 assert_eq!(
433 rep.batch.num_transactions,
434 rep.responses.num_transaction_responses
435 );
436 WorkerResponseBatch::Execution(unsafe {
437 self.allocator
438 .ptr_from_offset(rep.responses.transaction_responses_offset)
439 .cast()
440 })
441 }
442 (processed_codes::PROCESSED, worker_message_types::CHECK_RESPONSE) => {
443 assert_eq!(
444 rep.batch.num_transactions,
445 rep.responses.num_transaction_responses
446 );
447 WorkerResponseBatch::Check(unsafe {
448 self.allocator
449 .ptr_from_offset(rep.responses.transaction_responses_offset)
450 .cast()
451 })
452 }
453 (processed_codes::MAX_WORKING_SLOT_EXCEEDED, _) => WorkerResponseBatch::Unprocessed,
454 _ => panic!("Unexpected response; rep={rep:?}"),
455 };
456
457 for index in 0..usize::from(rep.batch.num_transactions) {
458 let KeyedTransactionMeta::<M> { key, meta } = unsafe { metas.add(index).read() };
461 let decision = self.handle_transaction_response(key, meta, index, &responses, callback);
462
463 if decision == TxDecision::Drop {
465 self.drop_transaction(key);
466 }
467 }
468
469 unsafe {
473 self.allocator.free_offset(rep.batch.transactions_offset);
474 match responses {
475 WorkerResponseBatch::Unprocessed => {}
476 WorkerResponseBatch::Execution(ptr) => self.allocator.free(ptr.cast()),
477 WorkerResponseBatch::Check(ptr) => self.allocator.free(ptr.cast()),
478 }
479 }
480 }
481
482 fn handle_transaction_response(
483 &mut self,
484 key: TransactionKey,
485 meta: M,
486 index: usize,
487 responses: &WorkerResponseBatch,
488 callback: &mut impl FnMut(&mut Self, WorkerResponse<'_, M>) -> TxDecision,
489 ) -> TxDecision {
490 let state = &mut self.state[key];
492 state.borrows = state.borrows.checked_sub(1).unwrap();
493
494 match (state.dead, responses) {
496 (true, WorkerResponseBatch::Check(rep)) => {
497 let rep = unsafe { rep.add(index).read() };
500
501 if rep.resolved_pubkeys.num_pubkeys > 0 {
503 unsafe {
508 let keys = PubkeysPtr::from_sharable_pubkeys(
509 &rep.resolved_pubkeys,
510 &self.allocator,
511 );
512 keys.free(&self.allocator);
513 };
514 }
515
516 TxDecision::Drop
517 }
518 (true, _) => TxDecision::Drop,
519 (false, WorkerResponseBatch::Unprocessed) => {
520 let rep = WorkerResponse {
521 key,
522 meta,
523 response: WorkerAction::Unprocessed,
524 };
525
526 callback(self, rep)
527 }
528 (false, WorkerResponseBatch::Execution(rep)) => {
529 let rep = unsafe { rep.add(index).read() };
532 let rep = WorkerResponse {
533 key,
534 meta,
535 response: WorkerAction::Execute(rep),
536 };
537
538 callback(self, rep)
539 }
540 (false, WorkerResponseBatch::Check(rep)) => {
541 let rep = unsafe { rep.add(index).read() };
544
545 let keys = (rep.resolved_pubkeys.num_pubkeys > 0).then(|| unsafe {
547 PubkeysPtr::from_sharable_pubkeys(&rep.resolved_pubkeys, &self.allocator)
551 });
552
553 let decision = callback(
555 self,
556 WorkerResponse {
557 key,
558 meta,
559 response: WorkerAction::Check(rep, keys.as_ref()),
560 },
561 );
562
563 if let Some(old_keys) = self.state[key].keys.take() {
565 unsafe { old_keys.free(&self.allocator) }
568 }
569
570 self.state[key].keys = keys;
572
573 decision
574 }
575 }
576 }
577}
578
579pub struct SchedulerWorker(ClientWorkerSession);
580
581impl SchedulerWorker {
582 pub fn is_empty(&mut self) -> bool {
583 self.len() == 0
584 }
585
586 pub fn len(&mut self) -> usize {
587 self.0.pack_to_worker.sync();
588
589 self.0.pack_to_worker.len()
590 }
591
592 pub fn rem(&mut self) -> usize {
593 self.0.pack_to_worker.sync();
594 let cap = self.0.pack_to_worker.capacity();
595 let len = self.0.pack_to_worker.len();
596
597 cap.checked_sub(len).unwrap()
598 }
599}
600
601enum WorkerResponseBatch {
602 Unprocessed,
603 Execution(NonNull<ExecutionResponse>),
604 Check(NonNull<CheckResponse>),
605}
606
607pub struct RuntimeState {
608 pub feature_set: FeatureSet,
609 pub fee_features: FeeFeatures,
610 pub lamports_per_signature: u64,
611 pub burn_percent: u64,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq)]
615pub struct ScheduleBatch<T> {
616 pub worker: usize,
617 pub transactions: T,
618 pub max_working_slot: u64,
619 pub flags: u16,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
623pub enum ScheduleError {
624 #[error("Queue full")]
625 Queue,
626 #[error("Allocation failed")]
627 Allocation,
628}
629
630#[derive(Debug, Clone)]
631pub struct WorkerResponse<'a, M> {
632 pub key: TransactionKey,
633 pub meta: M,
634 pub response: WorkerAction<'a>,
635}
636
637#[derive(Debug, Clone)]
638pub enum WorkerAction<'a> {
639 Unprocessed,
640 Check(CheckResponse, Option<&'a PubkeysPtr>),
641 Execute(ExecutionResponse),
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub struct KeyedTransactionMeta<M> {
646 pub key: TransactionKey,
647 pub meta: M,
648}
649
650slotmap::new_key_type! {
651 pub struct TransactionKey;
652}
653
654#[derive(Debug)]
655pub struct TransactionState {
656 pub dead: bool,
657 pub borrows: u64,
658 pub flags: u8,
659 pub data: SanitizedTransactionView<TransactionPtr>,
660 pub keys: Option<PubkeysPtr>,
661}
662
663impl TransactionState {
664 #[must_use]
665 pub const fn is_simple_vote(&self) -> bool {
666 self.flags & tpu_message_flags::IS_SIMPLE_VOTE != 0
667 }
668
669 pub fn locks(&self) -> impl Iterator<Item = (&Pubkey, bool)> {
670 self.write_locks()
671 .map(|lock| (lock, true))
672 .chain(self.read_locks().map(|lock| (lock, false)))
673 }
674
675 pub fn write_locks(&self) -> impl Iterator<Item = &Pubkey> {
676 self.data
677 .static_account_keys()
678 .iter()
679 .chain(self.keys.iter().flat_map(|keys| keys.as_slice().iter()))
680 .enumerate()
681 .filter(|(i, _)| self.is_writable(*i as u8))
682 .map(|(_, key)| key)
683 }
684
685 pub fn read_locks(&self) -> impl Iterator<Item = &Pubkey> {
686 self.data
687 .static_account_keys()
688 .iter()
689 .chain(self.keys.iter().flat_map(|keys| keys.as_slice().iter()))
690 .enumerate()
691 .filter(|(i, _)| !self.is_writable(*i as u8))
692 .map(|(_, key)| key)
693 }
694
695 fn is_writable(&self, index: u8) -> bool {
709 if index >= self.data.num_static_account_keys() {
710 let loaded_address_index = index.wrapping_sub(self.data.num_static_account_keys());
711 loaded_address_index < self.data.total_writable_lookup_accounts() as u8
712 } else {
713 index
714 < self
715 .data
716 .num_required_signatures()
717 .wrapping_sub(self.data.num_readonly_signed_static_accounts())
718 || (index >= self.data.num_required_signatures()
719 && index
720 < (self.data.static_account_keys().len() as u8)
721 .wrapping_sub(self.data.num_readonly_unsigned_static_accounts()))
722 }
723 }
724}
725
726#[derive(Debug, PartialEq, Eq)]
727pub enum TxDecision {
728 Keep,
729 Drop,
730}
731
732#[derive(Debug, PartialEq, Eq, Error)]
733pub enum TransactionInsertError {
734 #[error("Failed to parse or sanitize; err={0:?}")]
735 ParseSanitize(TransactionViewError),
736 #[error("Failed to allocate")]
737 Allocate,
738}