Docs/Use casesv0.0.1-preview

Escrow and expiring holds

Hold funds until delivery, release on confirmation, and let the engine expire what's abandoned — exactly once, to a distinct terminal state.

On this page
  1. The flow
  2. The TTL
  3. What the deadline guarantees
  4. Operational notes
  5. Related

Escrow is a hold with a deadline: buyer’s funds are locked when the order is placed, released to the seller on delivery, returned to the buyer on cancellation — and if nothing happens, the hold must not live forever. TulaDB runs all four outcomes in the engine, including the last one.

The flow

E = c.create_ledger(LedgerInstance(ledger_id=0, currency=840,
        reserve_ttl_ns=7 * 24 * 3600 * 10**9)).ledger_id   # see "The TTL" below
BUYER, SELLER, MARKET = 7001, 8001, 1   # integer ids
for acct in (BUYER, SELLER):
    c.create_account(AccountCreate(id=acct, ledger_id=E, currency=840,
                                   limit_low=-10**12, limit_high=10**12))

# Order placed: lock the buyer's funds. Nothing is posted.
c.reserve(TransferCreate(source_id=MARKET, ledger_id=E, currency=840,
        debit_account_id=BUYER, credit_account_id=SELLER,
        amount=5_000, txn_id="order-7841"))

# Delivery confirmed: release to the seller.
c.commit_by_key(MARKET, E, 840, "order-7841")     # → POSTED

# ...or cancelled: return to the buyer.
c.rollback_by_key(MARKET, E, 840, "order-7841")   # → VOIDED, funds released

While held, the funds are genuinely unavailable — a pending debit counts against the buyer’s balance limit, so escrowed money cannot be spent twice.

The TTL

The deadline is a property of the ledger, stamped into each reserve at reserve time:

c.create_ledger(LedgerInstance(ledger_id=0, currency=840,
        reserve_ttl_ns=7 * 24 * 3600 * 10**9))   # 7-day escrow window

There is nothing to switch on cluster-wide: the reservation-expiry capability is always active in this release, and expiry stays off for any ledger whose reserve_ttl_ns is 0 (the default). A reserve on a ledger with a non-zero TTL always carries its deadline, stamped at reserve time. You can also set or change a ledger’s TTL later with ledger_set_expiry(ledger_id, reserve_ttl_ns); it applies to reserves taken after the change.

An overdue hold resolves through two enforcement paths:

  • At touch, immediately: a commit or rollback at or after the deadline is refused with ERR_RESERVE_EXPIRED and the hold resolves then and there. A late delivery confirmation cannot capture expired funds.
  • In the background: a leader-side reaper takes due holds off a deadline queue and expires them through the WAL, so replicas converge on the same outcome at the same log position. The pass runs every reserve_expiry_interval_ns (default 100 ms), so when the reaper keeps up an untouched hold is released within a fraction of a second of its deadline — not at the exact instant.

Either way the record lands in a distinct EXPIRED terminal state — separate from VOIDED, so “the marketplace cancelled it” and “it timed out” are different answers in your audit trail — and the release happens exactly once.

What the deadline guarantees

The deadline is assigned from the replicated ledger TTL and carried in the WAL record itself, so leader, followers, and recovery all agree on it byte-for-byte — an election or restart mid-escrow cannot revive an expired hold or expire a live one. After expiry, replaying the original reserve returns the expired outcome; committing returns ERR_RESERVE_EXPIRED, forever.

Operational notes

  • tuladb_reservations_expired_total in the metrics counts every expiration — the reaper’s, and a commit or rollback that arrives after the deadline and finds the hold expired; alert on growth if expiry should be rare in your flow.
  • The TTL applies per ledger. Run different escrow windows as different ledgers, or leave reserve_ttl_ns = 0 for holds that must never expire.

Holds and two-phase transfers · Card payments — the same TTL cleans up abandoned authorizations · Spend controls — TTL holds as budgets