Skip to main content

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}