Docs / Protocol reference
Predicates P1 to P6
A predicate is one breach condition that a contract can decide from the evidence alone. Each service’s terms list which predicates apply and with what limits. Each predicate is a separate validator contract with one function; the Claims contract keeps an allowlist of validators by predicate ID.
| ID | Breach | Evidence | Proven when |
|---|---|---|---|
| P1 Equivocation | Seller signed two different receipts for one payment | Two receipts | Same service_id and payment_reference, receipts differ in any field |
| P2 Terms mismatch | Receipt cites the offer but different terms | Offer and receipt | receipt.terms_hash ≠ offer.terms_hash |
| P3 Stale data | Data older than promised | Receipt | served_at − data_as_of > max_data_age |
| P4 Bad status | Charged for a status the seller promised not to charge for | Receipt | status ∉ allowed_status |
| P5 Size limit | Response larger than promised | Receipt | response_bytes > max_response_bytes |
| P6 Oracle deviation | Served value differs from a named oracle observation beyond tolerance | Receipt and the oracle | Four checks ending in an exact integer deviation test |
P1 to P5 rest on what the seller signed: they catch a seller that contradicts itself or admits a breach in its own receipt. P6 is checked against evidence the seller does not control. Before any validator runs, the Claims contract has already verified both signatures, the offer binding, the claim window, replay and that the predicate is part of the terms (claim checks).
Interactive explorer
Pick a predicate, adjust the sample receipt and terms, and see how the validator would decide. The logic is the same as the contracts and as the buyer SDK’s local evaluation.
Predicate explorer
P1 Equivocation
Evidence: the claimed receipt plus a second signed receipt in extra = SecondReceipt(second). The Claims contract verifies the second receipt’s signature with the same key rules (including KeyRetired) before calling the validator, so the validator stays a pure function of its inputs.
proven ⇔ second.service_id == receipt.service_id
∧ second.payment_reference == receipt.payment_reference
∧ second != receipt // any field differs
not proven ⇔ extra is None, or a different payment, or an identical receiptThe reference services’ DUPLICATE_RECEIPT fault returns a second receipt with a different response_hash under PAYMENT-RESPONSE.extensions.lopw.duplicate; the buyer SDK stores it as warranty.duplicate and attaches it to a P1 claim automatically.
P2 Terms mismatch
proven ⇔ receipt.terms_hash != offer.terms_hash
not proven ⇔ equalThe Claims contract has already required receipt.offer_hash to equal the hash of the supplied offer and loaded the terms by offer.terms_hash, so P2 compares the terms the buyer paid under with the terms the seller claims to have served under.
P3 Stale data
not proven ⇔ data_as_of == 0 // "not applicable" never proves a breach
∨ data_as_of > served_at // no negative ages
proven ⇔ served_at - data_as_of > max_data_ageEdge rule: data_as_of = 0 means not applicable. Without this rule served_at − 0 would always exceed any limit. A seller that promises P3 and reports 0 is not slashable through P3, but the buyer SDK treats a P3 service reporting 0 as an untrusted response, so policy-driven agents stop paying it.
P4 Bad status
proven ⇔ status ∉ terms.allowed_status
not proven ⇔ status ∈ terms.allowed_statusP5 Size limit
proven ⇔ response_bytes > terms.max_response_bytes
not proven ⇔ response_bytes <= terms.max_response_bytesP6 Oracle deviation
P6 compares the value the seller served with one specific past observation of a SEP-40 price oracle, never with the price at claim time. The terms name the oracle contract, the asset as the oracle names it, a tolerance in basis points and a maximum observation age.
Seller side. Before quoting, the middleware reads the oracle’s last completed observation (lastprice) and records the timestamp the oracle returned. The receipt carries that timestamp, the oracle price seen and the value served, in the oracle’s own decimals.
Validator checks, in order:
terms.oraclemust beSome(else not proven) andreceipt.oraclemust beSome(else not verifiable). If the evidence names anotheroracle_contractororacle_assetthan the terms, the breach is proven as a terms mismatch.- The validator calls
price(oracle_asset, oracle_timestamp)on the oracle. No observation, or a price of zero or less, is not verifiable. - The returned observation’s own timestamp must not be after
served_at, andserved_at − observed.timestampmust not exceedmax_oracle_age; otherwise the breach is proven as stale. - The deviation test uses exact integer math with no division, so no rounding can hide a breach:
proven ⇔ |reported_value − oracle_price| × 10000 > max_deviation_bps × oracle_price
not proven ⇔ otherwise // oracle_price is the value the validator read, not the receipt's fieldEdge rules
- Tick rounding.
price(asset, t)roundstdown to the previous tick and returns that tick’s own timestamp;t = tick + 100andt = tickreturn the same record. The validator applies the age check to the returned timestamp, not the cited one, so citing an off-tick time cannot dodge a claim. - Retention. The reference fiat-rates oracle keeps 24 hours of history at 5-minute resolution with 14 decimals, quoting USD per unit of the asset. A P6 claim window must be shorter than the retention, which the registry enforces with
max_oracle_claim_window(1 hour). - Exactness. The division form
|r − p| × 10000 / p > bpsrounds down and would let a 50.99 bps deviation pass a 50 bps limit; the multiplication form is used instead. Arithmetic overflow, which needs absurd values far outside any tolerance, counts as proven. - Trust. The receipt’s
oracle_priceis informational. The oracle itself is trusted: a wrong observation produces a wrong outcome. A validator or oracle call that fails outright counts as not verifiable, which pays nothing and penalises no one. - Cost. A contract-to-contract oracle read from the validator carried no charge beyond the normal transaction fee on the first live P6 claim.
Validator interface
pub trait Validator {
fn validate(env: Env, terms: Terms, offer: Offer, receipt: Receipt, extra: ClaimExtra) -> Result<(), ValidationError>;
}
// Ok(()) breach proven
// Err(NotProven = 1) evidence valid, no breach
// Err(NotVerifiable = 2) the breach cannot be checked (P6: no oracle evidence or observation)The Claims contract calls the validator through try_validate. Ok(()) is proven; Err(NotProven) is recorded as Rejected(1); any other failure, including a trapped oracle call, is recorded as Rejected(2). evaluatePredicate(id, evidence) in @lopw/protocol mirrors every rule so a buyer knows before paying fees whether a claim will be paid.
Not slashable
Not provable against the seller
An invalid signature, a missing receipt, and a receipt citing an offer the buyer never saw cannot be proven against the seller: anyone could produce them. The buyer SDK flags each as an untrusted response and reports it; no claim is possible. A claim carrying a bad signature fails with no penalty to the seller.