Skip to main content

agave_scheduling_utils/bridge/
bindings.rs

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    /// Creates a new [`SchedulerBindingsBridge`] from a [`ClientSession`].
49    ///
50    /// # Note
51    ///
52    /// This bridge will leak any contained transaction allocations and in
53    /// flight message batch allocations on drop. It is intended to have a 1 to
54    /// 1 lifetime with the [`Allocator`] it owns (you should be dropping the
55    /// allocator shortly after you drop the bridge).
56    ///
57    /// # Panics
58    ///
59    /// - If the session contains more than one allocator.
60    #[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    /// Looks up the given worker by ID.
121    ///
122    /// # Panics
123    ///
124    /// - If the worker does not exist.
125    pub fn worker(&mut self, id: usize) -> &mut SchedulerWorker {
126        &mut self.workers[id]
127    }
128
129    /// Looks up the given transaction key.
130    ///
131    /// # Panics
132    ///
133    /// - If the transaction does not exist.
134    pub fn transaction(&self, key: TransactionKey) -> &TransactionState {
135        let tx = &self.state[key];
136        assert!(!tx.dead);
137
138        tx
139    }
140
141    /// Inserts a non TPU transaction into the bridge.
142    ///
143    /// # Panics
144    ///
145    /// - If the transaction exceeds 4096 bytes.
146    pub fn insert_transaction(
147        &mut self,
148        tx: &[u8],
149    ) -> Result<TransactionKey, TransactionInsertError> {
150        // TODO: Move to rts_alloc::MAX_ALLOC_SIZE once exposed.
151        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        // SAFETY:
158        // - We own this pointer exclusively.
159        // - The allocated region is at least `tx.len()` bytes.
160        unsafe {
161            std::ptr::copy_nonoverlapping(tx.as_ptr(), ptr.as_ptr(), tx.len());
162        }
163        // SAFETY:
164        // - We own this pointer and the size is correct.
165        let tx = unsafe { TransactionPtr::from_raw_parts(ptr, tx.len()) };
166
167        // Sanitize the transaction, drop it immediately if it fails sanitization.
168        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                // SAFETY:
182                // - We own `tx` exclusively.
183                // - The previous `TransactionPtr` has been dropped by `try_new_sanitized`.
184                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        // If we have requests that have borrowed this shared transaction region, then
195        // we can't immediately clean up and must instead flag it as dead.
196        match self.state[key].borrows {
197            0 => {
198                let state = self.state.remove(key).unwrap();
199
200                if let Some(keys) = state.keys {
201                    // SAFETY
202                    // - We own these pointers/allocations exclusively.
203                    unsafe {
204                        keys.free(&self.allocator);
205                    }
206                }
207
208                // SAFETY
209                // - We own the allocation exclusively.
210                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            // SAFETY:
250            // - Trust Agave to have properly transferred ownership to use & not to
251            //   free/access this.
252            // - We are only creating a single exclusive pointer.
253            let tx = unsafe {
254                TransactionPtr::from_sharable_transaction_region(&msg.transaction, &self.allocator)
255            };
256
257            // Sanitize the transaction, drop it immediately if it fails sanitization.
258            let Ok(tx) = SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true))
259            else {
260                // SAFETY:
261                // - We own `tx` exclusively.
262                // - The previous `TransactionPtr` has been dropped by `try_new_sanitized`.
263                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            // Get the ID so the caller can store it for later use.
273            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            // Remove & free the TX if the scheduler doesn't want it.
282            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                // SAFETY:
288                // - We own `tx` exclusively.
289                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    /// Builds & schedules the provided batch.
315    ///
316    /// # Panics
317    ///
318    /// - If the worker index does not exist.
319    /// - If any transaction in the batch does not exist.
320    /// - If the batch size exceeds [`MAX_TRANSACTIONS_PER_MESSAGE`].
321    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        // Check we have space.
333        queue.sync();
334        if queue.len() == queue.capacity() {
335            return Err(ScheduleError::Queue);
336        }
337
338        // Try allocate the batch.
339        let batch = Self::collect_batch(&self.allocator, &mut self.state, batch)?;
340
341        // Write the batch.
342        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        // Allocate a batch that can hold all our transaction pointers.
362        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        // Get our two pointers to the TX region & meta region.
368        let tx_ptr = unsafe {
369            allocator
370                .ptr_from_offset(transactions_offset)
371                .cast::<SharableTransactionRegion>()
372        };
373        // SAFETY
374        // - Pointer is guaranteed to not overrun the allocation as we just created it
375        //   with a sufficient size.
376        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        // Fill in the batch with transaction pointers.
384        for (i, meta) in batch.iter().copied().enumerate() {
385            let tx = &mut state[meta.key];
386            assert!(!tx.dead);
387
388            // We are sending a copy to Agave, we track this as a new borrow.
389            tx.borrows = tx.borrows.checked_add(1).unwrap();
390
391            // SAFETY
392            // - We have allocated the transaction batch to support at least
393            //   `MAX_TRANSACTIONS_PER_MESSAGE`, we terminate the loop before we overrun the
394            //   region.
395            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        // Get transaction & meta pointers.
417        let transactions = unsafe {
418            self.allocator
419                .ptr_from_offset(rep.batch.transactions_offset)
420                .cast::<SharableTransactionRegion>()
421        };
422        // SAFETY:
423        // - We ensured that this batch was originally allocated to support M.
424        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            // SAFETY
459            // - We took care to allocate these correctly originally.
460            let KeyedTransactionMeta::<M> { key, meta } = unsafe { metas.add(index).read() };
461            let decision = self.handle_transaction_response(key, meta, index, &responses, callback);
462
463            // Remove the tx from state & drop the allocation if requested.
464            if decision == TxDecision::Drop {
465                self.drop_transaction(key);
466            }
467        }
468
469        // SAFETY:
470        // - It is our responsibility to free the response pointers. The transaction
471        //   lifetimes we are already managing separately via Keep/Drop.
472        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        // Decrease the borrow counter as Agave has returned ownership to us.
491        let state = &mut self.state[key];
492        state.borrows = state.borrows.checked_sub(1).unwrap();
493
494        // Only callback if this state is not already dead (scheduler requested drop).
495        match (state.dead, responses) {
496            (true, WorkerResponseBatch::Check(rep)) => {
497                // SAFETY
498                // - We trust Agave to have correctly allocated the responses.
499                let rep = unsafe { rep.add(index).read() };
500
501                // Free shared pubkeys if there are any.
502                if rep.resolved_pubkeys.num_pubkeys > 0 {
503                    // SAFETY
504                    // - Region exists as `num_pubkeys > 0`.
505                    // - Trust Agave to have allocated this region correctly.
506                    // - We now own it exclusively.
507                    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                // SAFETY
530                // - We trust Agave to have correctly allocated the responses.
531                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                // SAFETY
542                // - We trust Agave to have correctly allocated the responses.
543                let rep = unsafe { rep.add(index).read() };
544
545                // Load shared pubkeys if there are any.
546                let keys = (rep.resolved_pubkeys.num_pubkeys > 0).then(|| unsafe {
547                    // SAFETY
548                    // - Region exists as `num_pubkeys > 0`.
549                    // - Trust Agave to have allocated this region correctly.
550                    PubkeysPtr::from_sharable_pubkeys(&rep.resolved_pubkeys, &self.allocator)
551                });
552
553                // Callback holding keys ref, defer storing keys on state.
554                let decision = callback(
555                    self,
556                    WorkerResponse {
557                        key,
558                        meta,
559                        response: WorkerAction::Check(rep, keys.as_ref()),
560                    },
561                );
562
563                // Free old keys if present before storing new keys.
564                if let Some(old_keys) = self.state[key].keys.take() {
565                    // SAFETY
566                    // - We own this allocation exclusively.
567                    unsafe { old_keys.free(&self.allocator) }
568                }
569
570                // Store the keys on state.
571                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    /// Determines if the account at `index` is writable based on its position
696    /// in the transaction header. This is a simplified version of the canonical
697    /// implementation in `ResolvedTransactionView::cache_is_writable` that
698    /// intentionally omits:
699    ///
700    /// - **Reserved account key demotion**: reserved accounts (sysvars, builtins)
701    ///   in writable positions are not demoted to read-only.
702    /// - **Program account demotion**: writable program accounts are not demoted
703    ///   when `bpf_loader_upgradeable` is absent.
704    ///
705    /// Both omissions make this implementation **conservatively over-report**
706    /// writability, which may reduce scheduling parallelism but cannot cause
707    /// incorrect lock conflicts.
708    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}