hopper_native/verify.rs
1//! Verified CPI -- pre/post state assertions around cross-program invocations.
2//!
3//! Hopper can bind a CPI call to explicit post-conditions instead of treating
4//! a successful return code as proof of the application-level result.
5//!
6//! The pattern: snapshot relevant state before CPI, invoke, then assert
7//! post-conditions. If the assertion fails, the instruction aborts before
8//! the corrupted state can be read by downstream logic.
9//!
10//! # Usage
11//!
12//! ```ignore
13//! use hopper_native::verify::{LamportSnapshot, verify_transfer};
14//!
15//! // Before CPI transfer:
16//! let snap = LamportSnapshot::capture(source, destination);
17//!
18//! // Do the CPI:
19//! system_transfer(&source, &destination, amount)?;
20//!
21//! // Verify the transfer actually happened correctly:
22//! snap.verify_transfer(source, destination, amount)?;
23//! ```
24//!
25//! This catches:
26//! - Called program transferring wrong amount
27//! - Called program not deducting from source
28//! - Called program crediting wrong destination
29//! - Integer overflow in lamport accounting
30
31use crate::account_view::AccountView;
32use crate::error::ProgramError;
33use crate::ProgramResult;
34
35// ---- Lamport snapshot ------------------------------------------------
36
37/// Snapshot of lamport balances for two accounts before a CPI.
38///
39/// Captures the source and destination balances so that after the CPI
40/// completes, we can verify the expected transfer occurred.
41#[derive(Clone, Copy, Debug)]
42pub struct LamportSnapshot {
43 source_before: u64,
44 destination_before: u64,
45}
46
47impl LamportSnapshot {
48 /// Capture the current lamport balances of source and destination.
49 #[inline(always)]
50 pub fn capture(source: &AccountView<'_>, destination: &AccountView<'_>) -> Self {
51 Self {
52 source_before: source.lamports(),
53 destination_before: destination.lamports(),
54 }
55 }
56
57 /// Verify that exactly `amount` lamports moved from source to destination.
58 ///
59 /// Checks:
60 /// 1. Source decreased by exactly `amount`
61 /// 2. Destination increased by exactly `amount`
62 /// 3. No overflow/underflow occurred
63 #[inline]
64 pub fn verify_transfer(
65 &self,
66 source: &AccountView<'_>,
67 destination: &AccountView<'_>,
68 amount: u64,
69 ) -> ProgramResult {
70 let source_after = source.lamports();
71 let dest_after = destination.lamports();
72
73 // Source must have decreased by exactly `amount`.
74 let source_delta = self
75 .source_before
76 .checked_sub(source_after)
77 .ok_or(ProgramError::ArithmeticOverflow)?;
78 if source_delta != amount {
79 return Err(ProgramError::InvalidAccountData);
80 }
81
82 // Destination must have increased by exactly `amount`.
83 let dest_delta = dest_after
84 .checked_sub(self.destination_before)
85 .ok_or(ProgramError::ArithmeticOverflow)?;
86 if dest_delta != amount {
87 return Err(ProgramError::InvalidAccountData);
88 }
89
90 Ok(())
91 }
92
93 /// Verify that the source decreased by exactly `amount` (one-sided check).
94 ///
95 /// Use this when the destination is a program-controlled escrow or
96 /// fee account where you only care about the deduction.
97 #[inline]
98 pub fn verify_deduction(&self, source: &AccountView<'_>, amount: u64) -> ProgramResult {
99 let delta = self
100 .source_before
101 .checked_sub(source.lamports())
102 .ok_or(ProgramError::ArithmeticOverflow)?;
103 if delta != amount {
104 return Err(ProgramError::InvalidAccountData);
105 }
106 Ok(())
107 }
108
109 /// Verify that neither balance changed (no-op CPI or read-only call).
110 #[inline]
111 pub fn verify_unchanged(
112 &self,
113 source: &AccountView<'_>,
114 destination: &AccountView<'_>,
115 ) -> ProgramResult {
116 if source.lamports() != self.source_before
117 || destination.lamports() != self.destination_before
118 {
119 return Err(ProgramError::InvalidAccountData);
120 }
121 Ok(())
122 }
123
124 /// Get the pre-CPI source balance.
125 #[inline(always)]
126 pub fn source_before(&self) -> u64 {
127 self.source_before
128 }
129
130 /// Get the pre-CPI destination balance.
131 #[inline(always)]
132 pub fn destination_before(&self) -> u64 {
133 self.destination_before
134 }
135}
136
137// ---- Single-account snapshot -----------------------------------------
138
139/// Snapshot of a single account's lamports for simple balance assertions.
140#[derive(Clone, Copy, Debug)]
141pub struct BalanceSnapshot {
142 before: u64,
143}
144
145impl BalanceSnapshot {
146 /// Capture a single account's lamport balance.
147 #[inline(always)]
148 pub fn capture(account: &AccountView<'_>) -> Self {
149 Self {
150 before: account.lamports(),
151 }
152 }
153
154 /// Verify the balance increased by at least `min_increase`.
155 #[inline]
156 pub fn verify_increased_by(
157 &self,
158 account: &AccountView<'_>,
159 min_increase: u64,
160 ) -> ProgramResult {
161 let current = account.lamports();
162 let delta = current
163 .checked_sub(self.before)
164 .ok_or(ProgramError::ArithmeticOverflow)?;
165 if delta < min_increase {
166 return Err(ProgramError::InsufficientFunds);
167 }
168 Ok(())
169 }
170
171 /// Verify the balance decreased by at most `max_decrease`.
172 #[inline]
173 pub fn verify_decreased_by_at_most(
174 &self,
175 account: &AccountView<'_>,
176 max_decrease: u64,
177 ) -> ProgramResult {
178 let current = account.lamports();
179 let delta = self
180 .before
181 .checked_sub(current)
182 .ok_or(ProgramError::ArithmeticOverflow)?;
183 if delta > max_decrease {
184 return Err(ProgramError::InsufficientFunds);
185 }
186 Ok(())
187 }
188
189 /// Verify the balance is unchanged.
190 #[inline]
191 pub fn verify_unchanged(&self, account: &AccountView<'_>) -> ProgramResult {
192 if account.lamports() != self.before {
193 return Err(ProgramError::InvalidAccountData);
194 }
195 Ok(())
196 }
197
198 /// Get the captured balance.
199 #[inline(always)]
200 pub fn before(&self) -> u64 {
201 self.before
202 }
203
204 /// Compute the net change (positive = gained, negative = lost).
205 ///
206 /// Returns the change as an i128 to avoid overflow.
207 #[inline(always)]
208 pub fn net_change(&self, account: &AccountView<'_>) -> i128 {
209 account.lamports() as i128 - self.before as i128
210 }
211}
212
213// ---- Data integrity snapshot -----------------------------------------
214
215/// Fast integrity check for account data using FNV-1a hash.
216///
217/// Use this to detect unexpected data mutations around CPI calls.
218/// Not cryptographically secure -- purely for integrity assertions.
219#[derive(Clone, Copy, Debug)]
220pub struct DataFingerprint {
221 hash: u64,
222 data_len: usize,
223}
224
225impl DataFingerprint {
226 /// Compute a fast fingerprint of the first `len` bytes of account data.
227 ///
228 /// Uses FNV-1a (fast, no dependencies, good collision resistance for
229 /// short inputs). Not suitable for cryptographic purposes.
230 #[inline]
231 pub fn capture(account: &AccountView<'_>, len: usize) -> Self {
232 let data_len = account.data_len().min(len);
233 let data_ptr = account.data_ptr_unchecked();
234
235 // FNV-1a hash.
236 let mut hash: u64 = 0xcbf29ce484222325;
237 let mut i = 0;
238 while i < data_len {
239 // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
240 let byte = unsafe { *data_ptr.add(i) };
241 hash ^= byte as u64;
242 hash = hash.wrapping_mul(0x100000001b3);
243 i += 1;
244 }
245
246 Self { hash, data_len }
247 }
248
249 /// Verify the data has not changed since the snapshot.
250 #[inline]
251 pub fn verify_unchanged(&self, account: &AccountView<'_>) -> ProgramResult {
252 let current = Self::capture(account, self.data_len);
253 if current.hash != self.hash || current.data_len != self.data_len {
254 return Err(ProgramError::InvalidAccountData);
255 }
256 Ok(())
257 }
258
259 /// Get the fingerprint hash.
260 #[inline(always)]
261 pub fn hash(&self) -> u64 {
262 self.hash
263 }
264}