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        unsafe {
113            raw.write(RuntimeAccount {
114                borrow_state: NOT_BORROWED,
115                is_signer: is_signer as u8,
116                is_writable: is_writable as u8,
117                executable: 0,
118                resize_delta: 0,
119                address: NativeAddress::new_from_array([address_byte; 32]),
120                owner: NativeAddress::new_from_array([7; 32]),
121                lamports: 1,
122                data_len: 16,
123            });
124        }
125        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
126        (backing, AccountView::from_backend(backend))
127    }
128
129    #[test]
130    fn detects_any_duplicate() {
131        let (_a_backing, first) = make_account(1, false, false);
132        let (_b_backing, second) = make_account(1, false, false);
133        let accounts = [first, second];
134        let audit = AccountAudit::new(&accounts);
135
136        let duplicate = audit.first_duplicate().unwrap();
137        assert_eq!(duplicate.first_index, 0);
138        assert_eq!(duplicate.second_index, 1);
139        assert_eq!(duplicate.address, Address::new_from_array([1; 32]));
140        assert_eq!(audit.require_all_unique(), Err(ProgramError::InvalidArgument));
141    }
142
143    #[test]
144    fn read_only_duplicates_do_not_fail_writable_audit() {
145        let (_a_backing, first) = make_account(2, false, false);
146        let (_b_backing, second) = make_account(2, false, false);
147        let accounts = [first, second];
148        let audit = AccountAudit::new(&accounts);
149
150        assert!(audit.first_duplicate_writable().is_none());
151        assert_eq!(audit.require_unique_writable(), Ok(()));
152    }
153
154    #[test]
155    fn writable_duplicates_are_rejected() {
156        let (_a_backing, first) = make_account(3, false, true);
157        let (_b_backing, second) = make_account(3, false, false);
158        let accounts = [first, second];
159        let audit = AccountAudit::new(&accounts);
160
161        let duplicate = audit.first_duplicate_writable().unwrap();
162        assert_eq!(duplicate.address, Address::new_from_array([3; 32]));
163        assert_eq!(audit.require_unique_writable(), Err(ProgramError::InvalidArgument));
164    }
165
166    #[test]
167    fn signer_duplicates_are_rejected() {
168        let (_a_backing, first) = make_account(4, true, false);
169        let (_b_backing, second) = make_account(4, false, false);
170        let accounts = [first, second];
171        let audit = AccountAudit::new(&accounts);
172
173        let duplicate = audit.first_duplicate_signer().unwrap();
174        assert_eq!(duplicate.address, Address::new_from_array([4; 32]));
175        assert_eq!(audit.require_unique_signers(), Err(ProgramError::InvalidArgument));
176    }
177}