Skip to main content

cow_settlement_interface/data/
state.rs

1//! Settlement state account: its byte layout and the zero-copy accessor over it.
2//!
3//! The state PDA (see [`crate::pda::state`]) stores the protocol's authority
4//! configuration in a fixed header (a discriminator byte followed by the holder
5//! of each [`Role`]), then the list of approved solvers, packed and sorted
6//! ascending by address.
7//!
8//! ```text
9//!  ┌──── discriminator
10//!  ┌┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───── ... ─────┬───────────────────────────────┐
11//!  ││            manager            │       reclaim_authority       │           solver[0]           │ other solvers │          solver[N-1]          │
12//!  └┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───── ... ─────┴───────────────────────────────┘
13//! 0 1                               33                              65                              97
14//!  └───────────────────────────── header ──────────────────────────┘└─────────────────────────────── N sorted solvers ─────────────────────────────┘
15//! ```
16//!
17//! [`StateAccount`] is a zero-copy accessor over an account's bytes, generic
18//! over the borrow (`&[u8]`, `&mut [u8]`): the reads are available for any
19//! borrow, the in-place role write only for a mutable one. It reads and updates
20//! the account data directly, so the program never copies the account into an
21//! owned struct.
22
23use core::mem::size_of;
24use core::ops::{Deref, DerefMut};
25
26use arrayref::{array_refs, mut_array_refs};
27use solana_account_view::{AccountView, Ref};
28use solana_program_error::ProgramError;
29use solana_pubkey::Pubkey;
30
31use crate::{Role, SettlementAccount, SettlementError};
32
33/// Single-byte account discriminator at the front of the header.
34pub const DISCRIMINATOR: u8 = SettlementAccount::SettlementState.discriminator();
35
36/// Byte width of the discriminator.
37const WIDTH_DISCRIMINATOR: usize = size_of::<u8>();
38
39/// Byte width of an account address (a [`Pubkey`]): each role holder in the
40/// header and each solver in the list is one.
41pub const WIDTH_PUBKEY: usize = size_of::<Pubkey>();
42
43/// Length of the fixed header: the discriminator byte followed by one holder
44/// per [`Role`].
45pub const WIDTH_HEADER: usize = WIDTH_DISCRIMINATOR + 2 * WIDTH_PUBKEY;
46
47/// A borrowed view over the bytes of a state acc, split into its
48/// discriminator and per-role slots so each can be named. The slots hold raw
49/// encoded bytes, not decoded [`Pubkey`]s.
50struct HeaderSlots<'a> {
51    discriminator: &'a [u8; WIDTH_DISCRIMINATOR],
52    manager: &'a [u8; WIDTH_PUBKEY],
53    reclaim_authority: &'a [u8; WIDTH_PUBKEY],
54}
55
56/// The mutable counterpart of [`HeaderSlots`], for in-place writes.
57struct HeaderSlotsMut<'a> {
58    discriminator: &'a mut [u8; WIDTH_DISCRIMINATOR],
59    manager: &'a mut [u8; WIDTH_PUBKEY],
60    reclaim_authority: &'a mut [u8; WIDTH_PUBKEY],
61}
62
63/// Split the header into its named slots.
64fn header_slots(header: &[u8; WIDTH_HEADER]) -> HeaderSlots<'_> {
65    let (discriminator, manager, reclaim_authority) =
66        array_refs![header, WIDTH_DISCRIMINATOR, WIDTH_PUBKEY, WIDTH_PUBKEY];
67    HeaderSlots {
68        discriminator,
69        manager,
70        reclaim_authority,
71    }
72}
73
74/// [`header_slots`] over a mutable header, for in-place writes.
75fn header_slots_mut(header: &mut [u8; WIDTH_HEADER]) -> HeaderSlotsMut<'_> {
76    let (discriminator, manager, reclaim_authority) =
77        mut_array_refs![header, WIDTH_DISCRIMINATOR, WIDTH_PUBKEY, WIDTH_PUBKEY];
78    HeaderSlotsMut {
79        discriminator,
80        manager,
81        reclaim_authority,
82    }
83}
84
85/// The parameters used to initialize the state account.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct StateInitArgs {
88    /// The [`Role::Manager`] holder.
89    pub manager: Pubkey,
90    /// The [`Role::ReclaimAuthority`] holder.
91    pub reclaim_authority: Pubkey,
92}
93
94/// A zero-copy accessor over a settlement state account's canonical byte
95/// representation: the discriminator byte followed by the current holder of
96/// each [`Role`].
97///
98/// `T` is the borrow backing it, anything that dereferences to the account's
99/// bytes: `&[u8]` grant read access; `&mut [u8]` grants write access.
100pub struct StateAccount<T>(T);
101
102impl<T: Deref<Target = [u8]>> StateAccount<T> {
103    /// Wrap an account's bytes, checking they begin with the settlement-state
104    /// discriminator and are at least a full header long. Every accessor relies
105    /// on that guarantee not to panic.
106    pub fn attach(bytes: T) -> Result<Self, ProgramError> {
107        let header = bytes
108            .first_chunk::<WIDTH_HEADER>()
109            .ok_or(ProgramError::InvalidAccountData)?;
110        if header_slots(header).discriminator != &[DISCRIMINATOR] {
111            return Err(ProgramError::InvalidAccountData);
112        }
113        Ok(Self(bytes))
114    }
115
116    fn header(&self) -> &[u8; WIDTH_HEADER] {
117        self.0
118            .first_chunk::<WIDTH_HEADER>()
119            .expect("header length is guaranteed by any constructor of `StateAccount`")
120    }
121
122    /// Current holder of `role`.
123    pub fn authority(&self, role: Role) -> Pubkey {
124        let slots = header_slots(self.header());
125        let holder = match role {
126            Role::Manager => slots.manager,
127            Role::ReclaimAuthority => slots.reclaim_authority,
128        };
129        Pubkey::new_from_array(*holder)
130    }
131
132    /// The sorted solver list that follows the header, as fixed-width entries.
133    /// A trailing partial entry (only possible on a corrupt account) is ignored
134    /// and an uninitialized/too-small account returns an empty slice.
135    fn solver_region(&self) -> &[[u8; WIDTH_PUBKEY]] {
136        let region = self.0.get(WIDTH_HEADER..).unwrap_or_default();
137        let (solvers, _partial) = region.as_chunks::<WIDTH_PUBKEY>();
138        solvers
139    }
140
141    /// Locate `solver` in the sorted solver list, mirroring
142    /// [`slice::binary_search`]: `Ok(index)` if present, `Err(index)` with the
143    /// slot it would occupy otherwise. The list is sorted by address bytes.
144    pub fn solver_search(&self, solver: &Pubkey) -> Result<usize, usize> {
145        let seek = solver.to_bytes();
146        self.solver_region().binary_search(&seek)
147    }
148
149    /// Whether `solver` is in the solver list.
150    pub fn is_solver(&self, solver: &Pubkey) -> bool {
151        self.solver_search(solver).is_ok()
152    }
153
154    /// The stored solvers, in order (sorted ascending by address).
155    pub fn solvers(&self) -> impl Iterator<Item = Pubkey> + '_ {
156        self.solver_region()
157            .iter()
158            .map(|raw| Pubkey::new_from_array(*raw))
159    }
160
161    /// The account's data length after growing it by one solver slot: the size
162    /// it must be resized to before [`insert_solver`](Self::insert_solver) can
163    /// fill that new slot.
164    ///
165    /// Returns [`ProgramError::ArithmeticOverflow`] if that length overflows
166    /// `usize`, which a caller handling data from a real account can treat as
167    /// unreachable: the runtime caps account data at
168    /// [`MAX_PERMITTED_DATA_LENGTH`](solana_system_interface::MAX_PERMITTED_DATA_LENGTH).
169    pub fn grown_len(&self) -> Result<usize, ProgramError> {
170        self.0
171            .len()
172            .checked_add(WIDTH_PUBKEY)
173            .ok_or(ProgramError::ArithmeticOverflow)
174    }
175}
176
177impl<'a> StateAccount<Ref<'a, [u8]>> {
178    pub fn from_account(account: &'a AccountView) -> Result<Self, ProgramError> {
179        Self::attach(account.try_borrow()?)
180    }
181}
182
183impl<T: DerefMut<Target = [u8]>> StateAccount<T> {
184    /// Stamp a fresh header (discriminator + role holders) into a zeroed account
185    /// and return the accessor over it.
186    ///
187    /// The generated data is a full header long, which is needed for the read
188    /// accessors not to panic.
189    pub fn initialize(mut bytes: T, args: &StateInitArgs) -> Result<Self, ProgramError> {
190        {
191            let slots = header_slots_mut(
192                bytes
193                    .first_chunk_mut::<WIDTH_HEADER>()
194                    .ok_or(ProgramError::AccountDataTooSmall)?,
195            );
196            *slots.discriminator = [DISCRIMINATOR];
197            *slots.manager = args.manager.to_bytes();
198            *slots.reclaim_authority = args.reclaim_authority.to_bytes();
199        }
200        Ok(Self(bytes))
201    }
202
203    fn header_mut(&mut self) -> &mut [u8; WIDTH_HEADER] {
204        let bytes: &mut [u8] = &mut self.0;
205        bytes
206            .first_chunk_mut::<WIDTH_HEADER>()
207            .expect("header length is guaranteed by `new`")
208    }
209
210    /// Set `role`'s holder to `new` in place.
211    pub fn set_authority(&mut self, role: Role, new: &Pubkey) {
212        let slots = header_slots_mut(self.header_mut());
213        let holder = match role {
214            Role::Manager => slots.manager,
215            Role::ReclaimAuthority => slots.reclaim_authority,
216        };
217        *holder = new.to_bytes();
218    }
219
220    /// Insert `solver` into the sorted solver list, or fail with
221    /// [`SettlementError::SolverAlreadyExists`] if it is already stored.
222    ///
223    /// The account must already be grown by one solver slot
224    /// ([`grown_len`](Self::grown_len)): the trailing slot is spare capacity
225    /// for the new entry, so the live list is every entry but that last one.
226    /// The solver is placed in sorted order by shifting all entries and
227    /// inserting the new solver in the initial slot.
228    /// An error is returned if the solver is already included.
229    pub fn insert_solver(&mut self, solver: &Pubkey) -> Result<(), ProgramError> {
230        let (_spare, occupied) = self
231            .solver_region()
232            .split_last()
233            .expect("account grown by one solver slot before insertion");
234        let index = match occupied.binary_search(&solver.to_bytes()) {
235            Ok(_) => return Err(SettlementError::SolverAlreadyExists.into()),
236            Err(index) => index,
237        };
238
239        // Shift the entries at and after `index` up one slot into the spare,
240        // then write the solver into the gap that opens at `index`.
241        let data: &mut [u8] = &mut self.0;
242        let occupied_end = data
243            .len()
244            .checked_sub(WIDTH_PUBKEY)
245            .expect("account grown by one solver slot before insertion");
246        let gap = WIDTH_HEADER
247            .checked_add(
248                index
249                    .checked_mul(WIDTH_PUBKEY)
250                    .expect("insertion index bound by account length"),
251            )
252            .expect("insertion offset bound by account length");
253        let gap_end = gap
254            .checked_add(WIDTH_PUBKEY)
255            .expect("insertion slot bound by account length");
256
257        data.copy_within(gap..occupied_end, gap_end);
258        data[gap..gap_end].copy_from_slice(&solver.to_bytes());
259        Ok(())
260    }
261}
262
263/// Test scaffolding for building state-account bytes, shared by this crate's
264/// tests and the settlement program's via the `test-fixtures` feature.
265#[cfg(any(test, feature = "test-fixtures"))]
266pub mod fixtures {
267    use proptest::prelude::*;
268    use solana_pubkey::Pubkey;
269
270    use super::{StateAccount, StateInitArgs, WIDTH_HEADER};
271
272    /// The bytes of a state account: the `header` followed by `solvers`, stored
273    /// sorted ascending by address as the on-chain list always is (so callers can
274    /// pass them in any order).
275    pub fn state_account_bytes(header: &StateInitArgs, solvers: &[Pubkey]) -> Vec<u8> {
276        let mut sorted = solvers.to_vec();
277        sorted.sort();
278        let mut bytes = vec![0u8; WIDTH_HEADER];
279        StateAccount::initialize(&mut bytes[..], header).expect("header fits");
280        for solver in &sorted {
281            bytes.extend_from_slice(&solver.to_bytes());
282        }
283        bytes
284    }
285
286    /// Any valid [`StateInitArgs`].
287    pub fn arb_init_params() -> impl Strategy<Value = StateInitArgs> {
288        (any::<[u8; 32]>(), any::<[u8; 32]>()).prop_map(|(manager, reclaim_authority)| {
289            StateInitArgs {
290                manager: Pubkey::new_from_array(manager),
291                reclaim_authority: Pubkey::new_from_array(reclaim_authority),
292            }
293        })
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use std::sync::LazyLock;
300
301    use super::*;
302    use crate::fixtures::pubkey_from_seed;
303
304    /// Byte offset of the discriminator within the account.
305    const DISCRIMINATOR_OFFSET: usize = 0;
306
307    static SAMPLE_INIT_ARGS: LazyLock<StateInitArgs> = LazyLock::new(|| StateInitArgs {
308        manager: pubkey_from_seed("SAMPLE_INIT_ARGS's sample manager"),
309        reclaim_authority: pubkey_from_seed("SAMPLE_INIT_ARGS's sample reclaim authority"),
310    });
311
312    /// State account bytes stamped with [`SAMPLE_INIT_ARGS`].
313    fn header_bytes() -> [u8; WIDTH_HEADER] {
314        let mut bytes = [0u8; WIDTH_HEADER];
315        StateAccount::initialize(&mut bytes[..], &SAMPLE_INIT_ARGS).expect("header fits");
316        bytes
317    }
318
319    /// [`SAMPLE_INIT_ARGS`] followed by `solvers`, stored sorted ascending by address
320    /// as the on-chain list always is, so callers can pass them in any order.
321    fn state_bytes(solvers: &[Pubkey]) -> Vec<u8> {
322        super::fixtures::state_account_bytes(&SAMPLE_INIT_ARGS, solvers)
323    }
324
325    #[test]
326    fn header_has_the_canonical_wire_layout() {
327        assert_eq!(WIDTH_HEADER, 65);
328
329        let bytes = header_bytes();
330        assert_eq!(bytes[0], SettlementAccount::SettlementState.discriminator());
331        assert_eq!(&bytes[1..33], &SAMPLE_INIT_ARGS.manager.to_bytes()[..]);
332        assert_eq!(
333            &bytes[33..65],
334            &SAMPLE_INIT_ARGS.reclaim_authority.to_bytes()[..]
335        );
336    }
337
338    #[test]
339    fn reads_role_holders_from_the_header() {
340        let bytes = header_bytes();
341        let state = StateAccount::attach(&bytes[..]).expect("valid header");
342        assert_eq!(state.authority(Role::Manager), SAMPLE_INIT_ARGS.manager);
343        assert_eq!(
344            state.authority(Role::ReclaimAuthority),
345            SAMPLE_INIT_ARGS.reclaim_authority
346        );
347    }
348
349    #[test]
350    fn new_rejects_wrong_discriminator() {
351        let mut bytes = header_bytes();
352        bytes[DISCRIMINATOR_OFFSET] = 0xff;
353        assert_eq!(
354            StateAccount::attach(&bytes[..]).err(),
355            Some(ProgramError::InvalidAccountData),
356        );
357    }
358
359    #[test]
360    fn new_rejects_too_short_buffer() {
361        let bytes = header_bytes();
362        assert_eq!(
363            StateAccount::attach(&bytes[..WIDTH_HEADER - 1]).err(),
364            Some(ProgramError::InvalidAccountData),
365        );
366    }
367
368    /// Sets `target`'s holder and asserts it changed while every other role's
369    /// holder stayed put.
370    fn assert_set_authority_updates_only(target: Role) {
371        let new_holder = pubkey_from_seed("set_authority's new holder");
372
373        let mut bytes = header_bytes();
374        let before = Role::ALL.map(|role| {
375            StateAccount::attach(&bytes[..])
376                .expect("valid header")
377                .authority(role)
378        });
379
380        StateAccount::attach(&mut bytes[..])
381            .expect("valid header")
382            .set_authority(target, &new_holder);
383
384        let state = StateAccount::attach(&bytes[..]).expect("valid header");
385        for (role, prior) in Role::ALL.into_iter().zip(before) {
386            let expected = if role == target { new_holder } else { prior };
387            assert_eq!(state.authority(role), expected);
388        }
389    }
390
391    /// Generates one test, `set_authority_updates_only_<role>`, for `$role`.
392    macro_rules! set_authority_test {
393        ($name:ident: $role:expr) => {
394            #[test]
395            fn $name() {
396                assert_set_authority_updates_only($role);
397            }
398        };
399    }
400
401    set_authority_test!(set_authority_updates_only_manager: Role::Manager);
402    set_authority_test!(set_authority_updates_only_reclaim_authority: Role::ReclaimAuthority);
403
404    #[test]
405    fn new_accepts_a_longer_account_and_reads_the_header() {
406        let mut bytes = header_bytes().to_vec();
407        bytes.push(0x42);
408
409        let state = StateAccount::attach(&bytes[..]).expect("header with trailing bytes is valid");
410        assert_eq!(state.authority(Role::Manager), SAMPLE_INIT_ARGS.manager);
411        assert_eq!(
412            state.authority(Role::ReclaimAuthority),
413            SAMPLE_INIT_ARGS.reclaim_authority
414        );
415    }
416
417    /// Three solvers in arbitrary order; `state_bytes` stores them sorted, as the
418    /// on-chain list always is. Returns both the stored bytes and the sorted
419    /// order the reads should reflect.
420    fn sample_solvers() -> (Vec<u8>, [Pubkey; 3]) {
421        let solvers = [
422            pubkey_from_seed("sample_solver's solver a"),
423            pubkey_from_seed("sample_solver's solver b"),
424            pubkey_from_seed("sample_solver's solver c"),
425        ];
426        let bytes = state_bytes(&solvers);
427        let mut sorted = solvers;
428        sorted.sort();
429        (bytes, sorted)
430    }
431
432    #[test]
433    fn solvers_is_empty_without_any_stored() {
434        let bytes = state_bytes(&[]);
435        let state = StateAccount::attach(&bytes[..]).expect("valid header");
436        assert_eq!(state.solvers().count(), 0);
437    }
438
439    #[test]
440    fn solvers_lists_stored_solvers_sorted() {
441        let (bytes, sorted) = sample_solvers();
442        let state = StateAccount::attach(&bytes[..]).expect("valid header");
443        assert_eq!(state.solvers().collect::<Vec<_>>(), sorted);
444    }
445
446    #[test]
447    fn solver_search_on_empty_list() {
448        let bytes = state_bytes(&[]);
449        let state = StateAccount::attach(&bytes[..]).expect("valid header");
450        let absent = pubkey_from_seed("absent solver");
451        assert_eq!(state.solver_search(&absent), Err(0));
452    }
453
454    #[test]
455    fn solver_search_finds_present() {
456        let (bytes, sorted) = sample_solvers();
457        let state = StateAccount::attach(&bytes[..]).expect("valid header");
458        for (index, solver) in sorted.iter().enumerate() {
459            assert_eq!(state.solver_search(solver), Ok(index));
460        }
461    }
462
463    #[test]
464    fn solver_search_locates_absent() {
465        let (bytes, sorted) = sample_solvers();
466        let state = StateAccount::attach(&bytes[..]).expect("valid header");
467        // An absent solver isn't found; its reported slot is where inserting it
468        // would keep the list sorted.
469        let absent = pubkey_from_seed("absent solver");
470        let slot = state.solver_search(&absent).expect_err("absent solver");
471        assert_eq!(slot, sorted.partition_point(|s| s < &absent));
472    }
473
474    #[test]
475    fn initialize_rejects_too_small_buffer() {
476        let mut bytes = [0u8; WIDTH_HEADER - 1];
477        assert_eq!(
478            StateAccount::initialize(&mut bytes[..], &SAMPLE_INIT_ARGS).err(),
479            Some(ProgramError::AccountDataTooSmall),
480        );
481    }
482
483    mod proptest {
484        use ::proptest::prelude::*;
485
486        use super::*;
487
488        proptest! {
489            /// `is_solver` is true for every stored solver and false for one that
490            /// isn't stored.
491            #[test]
492            fn is_solver_reflects_membership(
493                header in fixtures::arb_init_params(),
494                // Unique and already sorted, being a `BTreeSet`.
495                raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 0..50),
496                raw_absent in any::<[u8; 32]>(),
497            ) {
498                prop_assume!(!raw_solvers.contains(&raw_absent));
499                let stored: Vec<Pubkey> =
500                    raw_solvers.into_iter().map(Pubkey::new_from_array).collect();
501                let absent = Pubkey::new_from_array(raw_absent);
502
503                let bytes = fixtures::state_account_bytes(&header, &stored);
504                let state = StateAccount::attach(&bytes[..]).expect("valid header");
505
506                for solver in &stored {
507                    prop_assert!(state.is_solver(solver));
508                }
509                prop_assert!(!state.is_solver(&absent));
510            }
511
512            /// The encode roundtrip: any two role holders written with
513            /// `initialize` read back unchanged.
514            #[test]
515            fn account_encode_roundtrip(header in fixtures::arb_init_params()) {
516                let mut bytes = [0u8; WIDTH_HEADER];
517                StateAccount::initialize(&mut bytes[..], &header).expect("header fits");
518
519                let state = StateAccount::attach(&bytes[..]).expect("valid header");
520                let StateInitArgs { manager, reclaim_authority } = header;
521                prop_assert_eq!(state.authority(Role::Manager), manager);
522                prop_assert_eq!(state.authority(Role::ReclaimAuthority), reclaim_authority);
523            }
524
525            #[test]
526            fn insert_solver_inserts_an_absent_solver(
527                header in fixtures::arb_init_params(),
528                // Unique and already sorted, being a `BTreeSet`.
529                raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 0..50),
530                raw_new in any::<[u8; 32]>(),
531            ) {
532                prop_assume!(!raw_solvers.contains(&raw_new));
533                let stored: Vec<Pubkey> =
534                    raw_solvers.into_iter().map(Pubkey::new_from_array).collect();
535                let new = Pubkey::new_from_array(raw_new);
536
537                // Grow by one slot, exactly as the handler resizes the account
538                // before delegating the insert.
539                let mut bytes = fixtures::state_account_bytes(&header, &stored);
540                let grown_len = StateAccount::attach(&bytes[..])
541                    .expect("valid header")
542                    .grown_len()
543                    .expect("grown length fits");
544                prop_assert_eq!(grown_len, bytes.len().strict_add(WIDTH_PUBKEY));
545                bytes.resize(grown_len, 0);
546                StateAccount::attach(&mut bytes[..])
547                    .expect("valid header")
548                    .insert_solver(&new)
549                    .expect("absent solver inserts");
550
551                let mut expected = stored;
552                expected.push(new);
553                expected.sort();
554                let state = StateAccount::attach(&bytes[..]).expect("valid header");
555                prop_assert_eq!(state.solvers().collect::<Vec<_>>(), expected);
556            }
557
558            /// `insert_solver` rejects a solver that is already stored and leaves
559            /// the live list untouched.
560            #[test]
561            fn insert_solver_rejects_an_existing_solver(
562                header in fixtures::arb_init_params(),
563                // Unique and already sorted, being a `BTreeSet`.
564                raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 1..50),
565                pick in any::<prop::sample::Index>(),
566            ) {
567                let stored: Vec<Pubkey> =
568                    raw_solvers.into_iter().map(Pubkey::new_from_array).collect();
569                let existing = stored[pick.index(stored.len())];
570
571                // Grow by one slot as the handler does, then try to re-add.
572                let mut bytes = fixtures::state_account_bytes(&header, &stored);
573                bytes.resize(bytes.len().strict_add(WIDTH_PUBKEY), 0);
574                prop_assert_eq!(
575                    StateAccount::attach(&mut bytes[..])
576                        .expect("valid header")
577                        .insert_solver(&existing),
578                    Err(SettlementError::SolverAlreadyExists.into()),
579                );
580
581                // Nothing was written: the stored solvers still read back in
582                // order (the trailing spare slot is left as the zero pubkey).
583                let state = StateAccount::attach(&bytes[..]).expect("valid header");
584                prop_assert_eq!(state.solvers().take(stored.len()).collect::<Vec<_>>(), stored);
585            }
586        }
587    }
588}