payments/migration1: freeze core payment code

Copy the core payments/db code into payments/db/migration1 and
add the required sqlc-generated types/queries from sqldb/sqlc.
This effectively freezes the migration code so it stays robust
against future query or schema changes in the main payments package.

Replace the delegation to channeldb.ReadElement/WriteElement with
self-contained, frozen implementations that only handle the exact types
required by this migration package. This removes the dependency on the
live channeldb codec so that future changes to channeldb serialization
cannot silently corrupt or break the migration.

UnknownElementType is also defined locally for the same reason.
This commit is contained in:
ziggie committed 2026-02-25 18:52:32 +01:00
1 parent 74f8f2d9e6
commit fb705bb0f9
17 files changed
+7992

No files matched your search

+274
View File
@@ -0,0 +1,274 @@
package migration1
import (
"bytes"
"fmt"
"strconv"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
)
// dbPaymentToCreationInfo converts database payment data to the
// PaymentCreationInfo struct.
func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64,
createdAt time.Time, intentPayload []byte,
firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo {
// This is the payment hash for non-AMP payments and the SetID for AMP
// payments.
var identifier lntypes.Hash
copy(identifier[:], paymentIdentifier)
return &PaymentCreationInfo{
PaymentIdentifier: identifier,
Value: lnwire.MilliSatoshi(amountMsat),
// The creation time is stored in the database as UTC but here
// we convert it to local time.
CreationTime: createdAt.Local(),
PaymentRequest: intentPayload,
FirstHopCustomRecords: firstHopCustomRecords,
}
}
// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt.
func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow,
hops []sqlc.FetchHopsForAttemptsRow,
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) (
*HTLCAttempt, error) {
// Convert route-level first hop custom records to CustomRecords map.
var firstHopWireCustomRecords lnwire.CustomRecords
if len(routeCustomRecords) > 0 {
firstHopWireCustomRecords = make(lnwire.CustomRecords)
for _, record := range routeCustomRecords {
firstHopWireCustomRecords[uint64(record.Key)] =
record.Value
}
}
// Build the route from the database data.
route, err := dbDataToRoute(
hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat,
dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount,
dbAttempt.RouteSourceKey, firstHopWireCustomRecords,
)
if err != nil {
return nil, fmt.Errorf("failed to convert to route: %w",
err)
}
hash, err := lntypes.MakeHash(dbAttempt.PaymentHash)
if err != nil {
return nil, fmt.Errorf("failed to parse payment "+
"hash: %w", err)
}
// Create the attempt info.
var sessionKey [32]byte
copy(sessionKey[:], dbAttempt.SessionKey)
info := HTLCAttemptInfo{
AttemptID: uint64(dbAttempt.AttemptIndex),
sessionKey: sessionKey,
Route: *route,
AttemptTime: dbAttempt.AttemptTime,
Hash: &hash,
}
attempt := &HTLCAttempt{
HTLCAttemptInfo: info,
}
// If there's no resolution type, the attempt is still in-flight.
// Return early without processing settlement or failure info.
if !dbAttempt.ResolutionType.Valid {
return attempt, nil
}
// Add settlement info if present.
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
HTLCAttemptResolutionSettled {
var preimage lntypes.Preimage
copy(preimage[:], dbAttempt.SettlePreimage)
attempt.Settle = &HTLCSettleInfo{
Preimage: preimage,
SettleTime: dbAttempt.ResolutionTime.Time,
}
}
// Add failure info if present.
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
HTLCAttemptResolutionFailed {
failure := &HTLCFailInfo{
FailTime: dbAttempt.ResolutionTime.Time,
}
if dbAttempt.HtlcFailReason.Valid {
failure.Reason = HTLCFailReason(
dbAttempt.HtlcFailReason.Int32,
)
}
if dbAttempt.FailureSourceIndex.Valid {
failure.FailureSourceIndex = uint32(
dbAttempt.FailureSourceIndex.Int32,
)
}
// Decode the failure message if present.
if len(dbAttempt.FailureMsg) > 0 {
msg, err := lnwire.DecodeFailureMessage(
bytes.NewReader(dbAttempt.FailureMsg), 0,
)
if err != nil {
return nil, fmt.Errorf("failed to decode "+
"failure message: %w", err)
}
failure.Message = msg
}
attempt.Failure = failure
}
return attempt, nil
}
// dbDataToRoute converts database route data to a Route.
func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow,
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64,
sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) (
*Route, error) {
if len(hops) == 0 {
return nil, fmt.Errorf("no hops provided")
}
// Hops are already sorted by hop_index from the SQL query.
routeHops := make([]*Hop, len(hops))
for i, hop := range hops {
pubKey, err := NewVertexFromBytes(hop.PubKey)
if err != nil {
return nil, fmt.Errorf("failed to parse pub key: %w",
err)
}
var channelID uint64
if hop.Scid != "" {
// The SCID is stored as a string representation
// of the uint64.
var err error
channelID, err = strconv.ParseUint(hop.Scid, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse "+
"scid: %w", err)
}
}
routeHop := &Hop{
PubKeyBytes: pubKey,
ChannelID: channelID,
OutgoingTimeLock: uint32(hop.OutgoingTimeLock),
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward),
}
// Add MPP record if present.
if len(hop.MppPaymentAddr) > 0 {
var paymentAddr [32]byte
copy(paymentAddr[:], hop.MppPaymentAddr)
routeHop.MPP = record.NewMPP(
lnwire.MilliSatoshi(hop.MppTotalMsat.Int64),
paymentAddr,
)
}
// Add AMP record if present.
if len(hop.AmpRootShare) > 0 {
var rootShare [32]byte
copy(rootShare[:], hop.AmpRootShare)
var setID [32]byte
copy(setID[:], hop.AmpSetID)
routeHop.AMP = record.NewAMP(
rootShare, setID,
uint32(hop.AmpChildIndex.Int32),
)
}
// Add blinding point if present (only for introduction node
// in blinded route).
if len(hop.BlindingPoint) > 0 {
pubKey, err := btcec.ParsePubKey(hop.BlindingPoint)
if err != nil {
return nil, fmt.Errorf("failed to parse "+
"blinding point: %w", err)
}
routeHop.BlindingPoint = pubKey
}
// Add encrypted data if present (for all blinded hops).
if len(hop.EncryptedData) > 0 {
routeHop.EncryptedData = hop.EncryptedData
}
// Add total amount if present (only for final hop in blinded
// route).
if hop.BlindedPathTotalAmt.Valid {
routeHop.TotalAmtMsat = lnwire.MilliSatoshi(
hop.BlindedPathTotalAmt.Int64,
)
}
// Add hop-level custom records.
if records, ok := hopCustomRecords[hop.ID]; ok {
routeHop.CustomRecords = make(
record.CustomSet,
)
for _, rec := range records {
routeHop.CustomRecords[uint64(rec.Key)] =
rec.Value
}
}
// Add metadata if present.
if len(hop.MetaData) > 0 {
routeHop.Metadata = hop.MetaData
}
routeHops[i] = routeHop
}
// Parse the source node public key.
var sourceNode Vertex
copy(sourceNode[:], sourceKey)
route := &Route{
TotalTimeLock: uint32(totalTimeLock),
TotalAmount: lnwire.MilliSatoshi(totalAmount),
SourcePubKey: sourceNode,
Hops: routeHops,
FirstHopWireCustomRecords: firstHopWireCustomRecords,
}
// Set the first hop amount if it is set.
if firstHopAmountMsat != 0 {
route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
tlv.NewBigSizeT(lnwire.MilliSatoshi(
firstHopAmountMsat,
)),
)
}
return route, nil
}