LOPWDocumentation

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.

IDBreachEvidenceProven when
P1 EquivocationSeller signed two different receipts for one paymentTwo receiptsSame service_id and payment_reference, receipts differ in any field
P2 Terms mismatchReceipt cites the offer but different termsOffer and receiptreceipt.terms_hash ≠ offer.terms_hash
P3 Stale dataData older than promisedReceiptserved_at − data_as_of > max_data_age
P4 Bad statusCharged for a status the seller promised not to charge forReceiptstatus ∉ allowed_status
P5 Size limitResponse larger than promisedReceiptresponse_bytes > max_response_bytes
P6 Oracle deviationServed value differs from a named oracle observation beyond toleranceReceipt and the oracleFour 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

Terms
Receipt (sample from the test vectors)

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.

P1
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 receipt

The 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

P2
proven      ⇔  receipt.terms_hash != offer.terms_hash
not proven  ⇔  equal

The 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

P3
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_age

Edge 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

P4
proven      ⇔  status ∉ terms.allowed_status
not proven  ⇔  status ∈ terms.allowed_status

P5 Size limit

P5
proven      ⇔  response_bytes > terms.max_response_bytes
not proven  ⇔  response_bytes <= terms.max_response_bytes

P6 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:

  1. terms.oracle must be Some (else not proven) and receipt.oracle must be Some (else not verifiable). If the evidence names another oracle_contract or oracle_asset than the terms, the breach is proven as a terms mismatch.
  2. The validator calls price(oracle_asset, oracle_timestamp) on the oracle. No observation, or a price of zero or less, is not verifiable.
  3. The returned observation’s own timestamp must not be after served_at, and served_at − observed.timestamp must not exceed max_oracle_age; otherwise the breach is proven as stale.
  4. The deviation test uses exact integer math with no division, so no rounding can hide a breach:
P6 deviation test
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 field

Edge rules

Validator interface

contracts/types: Validator
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.