Skip to main content

breakwater/
handler.rs

1//! Handlers: the contract calls a suite may generate.
2
3use std::any::Any;
4use std::fmt::Debug;
5use std::sync::Arc;
6
7use proptest::prelude::*;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10use soroban_sdk::Env;
11use thiserror::Error;
12
13use crate::actor::Actor;
14use crate::config::AuthMode;
15
16/// Result of a handler call that completed without a bug.
17///
18/// Both variants are *expected* behaviour: a call may succeed or fail
19/// gracefully with a `contracterror` code. Anything else (a host error, a
20/// conversion error, a bare panic) is reported as a [`HandlerError`] or a
21/// panic and treated as a bug.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum CallOutcome {
24    /// The call succeeded and returned nothing worth recording.
25    Success,
26    /// The call succeeded; the value is the contract's return value in JSON
27    /// form and is recorded in the step log and the exported report.
28    SuccessWith(serde_json::Value),
29    /// The contract rejected the call with the given `contracterror` code.
30    ContractError(u32),
31    /// The host rejected the call's authorization (a policy or signer check
32    /// failed). Expected behaviour when fuzzing authorization.
33    Unauthorized,
34}
35
36impl CallOutcome {
37    /// A successful outcome carrying `value` as the recorded return value.
38    pub fn returning(value: impl Serialize) -> Self {
39        match serde_json::to_value(value) {
40            Ok(v) => CallOutcome::SuccessWith(v),
41            Err(_) => CallOutcome::Success,
42        }
43    }
44
45    /// True for [`CallOutcome::Success`] and [`CallOutcome::SuccessWith`].
46    pub fn is_success(&self) -> bool {
47        matches!(self, CallOutcome::Success | CallOutcome::SuccessWith(_))
48    }
49}
50
51/// Per-step information a handler may need to build real authorization.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct CallContext {
54    /// Zero-based index of the step in the sequence.
55    pub step: usize,
56    /// Authorization mode of the running suite.
57    pub auth: AuthMode,
58}
59
60impl CallContext {
61    /// A nonce that is unique per step, so one address can authorize one call
62    /// per step without nonce reuse.
63    pub fn nonce(&self) -> i64 {
64        self.step as i64 + 1
65    }
66
67    /// A signature expiration ledger comfortably ahead of the current one.
68    pub fn signature_expiration_ledger(&self, env: &Env) -> u32 {
69        env.ledger().sequence().saturating_add(100)
70    }
71}
72
73/// A handler call failed for a reason other than a graceful contract error.
74#[derive(Debug, Error)]
75pub enum HandlerError {
76    /// The host returned an error that is not a contract error.
77    #[error("host error: {0}")]
78    Host(String),
79    /// The runner passed arguments of the wrong type; indicates a framework bug.
80    #[error("argument type mismatch for handler `{0}`")]
81    ArgsMismatch(&'static str),
82    /// Any other failure the handler wants to surface as a bug.
83    #[error("{0}")]
84    Other(String),
85}
86
87/// One contract function the fuzzer may call.
88///
89/// Implementations wrap a single contract entry point: they declare which
90/// actors may call it, how to generate arguments, and how to perform the call
91/// against an [`Env`]. Use the generated `try_` client methods inside
92/// [`Handler::call`] so that contract errors become
93/// [`CallOutcome::ContractError`] instead of panics.
94pub trait Handler: Send + Sync + 'static {
95    /// Generated arguments for one call.
96    type Args: Clone + Debug + Serialize + DeserializeOwned + Send + Sync + 'static;
97
98    /// Name used in logs and exports; usually the contract function name.
99    fn name(&self) -> &'static str;
100
101    /// Actors allowed to perform this call. Must not be empty.
102    fn actors(&self) -> &[Actor];
103
104    /// proptest strategy for the arguments.
105    fn strategy(&self) -> BoxedStrategy<Self::Args>;
106
107    /// Rebuilds arguments from their logged JSON form, for replaying an
108    /// exported sequence. The default uses `serde_json`.
109    fn args_from_json(&self, value: &serde_json::Value) -> Option<Self::Args> {
110        serde_json::from_value(value.clone()).ok()
111    }
112
113    /// Performs the call as `actor` with `args`.
114    ///
115    /// In [`AuthMode::Real`] the handler must install the authorization
116    /// entries the call needs with `env.set_auths(..)` before invoking; see
117    /// [`crate::auth`].
118    fn call(
119        &self,
120        env: &Env,
121        ctx: &CallContext,
122        actor: &Actor,
123        args: &Self::Args,
124    ) -> Result<CallOutcome, HandlerError>;
125}
126
127/// Type-erased handler arguments carried inside a generated step.
128#[derive(Clone)]
129pub struct StepArgs(Arc<dyn ErasedArgs>);
130
131impl StepArgs {
132    /// JSON rendering of the arguments, used by the log and the exporter.
133    pub fn to_json(&self) -> serde_json::Value {
134        self.0.to_json()
135    }
136
137    fn downcast<T: 'static>(&self) -> Option<&T> {
138        self.0.as_any().downcast_ref::<T>()
139    }
140}
141
142impl Debug for StepArgs {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        self.0.fmt(f)
145    }
146}
147
148trait ErasedArgs: Debug + Send + Sync + 'static {
149    fn as_any(&self) -> &dyn Any;
150    fn to_json(&self) -> serde_json::Value;
151}
152
153impl<T> ErasedArgs for T
154where
155    T: Clone + Debug + Serialize + Send + Sync + 'static,
156{
157    fn as_any(&self) -> &dyn Any {
158        self
159    }
160
161    fn to_json(&self) -> serde_json::Value {
162        serde_json::to_value(self)
163            .unwrap_or_else(|_| serde_json::Value::String(format!("{self:?}")))
164    }
165}
166
167/// Object-safe view of a [`Handler`], used internally by the suite.
168pub(crate) trait DynHandler: Send + Sync {
169    fn name(&self) -> &'static str;
170    fn actors(&self) -> &[Actor];
171    fn strategy(&self) -> BoxedStrategy<StepArgs>;
172    fn args_from_json(&self, value: &serde_json::Value) -> Option<StepArgs>;
173    fn call(
174        &self,
175        env: &Env,
176        ctx: &CallContext,
177        actor: &Actor,
178        args: &StepArgs,
179    ) -> Result<CallOutcome, HandlerError>;
180}
181
182impl<H: Handler> DynHandler for H {
183    fn name(&self) -> &'static str {
184        Handler::name(self)
185    }
186
187    fn actors(&self) -> &[Actor] {
188        Handler::actors(self)
189    }
190
191    fn strategy(&self) -> BoxedStrategy<StepArgs> {
192        Handler::strategy(self)
193            .prop_map(|args| StepArgs(Arc::new(args)))
194            .boxed()
195    }
196
197    fn args_from_json(&self, value: &serde_json::Value) -> Option<StepArgs> {
198        Handler::args_from_json(self, value).map(|args| StepArgs(Arc::new(args)))
199    }
200
201    fn call(
202        &self,
203        env: &Env,
204        ctx: &CallContext,
205        actor: &Actor,
206        args: &StepArgs,
207    ) -> Result<CallOutcome, HandlerError> {
208        let args = args
209            .downcast::<H::Args>()
210            .ok_or_else(|| HandlerError::ArgsMismatch(Handler::name(self)))?;
211        Handler::call(self, env, ctx, actor, args)
212    }
213}