Previously a one(intent)-to-many(payment) relationship it is now changed to a one-to-one relationship because a payment request only can have 1 payment related to it. Looking into the future with BOLT12 offers, the fetched invoice from the offer could be stored here as well and the relationship would still hold.
443 lines
21 KiB
SQL
443 lines
21 KiB
SQL
-- ─────────────────────────────────────────────
|
|
-- Payment System Schema Migration
|
|
-- ─────────────────────────────────────────────
|
|
-- This migration creates the complete payment system schema including:
|
|
-- - Payment intents (only BOLT 11 invoices for now)
|
|
-- - Payment attempts and HTLC tracking
|
|
-- - Route hops and custom TLV records
|
|
-- - Resolution tracking for settled/failed payments
|
|
-- ─────────────────────────────────────────────
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Payments Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores all payments including all known payment types:
|
|
-- - Legacy payments
|
|
-- - Multi-Path Payments (MPP)
|
|
-- - Atomic Multi-Path Payments (AMP)
|
|
-- - Blinded payments
|
|
-- - Keysend payments
|
|
-- - Spontaneous AMP payments
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payments (
|
|
-- Primary key for the payment record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- The amount of the payment in millisatoshis
|
|
amount_msat BIGINT NOT NULL,
|
|
|
|
-- Timestamp when the payment was created
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Logical identifier for the payment
|
|
-- For legacy + MPP: matches the HTLC hash
|
|
-- For AMP: the setID
|
|
-- For future intent types: any unique payment-level key
|
|
payment_identifier BLOB NOT NULL,
|
|
|
|
-- The reason for payment failure (only set if payment has failed)
|
|
-- Integer enum type indicating failure reason
|
|
fail_reason INTEGER,
|
|
|
|
-- Ensure payment identifiers are unique across all payments
|
|
CONSTRAINT idx_payments_payment_identifier_unique
|
|
UNIQUE (payment_identifier)
|
|
);
|
|
|
|
-- Index for efficient querying by creation time (for chronological ordering)
|
|
CREATE INDEX IF NOT EXISTS idx_payments_created_at
|
|
ON payments(created_at);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Payment Intents Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores the descriptor of what the payment is paying for.
|
|
-- Depending on the type, the payload might contain:
|
|
-- - BOLT 11 invoice data
|
|
-- - BOLT 12 offer data
|
|
-- - NULL for legacy hash-only/keysend style payments
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_intents (
|
|
-- Primary key for the intent record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- Reference to the payment this intent belongs to (one-to-one relationship)
|
|
-- When the payment is deleted, the intent is automatically deleted
|
|
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
|
|
|
|
-- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_invoice)
|
|
-- Uses SMALLINT (int16) for efficient storage of enum values
|
|
intent_type SMALLINT NOT NULL,
|
|
|
|
-- The serialized payload for the payment intent
|
|
-- Content depends on type - could be invoice, offer, or NULL
|
|
intent_payload BLOB,
|
|
|
|
-- Ensure one-to-one relationship: each payment has at most one intent.
|
|
-- Currently we only support one intent per payment this makes sure we do
|
|
-- not accidentally pay the same request multiple times. This currently
|
|
-- only has bolt 11 payment requests/invoices. But in the future this can
|
|
-- also include BOLT 12 offers/invoices.
|
|
CONSTRAINT idx_payment_intents_payment_id_unique
|
|
UNIQUE (payment_id)
|
|
);
|
|
|
|
-- Index for efficient querying by intent type
|
|
CREATE INDEX IF NOT EXISTS idx_payment_intents_type
|
|
ON payment_intents(intent_type);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Payment HTLC Attempts Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores all HTLC attempts for a payment. A payment can have multiple
|
|
-- HTLC attempts depending on whether the payment is split and also
|
|
-- if some attempts fail and need to be retried.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_htlc_attempts (
|
|
-- Primary key for the HTLC attempt record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- The index of the HTLC attempt
|
|
-- TODO: This will be removed and the primary key will be used only
|
|
attempt_index BIGINT NOT NULL,
|
|
|
|
-- Reference to the parent payment
|
|
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
|
|
|
|
-- The session key of the HTLC attempt (also known as ephemeral key
|
|
-- of the Sphinx packet used for onion routing)
|
|
session_key BLOB NOT NULL,
|
|
|
|
-- Timestamp when the HTLC attempt was created
|
|
attempt_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- The payment hash for the payment attempt
|
|
-- The hash the HTLC will be locked to - this does not need to be
|
|
-- equal to the payment level identifier (e.g., for AMP payments)
|
|
payment_hash BLOB NOT NULL,
|
|
|
|
-- First hop amount in millisatoshis of the HTLC attempt
|
|
-- Normally the same as the total amount of the route, but when using
|
|
-- custom channels this might be different
|
|
first_hop_amount_msat BIGINT NOT NULL,
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Route Information for the HTLC Attempt
|
|
-- ─────────────────────────────────────────────
|
|
-- Every attempt has one route, so there is a 1:1 relationship between
|
|
-- attempts and routes. The route itself can be found in the hops table.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
-- The total time lock of the route (in blocks)
|
|
route_total_time_lock INTEGER NOT NULL,
|
|
|
|
-- The total amount of the route in millisatoshis
|
|
route_total_amount BIGINT NOT NULL,
|
|
|
|
-- The source key of the route (our node's public key)
|
|
route_source_key BLOB NOT NULL,
|
|
|
|
-- Ensure attempt indices are unique across all attempts
|
|
CONSTRAINT idx_htlc_attempt_index_unique
|
|
UNIQUE (attempt_index),
|
|
|
|
-- Ensure session keys are unique (each attempt has unique session key)
|
|
CONSTRAINT idx_htlc_session_key_unique
|
|
UNIQUE (session_key)
|
|
);
|
|
|
|
-- Index for efficient querying by payment ID (find all attempts for a payment)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_payment_id
|
|
ON payment_htlc_attempts(payment_id);
|
|
|
|
-- Index for efficient querying by attempt index (for lookups and joins)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_attempt_index
|
|
ON payment_htlc_attempts(attempt_index);
|
|
|
|
-- Index for efficient querying by payment hash (for HTLC matching)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_payment_hash
|
|
ON payment_htlc_attempts(payment_hash);
|
|
|
|
-- Index for efficient querying by attempt time (for chronological ordering)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_attempt_time
|
|
ON payment_htlc_attempts(attempt_time);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- HTLC Attempt Resolutions Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores resolution metadata for HTLC attempts. Rows appear once an
|
|
-- attempt settles or fails, providing the final outcome and timing.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_htlc_attempt_resolutions (
|
|
-- Primary key referencing the HTLC attempt
|
|
-- TODO: This will be removed and the primary key will be used only
|
|
attempt_index INTEGER PRIMARY KEY
|
|
REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE,
|
|
|
|
-- Timestamp when the attempt was resolved (settled or failed)
|
|
resolution_time TIMESTAMP NOT NULL,
|
|
|
|
-- Outcome of the attempt: 1 = settled, 2 = failed
|
|
resolution_type INTEGER NOT NULL CHECK (resolution_type IN (1, 2)),
|
|
|
|
-- Settlement payload (only populated for settled attempts)
|
|
-- Contains the preimage that proves payment completion
|
|
settle_preimage BLOB,
|
|
|
|
-- Failure payload (only populated for failed attempts)
|
|
-- Index of the node that sent the failure
|
|
failure_source_index INTEGER,
|
|
|
|
-- HTLC failure reason code
|
|
htlc_fail_reason INTEGER,
|
|
|
|
-- Failure message from the failing node, this message is binary encoded
|
|
-- using the lightning wire protocol, see also lnwire/onion_error.go
|
|
failure_msg BLOB,
|
|
|
|
-- Ensure data integrity: settled attempts must have preimage,
|
|
-- failed attempts must not have preimage
|
|
CHECK (
|
|
(resolution_type = 1 AND settle_preimage IS NOT NULL AND
|
|
failure_source_index IS NULL AND htlc_fail_reason IS NULL AND
|
|
failure_msg IS NULL)
|
|
OR
|
|
(resolution_type = 2 AND settle_preimage IS NULL)
|
|
)
|
|
);
|
|
|
|
-- Index for efficient querying by resolution type (settled vs failed)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_resolutions_type
|
|
ON payment_htlc_attempt_resolutions(resolution_type);
|
|
|
|
-- Index for efficient querying by resolution time (for chronological analysis)
|
|
CREATE INDEX IF NOT EXISTS idx_htlc_resolutions_time
|
|
ON payment_htlc_attempt_resolutions(resolution_time);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Payment Route Hops Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores the individual hops of a payment route. An attempt has only
|
|
-- one route, but a route can consist of several hops through the network.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_route_hops (
|
|
-- Primary key for the hop record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- Reference to the HTLC attempt this hop belongs to
|
|
htlc_attempt_index BIGINT NOT NULL
|
|
REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE,
|
|
|
|
-- The order/index of this hop within the route (0-based)
|
|
hop_index INTEGER NOT NULL,
|
|
|
|
-- The public key of the hop (node's public key)
|
|
pub_key BLOB,
|
|
|
|
-- The short channel ID of the hop (channel identifier)
|
|
scid TEXT NOT NULL,
|
|
|
|
-- The outgoing time lock of the hop (in blocks)
|
|
outgoing_time_lock INTEGER NOT NULL,
|
|
|
|
-- The amount to forward to the next hop (in millisatoshis)
|
|
amt_to_forward BIGINT NOT NULL,
|
|
|
|
-- The metadata blob transmitted to the hop (onion payload)
|
|
meta_data BLOB,
|
|
|
|
-- Ensure each attempt can only have one hop at each hop index
|
|
-- This prevents duplicate hops in the same position
|
|
CONSTRAINT idx_route_hops_unique_hop_per_attempt
|
|
UNIQUE (htlc_attempt_index, hop_index)
|
|
);
|
|
|
|
-- Index for efficient querying by attempt index (find all hops for an attempt)
|
|
CREATE INDEX IF NOT EXISTS idx_route_hops_htlc_attempt_index
|
|
ON payment_route_hops(htlc_attempt_index);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Per-Hop Payload Tables
|
|
-- ─────────────────────────────────────────────
|
|
-- These tables store specialized payload data for different payment types.
|
|
-- Each table is only populated for hops that require that specific payload.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- MPP (Multi-Path Payment) Payload Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores MPP-specific payload data. Only present for the final hop
|
|
-- of an MPP attempt, containing payment address and total amount info.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_route_hop_mpp (
|
|
-- Primary key referencing the hop
|
|
hop_id INTEGER PRIMARY KEY
|
|
REFERENCES payment_route_hops (id) ON DELETE CASCADE,
|
|
|
|
-- The payment address of the MPP path (for payment correlation)
|
|
payment_addr BLOB NOT NULL,
|
|
|
|
-- The total amount of the MPP payment in millisatoshis
|
|
-- This is the sum of all parts in the multi-path payment
|
|
total_msat BIGINT NOT NULL
|
|
);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- AMP (Atomic Multi-Path Payment) Payload Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores AMP-specific payload data. Only present for the final hop
|
|
-- of an AMP attempt, containing share information for atomicity.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_route_hop_amp (
|
|
-- Primary key referencing the hop
|
|
hop_id INTEGER PRIMARY KEY
|
|
REFERENCES payment_route_hops (id) ON DELETE CASCADE,
|
|
|
|
-- The root share of the AMP path (for share reconstruction)
|
|
root_share BLOB NOT NULL,
|
|
|
|
-- The set ID of the AMP path (groups related AMP parts)
|
|
set_id BLOB NOT NULL,
|
|
|
|
-- The child index of the AMP path (identifies this part)
|
|
child_index INTEGER NOT NULL
|
|
);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Blinded Route Payload Table
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores blinded route payload data. Rows only exist for hops that
|
|
-- are part of a blinded path, providing privacy-preserving routing.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_route_hop_blinded (
|
|
-- Primary key referencing the hop
|
|
hop_id INTEGER PRIMARY KEY
|
|
REFERENCES payment_route_hops (id) ON DELETE CASCADE,
|
|
|
|
-- The encrypted payload for the blinded hop
|
|
encrypted_data BLOB NOT NULL,
|
|
|
|
-- Only set for the introduction point of the blinded path
|
|
-- Contains the blinding point for the introduction node
|
|
blinding_point BLOB,
|
|
|
|
-- Only set for the final hop in the blinded path
|
|
-- Contains the total amount for the entire blinded path
|
|
blinded_path_total_amt BIGINT
|
|
);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Custom TLV Records Tables
|
|
-- ─────────────────────────────────────────────
|
|
-- These tables store custom TLV (Type-Length-Value) records associated
|
|
-- with payments, attempts, and hops. This is a denormalized structure
|
|
-- designed to simplify cascade deletions, as each record is owned by
|
|
-- a single parent entity.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Payment-Level First Hop Custom Records
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores custom TLV records that are part of the first hop of a payment.
|
|
-- These records are sent to the first hop and are payment-level data.
|
|
--
|
|
-- NOTE: This relates to the custom tlv record data which is sent to the first
|
|
-- hop in the wire message (UpdateAddHTLC) NOT the onion packet.
|
|
--
|
|
-- TODO(ziggie): We store mostly redundant data here and on the attempt level.
|
|
-- This might be improved in the future to reduce duplication.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_first_hop_custom_records (
|
|
-- Primary key for the custom record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- Reference to the parent payment
|
|
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
|
|
|
|
-- The TLV type identifier (must be >= 65536 for custom records)
|
|
key BIGINT NOT NULL,
|
|
|
|
-- The TLV value data
|
|
value BLOB NOT NULL,
|
|
|
|
-- Ensure we only store custom TLV records (not standard ones)
|
|
CHECK (key >= 65536),
|
|
|
|
-- Ensure each payment can only have one record per TLV type
|
|
CONSTRAINT idx_payment_first_hop_custom_records_unique
|
|
UNIQUE (payment_id, key)
|
|
);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Attempt-Level First Hop Custom Records
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores custom TLV records for the first hop on the route level.
|
|
-- These might be different from the payment-level first hop records
|
|
-- in case of custom channels or route-specific modifications.
|
|
--
|
|
-- NOTE: This relates to the custom tlv record data which is sent to the first
|
|
-- hop in the wire message (UpdateAddHTLC) NOT the onion packet.
|
|
--
|
|
-- TODO(ziggie): We store mostly redundant data here and on the payment level.
|
|
-- This might be improved in the future to reduce duplication.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_attempt_first_hop_custom_records (
|
|
-- Primary key for the custom record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- Reference to the parent HTLC attempt
|
|
htlc_attempt_index BIGINT NOT NULL
|
|
REFERENCES payment_htlc_attempts (attempt_index) ON DELETE CASCADE,
|
|
|
|
-- The TLV type identifier (must be >= 65536 for custom records)
|
|
key BIGINT NOT NULL,
|
|
|
|
-- The TLV value data
|
|
value BLOB NOT NULL,
|
|
|
|
-- Ensure we only store custom TLV records (not standard ones)
|
|
CHECK (key >= 65536),
|
|
|
|
-- Ensure each attempt can only have one record per TLV type
|
|
CONSTRAINT idx_payment_attempt_first_hop_custom_records_unique
|
|
UNIQUE (htlc_attempt_index, key)
|
|
);
|
|
|
|
-- ─────────────────────────────────────────────
|
|
-- Hop-Level Custom Records
|
|
-- ─────────────────────────────────────────────
|
|
-- Stores custom TLV records associated with a specific hop within
|
|
-- a payment route. These records are sent to that specific hop
|
|
-- and are hop-specific data.
|
|
-- ─────────────────────────────────────────────
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_hop_custom_records (
|
|
-- Primary key for the custom record
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
-- Reference to the parent hop
|
|
hop_id BIGINT NOT NULL REFERENCES payment_route_hops (id) ON DELETE CASCADE,
|
|
|
|
-- The TLV type identifier (must be >= 65536 for custom records)
|
|
key BIGINT NOT NULL,
|
|
|
|
-- The TLV value data
|
|
value BLOB NOT NULL,
|
|
|
|
-- Ensure we only store custom TLV records (not standard ones)
|
|
CHECK (key >= 65536),
|
|
|
|
-- Ensure each hop can only have one record per TLV type
|
|
CONSTRAINT idx_payment_hop_custom_records_unique
|
|
UNIQUE (hop_id, key)
|
|
); |