Skip to main content

hopper_runtime/
audit.rs

1use crate::error::ProgramError;
2use crate::{AccountView, Address, ProgramResult};
3
4/// Duplicate-account details discovered during instruction-scope auditing.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct DuplicateAccount {
7    pub first_index: usize,
8    pub second_index: usize,
9    pub address: Address,
10}
11
12/// Instruction-scope duplicate-account audit over a slice of account views.
13#[derive(Clone, Copy)]
14pub struct AccountAudit<'a> {
15    accounts: &'a [AccountView],
16}
17
18impl<'a> AccountAudit<'a> {
19    #[inline(always)]
20    pub const fn new(accounts: &'a [AccountView]) -> Self {
21        Self { accounts }
22    }
23
24    #[inline(always)]
25    pub fn accounts(&self) -> &'a [AccountView] {
26        self.accounts
27    }
28
29    #[inline]
30    pub fn first_duplicate(&self) -> Option<DuplicateAccount> {
31        self.first_duplicate_where(|_, _| true)
32    }
33
34    #[inline]
35    pub fn first_duplicate_writable(&self) -> Option<DuplicateAccount> {
36        self.first_duplicate_where(|left, right| left.is_writable() || right.is_writable())
37    }
38
39    #[inline]
40    pub fn first_duplicate_signer(&self) -> Option<DuplicateAccount> {
41        self.first_duplicate_where(|left, right| left.is_signer() || right.is_signer())
42    }
43
44    #[inline]
45    pub fn require_all_unique(&self) -> ProgramResult {
46        if self.first_duplicate().is_some() {
47            Err(ProgramError::InvalidArgument)
48        } else {
49            Ok(())
50        }
51    }
52
53    #[inline]
54    pub fn require_unique_writable(&self) -> ProgramResult {
55        if self.first_duplicate_writable().is_some() {
56            Err(ProgramError::InvalidArgument)
57        } else {
58            Ok(())
59        }
60    }
61
62    #[inline]
63    pub fn require_unique_signers(&self) -> ProgramResult {
64        if self.first_duplicate_signer().is_some() {
65            Err(ProgramError::InvalidArgument)
66        } else {
67            Ok(())
68        }
69    }
70
71    #[inline]
72    fn first_duplicate_where(
73        &self,
74        predicate: impl Fn(&AccountView, &AccountView) -> bool,
75    ) -> Option<DuplicateAccount> {
76        let mut i = 0;
77        while i < self.accounts.len() {
78            let left = &self.accounts[i];
79            let mut j = i + 1;
80            while j < self.accounts.len() {
81                let right = &self.accounts[j];
82                if left.address() == right.address() && predicate(left, right) {
83                    return Some(DuplicateAccount {
84                        first_index: i,
85                        second_index: j,
86                        address: *left.address(),
87                    });
88                }
89                j += 1;
90            }
91            i += 1;
92        }
93        None
94    }
95}
96
97#[cfg(all(test, feature = "hopper-native-backend"))]
98mod tests {
99    use super::*;
100
101    use hopper_native::{
102        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
103    };
104
105    fn make_account(
106        address_byte: u8,
107        is_signer: bool,
108        is_writable: bool,
109    ) -> (std::vec::Vec<u8>, AccountView) {
110        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + 16];
111        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
112        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
113        unsafe {
114            raw.write(RuntimeAccount {
115                borrow_state: NOT_BORROWED,
116                is_signer: is_signer as u8,
117                is_writable: is_writable as u8,
118                executable: 0,
119                resize_delta: 0,
120                address: NativeAddress::new_from_array([address_byte; 32]),
121                owner: NativeAddress::new_from_array([7; 32]),
122                lamports: 1,
123                data_len: 16,
124            });
125        }
126        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
127        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
128        (backing, AccountView::from_backend(backend))
129    }
130
131    #[test]
132    fn detects_any_duplicate() {
133        let (_a_backing, first) = make_account(1, false, false);
134        let (_b_backing, second) = make_account(1, false, false);
135        let accounts = [first, second];
136        let audit = AccountAudit::new(&accounts);
137
138        let duplicate = audit.first_duplicate().unwrap();
139        assert_eq!(duplicate.first_index, 0);
140        assert_eq!(duplicate.second_index, 1);
141        assert_eq!(duplicate.address, Address::new_from_array([1; 32]));
142        assert_eq!(
143            audit.require_all_unique(),
144            Err(ProgramError::InvalidArgument)
145        );
146    }
147
148    #[test]
149    fn read_only_duplicates_do_not_fail_writable_audit() {
150        let (_a_backing, first) = make_account(2, false, false);
151        let (_b_backing, second) = make_account(2, false, false);
152        let accounts = [first, second];
153        let audit = AccountAudit::new(&accounts);
154
155        assert!(audit.first_duplicate_writable().is_none());
156        assert_eq!(audit.require_unique_writable(), Ok(()));
157    }
158
159    #[test]
160    fn writable_duplicates_are_rejected() {
161        let (_a_backing, first) = make_account(3, false, true);
162        let (_b_backing, second) = make_account(3, false, false);
163        let accounts = [first, second];
164        let audit = AccountAudit::new(&accounts);
165
166        let duplicate = audit.first_duplicate_writable().unwrap();
167        assert_eq!(duplicate.address, Address::new_from_array([3; 32]));
168        assert_eq!(
169            audit.require_unique_writable(),
170            Err(ProgramError::InvalidArgument)
171        );
172    }
173
174    #[test]
175    fn signer_duplicates_are_rejected() {
176        let (_a_backing, first) = make_account(4, true, false);
177        let (_b_backing, second) = make_account(4, false, false);
178        let accounts = [first, second];
179        let audit = AccountAudit::new(&accounts);
180
181        let duplicate = audit.first_duplicate_signer().unwrap();
182        assert_eq!(duplicate.address, Address::new_from_array([4; 32]));
183        assert_eq!(
184            audit.require_unique_signers(),
185            Err(ProgramError::InvalidArgument)
186        );
187    }
188}