# Predicates P1 to P6

> The six LOPW breach conditions with their exact proven conditions and edge rules (data_as_of 0, oracle tick rounding, the exact deviation test), the validator interface, and an interactive explorer.

*LOPW documentation · Protocol reference.* Canonical page: https://lopw.com/docs/protocol/predicates.html · This Markdown twin: https://lopw.com/docs/protocol/predicates.md · Index of all pages: https://lopw.com/llms.txt · Everything in one file: https://lopw.com/llms-full.txt

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](#p6) 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](https://lopw.com/docs/protocol/claims.html#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 | Proven when | Not proven / not verifiable when |
| --- | --- | --- |
| P1 | `extra` holds a second receipt with the same `service_id` and `payment_reference` whose fields differ in any way (both signatures already verified by Claims) | No second receipt, different payment, or an identical receipt |
| P2 | `receipt.terms_hash != offer.terms_hash` | Hashes equal |
| P3 | `served_at - data_as_of > max_data_age` | `data_as_of == 0` (not applicable) or `data_as_of > served_at`: not proven |
| P4 | `status` not in `allowed_status` | Status is listed |
| P5 | `response_bytes > max_response_bytes` | At or below the limit |
| P6 | Evidence names another oracle or asset; or the returned tick is after `served_at` or older than `max_oracle_age`; or `\|reported_value - oracle_price\| * 10000 > max_deviation_bps * oracle_price` | No evidence block, no observation, or non-positive price: not verifiable; within tolerance: not proven |

## 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*

```text
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*

```text
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*

```text
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*

```text
proven      ⇔  status ∉ terms.allowed_status
not proven  ⇔  status ∈ terms.allowed_status
```

## P5 Size limit

*P5*

```text
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*

```text
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

- **Tick rounding.** `price(asset, t)` rounds `t` down to the previous tick and returns that tick’s own timestamp; `t = tick + 100` and `t = tick` return 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 > bps` rounds 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_price` is 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

*contracts/types: Validator*

```rust
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.
