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