Skip to main content

sentri_core/
account_abstraction.rs

1//! Account Abstraction cross-layer invariant support.
2//!
3//! Supports phase-qualified invariants for ERC-4337 execution:
4//! - Validation Phase: validateUserOp, account signature checks
5//! - Execution Phase: account code execution, state mutations
6//! - Settlement Phase: bundles with other ops, fund transfers
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// Execution phases in the ERC-4337 UserOp lifecycle.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
13pub enum ExecutionPhase {
14    /// Validation phase: validateUserOp, signature checks, paymaster verification.
15    Validation,
16    /// Execution phase: account code runs, state mutations, call execution.
17    Execution,
18    /// Settlement phase: bundled with other ops, balances transferred.
19    Settlement,
20}
21
22impl ExecutionPhase {
23    /// Get string representation.
24    pub fn as_str(&self) -> &'static str {
25        match self {
26            Self::Validation => "validation",
27            Self::Execution => "execution",
28            Self::Settlement => "settlement",
29        }
30    }
31}
32
33impl std::str::FromStr for ExecutionPhase {
34    type Err = ();
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "validation" => Ok(Self::Validation),
39            "execution" => Ok(Self::Execution),
40            "settlement" => Ok(Self::Settlement),
41            _ => Err(()),
42        }
43    }
44}
45
46impl std::fmt::Display for ExecutionPhase {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.as_str())
49    }
50}
51
52/// Identifies different layers in account abstraction execution.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
54pub enum AALayer {
55    /// Bundler mempool and aggregation layer.
56    Bundler,
57    /// Account smart contract execution layer.
58    Account,
59    /// Optional paymaster sponsorship layer.
60    Paymaster,
61    /// Protocol and EntryPoint layer.
62    Protocol,
63    /// EntryPoint contract layer.
64    EntryPoint,
65}
66
67impl AALayer {
68    /// Get string representation.
69    pub fn as_str(&self) -> &'static str {
70        match self {
71            Self::Bundler => "bundler",
72            Self::Account => "account",
73            Self::Paymaster => "paymaster",
74            Self::Protocol => "protocol",
75            Self::EntryPoint => "entrypoint",
76        }
77    }
78}
79
80impl std::str::FromStr for AALayer {
81    type Err = ();
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        match s {
85            "bundler" => Ok(Self::Bundler),
86            "account" => Ok(Self::Account),
87            "paymaster" => Ok(Self::Paymaster),
88            "protocol" => Ok(Self::Protocol),
89            "entrypoint" => Ok(Self::EntryPoint),
90            _ => Err(()),
91        }
92    }
93}
94
95impl std::fmt::Display for AALayer {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", self.as_str())
98    }
99}
100
101/// Cross-layer context for account abstraction analysis.
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct AAContext {
104    /// Current execution phase (Validation, Execution, or Settlement).
105    pub current_phase: Option<ExecutionPhase>,
106
107    /// Layer-specific state variables: layer -> (variable -> value).
108    pub layer_state: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
109
110    /// Phase-specific state snapshots for cross-phase analysis: phase -> layer_state.
111    pub phase_snapshots: BTreeMap<String, BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
112
113    /// UserOperation data from bundler layer.
114    pub user_op: Option<UserOpData>,
115
116    /// Account state from account layer.
117    pub account_state: Option<AccountState>,
118
119    /// Paymaster state from paymaster layer (if applicable).
120    pub paymaster_state: Option<PaymasterState>,
121
122    /// EntryPoint state from protocol layer.
123    pub entry_point_state: Option<EntryPointState>,
124}
125
126impl AAContext {
127    /// Set the current execution phase.
128    pub fn set_phase(&mut self, phase: ExecutionPhase) {
129        self.current_phase = Some(phase);
130    }
131
132    /// Get the current execution phase.
133    pub fn get_phase(&self) -> Option<ExecutionPhase> {
134        self.current_phase
135    }
136
137    /// Check if currently in a specific phase.
138    pub fn in_phase(&self, phase: ExecutionPhase) -> bool {
139        self.current_phase == Some(phase)
140    }
141
142    /// Snapshot layer state at current phase.
143    pub fn snapshot_phase(&mut self, phase: ExecutionPhase) {
144        self.phase_snapshots
145            .insert(phase.to_string(), self.layer_state.clone());
146    }
147
148    /// Get snapshots from a specific phase for comparison.
149    pub fn get_phase_snapshot(
150        &self,
151        phase: ExecutionPhase,
152    ) -> Option<&BTreeMap<String, BTreeMap<String, serde_json::Value>>> {
153        self.phase_snapshots.get(phase.as_str())
154    }
155
156    /// Get variable value from a specific layer.
157    pub fn get_layer_var(&self, layer: &str, var: &str) -> Option<&serde_json::Value> {
158        self.layer_state.get(layer)?.get(var)
159    }
160
161    /// Set variable value in a specific layer.
162    pub fn set_layer_var(&mut self, layer: String, var: String, value: serde_json::Value) {
163        self.layer_state
164            .entry(layer)
165            .or_default()
166            .insert(var, value);
167    }
168
169    /// Get variable value from a specific layer at a specific phase.
170    pub fn get_layer_var_at_phase(
171        &self,
172        phase: ExecutionPhase,
173        layer: &str,
174        var: &str,
175    ) -> Option<&serde_json::Value> {
176        self.phase_snapshots
177            .get(phase.as_str())?
178            .get(layer)?
179            .get(var)
180    }
181}
182
183/// UserOperation data from bundler layer.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct UserOpData {
186    /// Account address.
187    pub sender: String,
188    /// Operation nonce.
189    pub nonce: u128,
190    /// Init code if deploying new account.
191    pub init_code: Vec<u8>,
192    /// Call data.
193    pub call_data: Vec<u8>,
194    /// Call gas limit.
195    pub call_gas_limit: u128,
196    /// Verification gas limit.
197    pub verification_gas_limit: u128,
198    /// Pre-operation gas.
199    pub pre_op_gas: u128,
200    /// Maximum gas price account will accept.
201    pub max_gas_price: u128,
202    /// Maximum priority gas price.
203    pub max_priority_fee_per_gas: u128,
204    /// Paymaster address (if applicable).
205    pub paymaster_and_data: Vec<u8>,
206    /// Signature data.
207    pub signature: Vec<u8>,
208}
209
210/// Account smart contract state.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct AccountState {
213    /// Account nonce value.
214    pub nonce: u128,
215    /// Account balance.
216    pub balance: u128,
217    /// Expected signer address.
218    pub expected_signer: String,
219    /// Whether signature is valid.
220    pub signature_valid: bool,
221    /// Reentrancy guard state.
222    pub reentrancy_locked: bool,
223    /// Execution failed flag.
224    pub execution_failed: bool,
225    /// State hash before execution.
226    pub state_hash_before: String,
227    /// State hash after execution.
228    pub state_hash_after: String,
229}
230
231/// Paymaster contract state (optional).
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct PaymasterState {
234    /// Paymaster address.
235    pub address: String,
236    /// Deposit amount.
237    pub deposit: u128,
238    /// Paymaster nonce.
239    pub nonce: u128,
240    /// Verification status.
241    pub status: String,
242}
243
244/// EntryPoint contract state.
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct EntryPointState {
247    /// EntryPoint address.
248    pub address: String,
249    /// Current block number.
250    pub block_number: u128,
251    /// Current block timestamp.
252    pub block_timestamp: u128,
253    /// Authenticated caller (msg.sender during handleOps).
254    pub authenticated_caller: String,
255}
256
257/// Cross-layer invariant check result.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct CrossLayerCheckResult {
260    /// Invariant name.
261    pub invariant_name: String,
262    /// Layers involved in this check.
263    pub layers_involved: Vec<String>,
264    /// Whether the invariant holds.
265    pub holds: bool,
266    /// Detailed reason if invariant fails.
267    pub failure_reason: Option<String>,
268    /// Variables used in evaluation.
269    pub variables_used: BTreeMap<String, serde_json::Value>,
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use std::str::FromStr;
276
277    #[test]
278    fn test_execution_phase_from_str() {
279        assert_eq!(
280            ExecutionPhase::from_str("validation"),
281            Ok(ExecutionPhase::Validation)
282        );
283        assert_eq!(
284            ExecutionPhase::from_str("execution"),
285            Ok(ExecutionPhase::Execution)
286        );
287        assert_eq!(
288            ExecutionPhase::from_str("settlement"),
289            Ok(ExecutionPhase::Settlement)
290        );
291        assert_eq!(ExecutionPhase::from_str("invalid"), Err(()));
292    }
293
294    #[test]
295    fn test_aa_layer_from_str() {
296        assert_eq!(AALayer::from_str("bundler"), Ok(AALayer::Bundler));
297        assert_eq!(AALayer::from_str("account"), Ok(AALayer::Account));
298        assert_eq!(AALayer::from_str("paymaster"), Ok(AALayer::Paymaster));
299        assert_eq!(AALayer::from_str("protocol"), Ok(AALayer::Protocol));
300        assert_eq!(AALayer::from_str("invalid"), Err(()));
301    }
302
303    #[test]
304    fn test_aa_context_layer_vars() {
305        let mut ctx = AAContext::default();
306        ctx.set_layer_var(
307            "bundler".to_string(),
308            "nonce".to_string(),
309            serde_json::json!(42),
310        );
311
312        let value = ctx.get_layer_var("bundler", "nonce");
313        assert_eq!(value, Some(&serde_json::json!(42)));
314    }
315
316    #[test]
317    fn test_phase_tracking() {
318        let mut ctx = AAContext::default();
319        assert_eq!(ctx.get_phase(), None);
320
321        ctx.set_phase(ExecutionPhase::Validation);
322        assert!(ctx.in_phase(ExecutionPhase::Validation));
323        assert!(!ctx.in_phase(ExecutionPhase::Execution));
324
325        ctx.set_layer_var(
326            "account".to_string(),
327            "balance".to_string(),
328            serde_json::json!(1000),
329        );
330        ctx.snapshot_phase(ExecutionPhase::Validation);
331
332        ctx.set_phase(ExecutionPhase::Execution);
333        ctx.set_layer_var(
334            "account".to_string(),
335            "balance".to_string(),
336            serde_json::json!(500),
337        );
338
339        let pre_exec_balance =
340            ctx.get_layer_var_at_phase(ExecutionPhase::Validation, "account", "balance");
341        assert_eq!(pre_exec_balance, Some(&serde_json::json!(1000)));
342    }
343}