fix(payments): add local idempotency gate for Lightning recording
The Fava-side duplicate checks (add_entry_idempotent, journal-link scan) are read-then-write races: on restart with a persisted invoice queue, or webhook + poller firing together, both callers pass the "not present" check and both insert. New processed_payments table (m005) keyed on payment_hash; exactly one claimant wins the INSERT ... ON CONFLICT DO NOTHING. Lifecycle: 'processing' while the write is in flight, 'done' after; failed recordings release the claim so redelivery retries, and 'processing' rows from a crashed process are cleared at listener startup. Also wraps the invoice-listener loop body in try/except so one poison payment can't kill payment recording for the process lifetime. Addresses CODE-REVIEW-2026-06 findings #4 and #9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c50455d5f6
commit
4fdb358bb0
4 changed files with 137 additions and 1 deletions
|
|
@ -624,3 +624,30 @@ async def m004_add_rbac_tables(db):
|
|||
"created_by": "system", # System-created default roles
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def m005_add_processed_payments(db):
|
||||
"""
|
||||
Local idempotency gate for Lightning payment recording.
|
||||
|
||||
The Fava-side duplicate check (`add_entry_idempotent`, journal-link
|
||||
scan) is a read-then-write race: the background invoice listener and
|
||||
the client-driven /record-payment endpoint can both pass the "not
|
||||
present" check for the same payment_hash and both insert. The
|
||||
primary key on payment_hash makes exactly one claimant win.
|
||||
|
||||
status lifecycle: 'processing' (claimed, write in flight) → 'done'
|
||||
(entry recorded). Failed claims are deleted so redelivery retries;
|
||||
'processing' rows from a crashed process are cleared at listener
|
||||
startup.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS processed_payments (
|
||||
payment_hash TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'processing',
|
||||
entry_id TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue