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