dig-options 0.3.0

The DIG Network canonical Chia option-contract (CHIP-0042) expert crate: a pure, key-free, network-free CoinSpend-builder for covered options — create, exercise, clawback/cancel, inspect. Never holds a key, never signs, never broadcasts — the consumer signs + pushes.
Documentation
# dig-options — normative specification

`dig-options` is the DIG Network canonical builder library for Chia covered-option contracts. It
constructs the exact `CoinSpend`s for the option lifecycle and reports the signatures a caller must
produce. This document is the authoritative contract; an independent reimplementation can be built
against it.

## 1. Scope

dig-options covers the Chia covered-option primitive (CHIP-0042): a singleton "option ticket" plus a
separate underlying coin that holds a locked asset under a 1-of-2 path — **exercise** (the ticket
holder pays the strike asset into the settlement puzzle and unlocks the underlying, valid strictly
before expiry) and **clawback** (strictly after expiry, the creator reclaims the locked underlying).

**v0.1.0 scope:**

- **Underlying: XCH.** The locked asset is XCH; the underlying coin is a plain XCH coin at the option's
  1-of-2 path.
- **Strike: XCH only (v0.1.0).** `create` REJECTS a non-XCH strike up front (§6), so create and
  exercise have symmetric support envelopes — a holder can never acquire an option it could never
  exercise. `parse`/`parse_child` still inspect any strike type curried into an existing option.
  **Exercise builds the full settlement legs for an XCH strike** (both the underlying-claim leg to the
  holder and the strike-payment leg to the creator); its non-XCH guard remains as defense-in-depth.
  CAT/revocable-CAT/NFT strike (create + exercise) lands with the CAT/NFT follow-up (#1254).
- **Future extension (stated positively):** CAT / revocable-CAT / NFT underlyings and CAT/NFT strike
  exercise use the same `OptionUnderlying::exercise_spend` / `clawback_spend` primitives wrapped for the
  asset; they are additive and land in a later minor version.

**v0.2.0 additions (additive):**

- **`transfer`** — move the option ticket to a new owner (§5.6). Additive; the mint/exercise/clawback
  envelope is unchanged.
- **Rehydration (`rehydrate` + `parse_metadata`)** — reconstruct a full, operable `CreatedOption` from
  on-chain state, verified against the option's commitments (§5.7). This lifts the §5.4 recoverable-fields
  limitation for a caller willing to supply the creator puzzle hash: `parse` still only inverts identity
  fields, but `rehydrate` reconstructs + VERIFIES the full terms, so a caller can operate an option it did
  not mint in the same session.

## 2. Custody invariants (HARD)

These are the crate's defining properties and MUST hold for every operation:

1. **Key-free.** No function accepts, holds, derives, or stores a secret key. A creator/holder is
   expressed as an `Owner` (a public key or a borrowed inner spender) and explicit `Bytes32` puzzle
   hashes, never a secret. No `IndexedKeys`, no synthetic secret key.
2. **Never signs.** No function produces a `Signature`. The only signing-adjacent surface is
   `required_signatures`, which REPORTS the BLS messages a caller must sign; the caller signs and
   aggregates.
3. **Network-free.** No function performs I/O. Every coin and parent spend a builder needs is fetched by
   the caller and passed in.

A build produces unsigned `CoinSpend`s appended to a caller-owned `SpendContext`. The caller signs the
reported messages, assembles a `SpendBundle`, and broadcasts.

## 3. The identity boundary (#908)

dig-options is identity-agnostic. It references parties purely by public key and puzzle hash — it NEVER
constructs, spends, or holds a DID coin or key, and depends on NO DIG identity crate. The user key stays
entirely on the caller's side of the boundary.

## 4. Public types

### `Owner<'a>`
The p2 layer that authorizes an inner spend, without a secret.
- `Standard(PublicKey)` — the standard `p2_delegated_puzzle_or_hidden_puzzle` layer, identified by its
  BLS public key.
- `Custom(&'a dyn SpendWithConditions)` — any layer implementing `SpendWithConditions` (multisig, custom
  p2), borrowed for the build. Supported by every builder (create, exercise, clawback).
- `standard_puzzle_hash() -> Option<Bytes32>` — the standard p2 puzzle hash for `Standard`; `None` for
  `Custom` (used by the clawback guard, §5.3).
- Implements `SpendWithConditions` by routing to the concrete layer.

### `OptionTerms`
- `creator_puzzle_hash: Bytes32` — where the creator reclaims the underlying on clawback.
- `owner_puzzle_hash: Bytes32` — the option singleton's initial holder/owner.
- `underlying_amount: u64` — XCH mojos locked as the underlying.
- `strike_type: OptionType` — the asset + amount the holder must pay to exercise.
- `expiry_seconds: u64` — absolute unix seconds; exercise valid strictly before, clawback strictly after.
- `new(creator_puzzle_hash, underlying_amount, strike_type, expiry_seconds)` — sets `owner_puzzle_hash`
  = `creator_puzzle_hash` (the self-minted case). Use the struct literal to mint to a different owner.

### `CreatedOption`
The confirmed-option handle the caller retains to operate the option later.
- `option: OptionContract` — the option singleton.
- `underlying: OptionUnderlying` — the underlying terms (launcher id, creator ph, seconds, amount,
  strike type).
- `underlying_coin: Coin` — the locked-underlying XCH coin.

### `StrikePayment`
- `funding_coin: Coin` — the caller-supplied XCH coin the holder spends to fund the strike; must hold at
  least the strike amount.

### `OptionSpend`
- `coin_spends: Vec<CoinSpend>` — the unsigned spends produced.
- `created: Option<CreatedOption>``Some` for `create`; `None` for `exercise`/`clawback`.

### `ParsedOption`
The identity fields recoverable from an option coin spend (§5.4): `option`, `launcher_id`, `coin_id`,
`underlying_coin_id`, `underlying_delegated_puzzle_hash`, `p2_puzzle_hash`.

### `RehydratedTerms` (v0.2.0)
The terms a caller supplies to `rehydrate` (§5.7); each is VERIFIED against the option's on-chain
commitments, never trusted blindly.
- `creator_puzzle_hash: Bytes32` — the clawback destination the caller recorded at mint (committed only
  inside the underlying's clawback path, so it is supplied + verified rather than inverted).
- `expiry_seconds: u64` — recoverable from the launcher metadata via `parse_metadata`.
- `strike_type: OptionType` — recoverable from the launcher metadata via `parse_metadata`.

### `OptionMetadata` (v0.2.0, re-exported)
The launcher key-value metadata `parse_metadata` recovers: `expiration_seconds: u64`,
`strike_type: OptionType`.

## 5. Operations

### 5.1 `create(ctx, creator, funding_coin, terms) -> OptionSpend`
Locks `terms.underlying_amount` XCH and mints the option singleton to `terms.owner_puzzle_hash`.
- **Emitted spends:** one `funding_coin` spend (through `creator`) that creates the launcher coin and the
  locked-underlying coin, plus the launcher/eve option spends.
- **Enforced invariants:** `terms.strike_type` is `Xch` (v0.1.0; else error, §6, same shape as the
  exercise guard); `underlying_amount > 0`; `funding_coin.amount >= underlying_amount + 1`
  (underlying + the 1-mojo singleton), computed with a checked add (overflow → error). Excess is an
  implicit fee.
- **Returns** `created: Some(..)` — the handle for exercise/clawback.

### 5.2 `exercise(ctx, holder, created, strike) -> OptionSpend`
Spends the option singleton through its exercise path and builds BOTH settlement legs in one bundle:
the unlocked underlying — which the exercise-path puzzle emits onto a bare settlement coin — is claimed
to the holder (the option's current `p2_puzzle_hash`) via a `SettlementLayer` spend paying the full
underlying amount, and the XCH strike is paid into the settlement puzzle and settled to the creator's
requested payment.
- **Enforced invariants:** `created.underlying.strike_type` is `Xch` (else error, §6); `strike.funding_coin.amount`
  ≥ the strike amount; the exercise's `AssertBeforeSecondsAbsolute(expiry)` boundary is enforced by the
  consensus (valid strictly before expiry).
- **Value conservation:** the underlying is CLAIMED to the holder in the same bundle (no bare settlement
  coin holding the underlying survives — nothing is left for a key-free thief); the strike is paid to the
  creator's requested payment. No value is created.
- **Builder-enforced underlying claim:** the returned `coin_spends` carry a settlement spend that claims
  the underlying to the holder. **Consensus enforces the strike payment to the creator, but does NOT
  enforce the underlying claim — only the builder enforces it.** Callers MUST broadcast the full
  returned bundle intact. Dropping or reordering the underlying-claim spend strands the underlying at a
  bare settlement coin that any mempool watcher can claim key-free.
- **Returns** `created: None`.

### 5.3 `clawback(ctx, creator, created) -> OptionSpend`
The creator reclaims the locked underlying to `created.underlying.creator_puzzle_hash` via the
underlying's clawback path, valid strictly after expiry (consensus-enforced `AssertSecondsAbsolute`).
- **Enforced invariants:** a `Standard` `creator` whose `standard_puzzle_hash()`  `created.underlying.creator_puzzle_hash` is rejected up front; a `Custom` creator cannot be checked
  here and relies on the consensus to reject a wrong-party spend.
- **Value conservation:** exactly `underlying_coin.amount` is recovered.
- **Returns** `created: None`.

### 5.4 `parse(ctx, coin, puzzle_reveal, solution)` / `parse_child(ctx, parent_coin, parent_puzzle_reveal, parent_solution)`
Reconstruct an option from a fetched coin spend. `parse` decodes an option from its own spend;
`parse_child` walks a parent option spend to the option child. Both return `Ok(None)` when the puzzle is
not an option contract.

**Recoverable-fields limitation (normative):** an option singleton's on-chain puzzle commits only to its
identity fields (launcher id, underlying coin id, underlying delegated-puzzle hash, current p2 puzzle
hash — the SDK's `OptionInfo`). The option's *terms* (creator puzzle hash, expiry seconds, underlying
amount, strike type) live in the launcher metadata and the underlying coin and are NOT invertible from an
option singleton coin spend. `ParsedOption` therefore carries only the identity fields; a caller that
needs the terms retains the `CreatedOption` / `OptionTerms` from `create`.

### 5.5 `required_signatures(coin_spends, agg_sig_me) -> Vec<RequiredSignature>`
Runs each spend's puzzle to collect its `AGG_SIG_*` conditions and reports the BLS messages the caller
must sign, given the network's `agg_sig_me` additional data. Performs NO signing.

### 5.6 `transfer(ctx, owner, created, new_owner_puzzle_hash) -> OptionSpend` (v0.2.0)
Moves the option singleton to a new owner. Spends the singleton through the current `owner`'s p2 layer
and recreates it (same launcher id, same underlying, same amount, hinted for wallet discovery) at
`new_owner_puzzle_hash`. Only the ticket moves — the locked underlying coin and the option's terms are
unchanged.
- **Enforced invariants:** a `Standard` `owner` whose `standard_puzzle_hash()` ≠ the option's current
  `p2_puzzle_hash` is rejected up front (§6); a `Custom` owner cannot be checked here and relies on the
  consensus to reject a wrong-party spend.
- **Returns** `created: Some(..)` — the option in its NEW-owner state, so the caller can chain further
  transfers or an exercise/clawback against the transferred singleton once confirmed. (A transferred
  option remains fully operable: the new owner exercises it and the ORIGINAL creator still receives the
  strike — test `transfer_moves_option_then_new_owner_exercises`.)

### 5.7 `rehydrate(option, terms, underlying_coin) -> CreatedOption` and `parse_metadata(ctx, launcher_solution) -> OptionMetadata` (v0.2.0)
Reconstruct a full, operable `CreatedOption` from on-chain state, lifting the §5.4 recoverable-fields
limitation for a caller that supplies (and lets the crate verify) the creator puzzle hash.
- **`parse_metadata`** decodes the launcher coin's solution (fetched by the caller) into `OptionMetadata`
  — recovering `expiration_seconds` and `strike_type`. Network-free.
- **`rehydrate`** rebuilds the `OptionUnderlying` from `option.info.launcher_id`, the supplied `terms`,
  and `underlying_coin.amount`, then REJECTS the reconstruction unless ALL THREE on-chain commitments
  match. Per the chia-wallet-sdk 0.30 `OptionUnderlying` derivation, the three checks bind **disjoint**
  field sets and are therefore **jointly** load-bearing — none is mere defense-in-depth:
    1. **1-of-2 path hash** = `underlying_coin.puzzle_hash`. The path is
       `merkle([exercise_path(launcher_id), clawback_path(expiry, creator_ph)])`, so it binds ONLY the
       launcher id, expiry, and creator puzzle hash — NOT the amount or strike type. Sole check that
       catches a wrong **creator puzzle hash**.
    2. **delegated-puzzle hash** = `option.info.underlying_delegated_puzzle_hash`. The delegated puzzle
       commits to the expiry, the underlying amount, and the strike type (settlement target +
       requested-payment amount). Sole check that catches a wrong **strike type**; also catches a wrong
       amount.
    3. **underlying coin id** = `option.info.underlying_coin_id`. Binds the coin's full identity
       (parent + puzzle hash + amount), uniquely rejecting a substituted coin of the right shape but
       wrong parent.
  On success the returned `CreatedOption` is operable by `exercise` / `clawback` / `transfer` exactly as
  one returned by `create`.
- **Verified, not trusted (security property):** a wrong term cannot produce a `CreatedOption` — it is
  rejected with `InvalidInput`, never a handle that builds an unspendable or mis-targeted bundle. (Tests:
  `rehydrate_recovers_operable_option` round-trips create → rehydrate → exercise; `rehydrate_rejects_wrong_creator`
  asserts a wrong creator puzzle hash is rejected.)
- **Pure:** both perform no I/O and hold no key.

## 6. Error taxonomy

`Error` (`thiserror`), `Result<T> = std::result::Result<T, Error>`:
- `Driver(#[from] DriverError)` — a chia-wallet-sdk driver failure (allocation, currying, launcher mint).
- `Signer(#[from] SignerError)` — a failure computing required signatures.
- `InvalidInput(String)` — caller input that cannot produce a valid spend: zero underlying, overflow,
  underfunded funding/strike coin, wrong-party clawback, or an unsupported (CAT/NFT) strike (rejected at
  BOTH create and exercise). The message states the precise violation. A non-XCH strike returns this
  rather than minting an unexercisable option or emitting an incorrect spend (an honest gap, never a
  silent/incorrect settlement).

## 7. Lifecycle state machine

```
                 create
   (funding) ───────────────▶ CREATED ──────────────────────────────▶ terminal
                              │  │
             exercise (before │  │ clawback (after expiry):
             expiry): strike  │  │ creator reclaims underlying
             paid to creator, │  │
             underlying       │  │
             claimed to holder▼  ▼
                           EXERCISED   CLAWED-BACK
```
An option is created, then reaches exactly one terminal state: **exercised** (strictly before expiry) or
**clawed-back** (strictly after expiry). Both exits are always reachable — the option is never
locked-forever (§8.6). **`transfer` (v0.2.0) is a self-loop on CREATED:** it re-homes the ticket to a new
owner without changing state or terms; the new owner then exercises or claws back as usual.

## 8. Security properties (guarantees)

1. **No theft of the underlying without the strike.** Exercise unlocks the underlying only in a bundle
   that also pays the strike (the underlying's delegated puzzle asserts the settlement payment);
   consensus-gated. (Test: `exercise_without_strike_leg_is_rejected`.)
2. **No exercise after expiry.** The exercise path asserts `AssertBeforeSecondsAbsolute(expiry)`. (Test:
   `exercise_after_expiry_is_rejected`.)
3. **No clawback before expiry.** The clawback path asserts `AssertSecondsAbsolute(expiry)`. (Test:
   `clawback_before_expiry_is_rejected`.)
4. **Terms immutable after create.** The terms are curried into the option/underlying puzzles, so any
   change yields a different coin id.
5. **Never signs / no key leak.** No `SecretKey` appears in any type; no function returns a `Signature`.
6. **No locked-forever option.** Both exits (exercise, clawback) are reachable across the expiry
   boundary. (Tests: the round-trip + clawback-on-expiry.)
7. **Wrong-party clawback rejected.** A `Standard` clawback owner mismatching the creator ph is rejected
   up front; the consensus enforces it regardless. (Test: `clawback_rejects_wrong_creator_key`.)
8. **Value conservation.** Create requires `funding ≥ underlying + 1`; exercise conserves value for
   both parties — the underlying is CLAIMED to the holder and the strike is paid to the creator in the
   same bundle, leaving no orphan settlement coin a third party could claim key-free; clawback recovers
   exactly the locked amount. (Tests: `create_then_exercise_round_trip` asserts the holder nets exactly
   `underlying − strike` and the creator nets the strike; `exercise_leaves_no_orphan_underlying_settlement_coin`
   asserts no bare settlement coin survives.)
9. **Builder-enforced underlying-claim leg (not consensus-enforced).** The `exercise` builder emits a
   settlement spend that claims the unlocked underlying to the holder in the SAME bundle as the strike
   payment. Consensus enforces the strike leg (the option puzzle asserts the settlement payment) but
   does NOT enforce the underlying claim — only the builder enforces it. Callers MUST broadcast the
   full returned `coin_spends` intact. Any caller that drops or reorders the underlying-claim spend
   strands the underlying at a public settlement coin, allowing any mempool watcher to claim it
   key-free even though the holder has paid the strike and received nothing. (Test:
   `exercise_drops_underlying_claim_leaves_coin_strandable`.)
10. **Rehydration is verified, not trusted (v0.2.0).** `rehydrate` cannot fabricate a `CreatedOption` from
    wrong terms: it reconstructs the `OptionUnderlying` and rejects it unless the 1-of-2 path hash, the
    delegated-puzzle hash, and the underlying coin id all match the on-chain option. These three checks
    bind disjoint field sets and are jointly load-bearing (§5.7): a wrong **creator puzzle hash** is
    caught only by the path-hash check, a wrong **strike type** only by the delegated-puzzle-hash check,
    and a substituted same-shape coin only by the coin-id check — so each is rejected with `InvalidInput`
    rather than yielding a handle that builds an unspendable or mis-targeted bundle. (Tests:
    `rehydrate_rejects_wrong_creator`, `rehydrate_rejects_wrong_strike`, `rehydrate_rejects_wrong_amount`.)
11. **Transfer moves only the ticket (v0.2.0).** `transfer` re-homes the option singleton to a new owner
    and leaves the underlying and terms untouched; a wrong-party `Standard` owner is rejected up front. A
    transferred option is fully operable — the new owner exercises it and the original creator still
    receives the strike. (Test: `transfer_moves_option_then_new_owner_exercises`.)


## 9. Conformance

The option puzzles (`OptionLauncher` / `OptionContract` / `OptionUnderlying` / `SettlementLayer`) are the
canonical chia-wallet-sdk 0.30 puzzles — the byte-source-of-truth; dig-options NEVER hand-rolls a puzzle.
Every builder's output is validated on the in-process Chia simulator (`chia-sdk-test`), including a
create → exercise round-trip, a create → clawback-on-expiry flow, the adversarial negatives above, and a
parse identity round-trip.