cow_settlement_interface/data/
state.rs1use 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
33pub const DISCRIMINATOR: u8 = SettlementAccount::SettlementState.discriminator();
35
36const WIDTH_DISCRIMINATOR: usize = size_of::<u8>();
38
39pub const WIDTH_PUBKEY: usize = size_of::<Pubkey>();
42
43pub const WIDTH_HEADER: usize = WIDTH_DISCRIMINATOR + 2 * WIDTH_PUBKEY;
46
47struct HeaderSlots<'a> {
51 discriminator: &'a [u8; WIDTH_DISCRIMINATOR],
52 manager: &'a [u8; WIDTH_PUBKEY],
53 reclaim_authority: &'a [u8; WIDTH_PUBKEY],
54}
55
56struct 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
63fn 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
74fn 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#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct StateInitArgs {
88 pub manager: Pubkey,
90 pub reclaim_authority: Pubkey,
92}
93
94pub struct StateAccount<T>(T);
101
102impl<T: Deref<Target = [u8]>> StateAccount<T> {
103 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 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 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 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 pub fn is_solver(&self, solver: &Pubkey) -> bool {
151 self.solver_search(solver).is_ok()
152 }
153
154 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 #[test]
492 fn is_solver_reflects_membership(
493 header in fixtures::arb_init_params(),
494 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 #[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 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 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 #[test]
561 fn insert_solver_rejects_an_existing_solver(
562 header in fixtures::arb_init_params(),
563 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 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 let state = StateAccount::attach(&bytes[..]).expect("valid header");
584 prop_assert_eq!(state.solvers().take(stored.len()).collect::<Vec<_>>(), stored);
585 }
586 }
587 }
588}