Skip to content

Feat/lock payment draining - #3

Open
dzdidi wants to merge 23 commits into
masterfrom
feat/lock-payment-draining
Open

dzdidi wants to merge 23 commits into
masterfrom
feat/lock-payment-draining

Conversation

@dzdidi

@dzdidi dzdidi commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

TO BE DEPLOYED TOGETHER WITH pubky/locks#61

Summary

Adds Paykit-owned Payment Request lifecycle and draining required for graceful Locks deletion, then updates invoice terms to released Paykit rc56 semantics.

This change:

  • pins paykit-lib and paykit-sdk to signed release tag v0.1.0-rc56 (peeled commit 24162ebbcc703251d8038f2e176d1f4cfb117c6a);
  • binds invoice creation to separate deployment-configured proposal and payment windows;
  • persists authoritative invoice creation, proposal-expiry, and payment-deadline timestamps;
  • tracks Payment Request lifecycle, Bitcoin payment state, deadline eligibility, and observed evidence independently;
  • persists durable, lock-wide payment-drain snapshots;
  • enqueues cancellations for unanswered Payment Requests;
  • exposes signed drain creation, lookup, cleanup, and per-Bundle status APIs;
  • retains terminal invoice/payment history after operational drain cleanup;
  • fences later publication generations of same canonical Lock ID;
  • keeps current Locks compatible by normalizing omitted payment_in to 24 hours.

rc56 deadline and evidence invariants

Default deployment policy is 1-hour proposal-acceptance window plus 24-hour payment window. Configuration must satisfy:

0 < proposal_acceptance_window < payment_window
proposal_expires_at = invoice_created_at + proposal_acceptance_window
payment_deadline = invoice_created_at + payment_window

payment_in remains signed compatibility binding: missing request and canonical criterion values normalize to 24, explicit values must be positive whole-hour JSON u64 values, and request/criterion mismatch is rejected before persistence. It does not configure either persisted deadline.

For each new invoice, Paykit Server samples PostgreSQL clock_timestamp() after acquiring decisive row fence, computes deadline once in DB transaction, and uses same instant for persisted invoice state, outbound rc56 PaymentRequestTerms.payment_deadline, replayed semantic intent, and HTTP response. Exact replay returns persisted timestamps without recomputation.

First amount-matched durable observation is timely when first_amount_matched_observed_at <= payment_deadline. Deadline expiry changes eligibility, not factual evidence: late and underpaid outputs remain persisted and observed until factual Bitcoin finality, but cannot grant automatic access. Required-peer freshness and same-fence target revalidation remain mandatory before status/drain decisions.

New signed APIs

POST /payment-request-drains
POST /payment-request-drain-lookups
POST /payment-request-drain-cleanups
POST /payment-requests/status

Drain responses expose aggregate state and opaque cleanup token. They do not expose Reader identities, Bundle IDs, Payment Request IDs, addresses, or raw internal errors.

Drain behavior

Drain creation atomically freezes current lifecycle classification:

  • accepted requests remain blocking;
  • rejected, canceled, and proposal-expired requests are terminal;
  • unanswered proposed requests receive durable cancellation intents;
  • exact replay retains original frozen classification;
  • accepted items advance monotonically as payment observations become terminal;
  • cleanup removes only operational drain state;
  • invoice and payment history remain retained;
  • successful cleanup advances lock payment generation.

Rejection-aware Locks entitlement handling is not included here. It remains later consumer change against separate lifecycle/payment/deadline/evidence model.

Staging reset and deployment

Migration 0007_payment_request_deadline_terms.sql is intentional, one-time destructive staging cutover. It truncates incompatible Paykit application state before replacing rc48/rc55 deadline terms with rc56 schema. There is no production backfill, dual read, or compatibility migration. SQLx records migration transactionally, so later starts do not repeat reset. It does not touch Locks database.

Deployment requires explicit acceptance of this staging data reset:

1. Stop Paykit Server and preserve any staging data needed for audit.
2. Deploy this exact Paykit Server revision to non-production only.
3. Start one instance and let embedded migration 0007 run once.
4. Verify migration ledger, readiness, and empty/reset application state.
5. Keep current Locks deployed unchanged.
6. Deploy reviewed Locks draining stack later.

No migration, deployment, release, tag, or merge is performed by this PR update.

Out of scope

  • rejection-aware Locks entitlement handling;
  • Marketplace prepare/activate/void/resolve lifecycle;
  • automatic refunds;
  • migration or backfill of historical prototype invoices;
  • exactly-once encrypted-message delivery;
  • Locks deletion orchestration.

Verification

Independent review approved exact commit 8d86af67f0a3639284c038bf67bc1faa02688ce0 (tree 24dd0787330952309879bfb28698530e96c590d9). This commit changes only Cargo.toml, Cargo.lock, and scripts/prepare-local-docker-sources.sh from prior approved head 1f9ba9ff4b99abe72f5c88d51abe5b91fc5245ca, replacing Paykit rev coordinates with signed release tag v0.1.0-rc56.

Verified locally against PostgreSQL 16.15:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p paykit-server --all-targets -- --test-threads=1
cargo test -p paykit-server-e2e -- --test-threads=1
cargo check --workspace --all-targets --locked
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features
git diff --check

Focused invoice, observation, drain, lifecycle, migration, exact-boundary, delayed-observer, and concurrency coverage also passed. GitHub CI results below must be read against exact head 8d86af67f0a3639284c038bf67bc1faa02688ce0; this repository does not currently report configured required checks.

@dzdidi
dzdidi marked this pull request as draft August 11, 2026 23:20
dzdidi added 10 commits August 25, 2026 10:24
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
@dzdidi
dzdidi force-pushed the feat/lock-payment-draining branch from 96dbf96 to 27662b3 Compare August 25, 2026 13:28
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	paykit-server-e2e/tests/invoices.rs
#	paykit-server-e2e/tests/server_workflow.rs
#	paykit-server/src/paykit.rs
#	paykit-server/src/persistence/invoices.rs
#	paykit-server/src/server.rs
#	paykit-server/tests/create_invoice.rs
- default omitted invoice request payment_in to 24
- default omitted canonical lock criterion payment_in to 24
- normalize omitted and explicit 24 to same idempotency binding
- preserve rejection of invalid values and explicit mismatches
- add focused compatibility regressions

Signed-off-by: dzdidi <dzdidi@protonmail.com>
Signed-off-by: dzdidi <dzdidi@protonmail.com>
# Conflicts:
#	Cargo.lock
#	paykit-server-e2e/Cargo.toml
#	paykit-server/Cargo.toml
@dzdidi
dzdidi marked this pull request as ready for review September 25, 2026 15:34
- correlate ambiguous proposal retries across all SDK attempts
- persist and aggregate canonical Payment Request lifecycle state
- add signed status, drain, lookup, and cleanup endpoints
- fence proposal handoff while drains are active
- commit frozen drain ownership, classification, and cancellation sets
- fail closed on replay, cleanup, generation, and projection conflicts
- use post-fence PostgreSQL clocks for deadlines and queue leases
- bind timely Bitcoin observations to qualifying outpoints
- bind Locks signatures to method, query-free path, and raw body
- preserve omitted payment_in as the 24-hour default
- add one-time prototype database reset migration
- update runtime, API, rollout, and release documentation

Signed-off-by: dzdidi <dzdidi@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant