Expand description
Breakwater core library.
Stateful property testing for Soroban contracts. A suite declares actors
(who may call), handlers (what may be called) and invariants (what must
always hold). Breakwater generates randomized call sequences against an
in-process soroban_sdk::Env, checks every invariant after every step,
shrinks any failing sequence to a minimal reproduction and exports it.
§Example
A counter contract whose count must never exceed 2. The suite calls
increment in random sequences, finds the violation and shrinks it to the
three increments that cause it.
use breakwater::proptest::prelude::*;
use breakwater::{
Actor, CallContext, CallOutcome, Handler, HandlerError, Invariant, Suite, TxLog, Violation,
};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol};
const COUNT: Symbol = symbol_short!("COUNT");
#[contract]
pub struct Counter;
#[contractimpl]
impl Counter {
pub fn increment(env: Env) -> u32 {
let n: u32 = env.storage().instance().get(&COUNT).unwrap_or(0) + 1;
env.storage().instance().set(&COUNT, &n);
n
}
pub fn get(env: Env) -> u32 {
env.storage().instance().get(&COUNT).unwrap_or(0)
}
}
// A handler describes one callable entry point and the arguments to feed it.
struct Increment {
contract: String,
actors: Vec<Actor>,
}
impl Handler for Increment {
type Args = ();
fn name(&self) -> &'static str {
"increment"
}
fn actors(&self) -> &[Actor] {
&self.actors
}
fn strategy(&self) -> BoxedStrategy<()> {
Just(()).boxed()
}
fn call(
&self,
env: &Env,
_ctx: &CallContext,
_actor: &Actor,
_args: &(),
) -> Result<CallOutcome, HandlerError> {
let id = Address::from_str(env, &self.contract);
match CounterClient::new(env, &id).try_increment() {
Ok(Ok(count)) => Ok(CallOutcome::returning(count)),
Ok(Err(e)) => Err(HandlerError::Other(format!("conversion error: {e:?}"))),
Err(Ok(e)) => Ok(CallOutcome::ContractError(e.get_code())),
Err(Err(e)) => Err(HandlerError::Host(format!("{e:?}"))),
}
}
}
// An invariant is checked after every step of every sequence.
struct CountAtMostTwo {
contract: String,
}
impl Invariant for CountAtMostTwo {
fn name(&self) -> &'static str {
"count_at_most_two"
}
fn check(&self, env: &Env, _log: &TxLog) -> Result<(), Violation> {
let id = Address::from_str(env, &self.contract);
let count = CounterClient::new(env, &id).get();
if count > 2 {
return Err(Violation::new("count exceeded 2").with_detail(count));
}
Ok(())
}
}
fn main() {
let env = Env::default();
let contract = env.register(Counter, ()).to_string().to_string();
let owner = Actor::generate(&env, "owner");
let restore_id = contract.clone();
let report = Suite::new("counter", env)
.seed(42)
.steps(1..=8)
.cases(16)
.handler(Increment { contract: contract.clone(), actors: vec![owner] })
.invariant(CountAtMostTwo { contract })
// Re-registers the contract whenever the runner restores a snapshot.
.on_restore(move |env| {
env.register_at(&Address::from_str(env, &restore_id), Counter, ());
})
.run()
.expect("the suite runs");
let failure = report.failure().expect("the third increment breaks the invariant");
assert_eq!(failure.violation.source, "count_at_most_two");
assert_eq!(failure.log.len(), 3, "shrunk to three increments");
}Re-exports§
pub use proptest;
Modules§
- auth
- Building blocks for real Soroban authorization entries.
Structs§
- Actor
- A named signer with a Soroban address.
- Call
Context - Per-step information a handler may need to build real authorization.
- Exporter
- Serialises
RunReports. - Failure
- A shrunk failing sequence.
- Gap
- How far the ledger moved before a step.
- Replay
Step - One step of a recorded sequence, by name rather than by index, so it can be stored in a file and replayed against a freshly built suite.
- RunReport
- Result of
Suite::run. - Step
- One generated call: which handler, which of its actors, with what arguments, after what ledger gap.
- Step
Args - Type-erased handler arguments carried inside a generated step.
- Step
Record - One executed step.
- Suite
- A stateful property test for a set of Soroban contracts.
- Suite
Config - Knobs for one
crate::Suiterun. - TxLog
- The steps executed so far in the current sequence, oldest first.
- Violation
- A violated invariant, or a step that failed in a way that counts as a bug.
Enums§
- Auth
Mode - How authorization is handled while executing steps.
- Call
Outcome - Result of a handler call that completed without a bug.
- Handler
Error - A handler call failed for a reason other than a graceful contract error.
- Ledger
Advance - Policy for moving
env.ledger()before each step. - Outcome
- Whether all invariants held.
- Replay
Error - Why a recorded step could not be mapped onto the suite.
- Step
Outcome - What happened when a step was executed.
- Suite
Error - Errors returned by
crate::Suite::runand the exporter. - Violation
Kind - Why a run failed.
Constants§
- LEDGERS_
PER_ DAY - Ledgers in one day at
SECONDS_PER_LEDGER. - SECONDS_
PER_ LEDGER - Stellar’s approximate ledger close time, used to derive timestamps from ledger counts.
Traits§
- Handler
- One contract function the fuzzer may call.
- Invariant
- A property checked after every step of every generated sequence.
Functions§
- parse_
steps - Parses
A..=B(or a singleN, meaningN..=N).