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