Merge pull request #10485 from ziggie1984/migration-kvdb-sql-payments-part1

payments: kv-to-sql migration (tests + wiring)
This commit is contained in:
ziggieXXX authored and GitHub committed 2026-03-02 10:29:46 +01:00
commit 2aec8f3380
66 files changed
+19914 -11

No files matched your search

+3 -1
View File
@@ -302,7 +302,9 @@ linters:
- "internal\\/musig2v040" - "internal\\/musig2v040"
- channeldb/migration_01_to_11 - channeldb/migration_01_to_11
- channeldb/migration/lnwire21 - channeldb/migration/lnwire21
- payments/db/migration1/lnwire
- payments/db/migration1/record
issues: issues:
# Only show newly introduced problems. # Only show newly introduced problems.
new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78 new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
+58
View File
@@ -51,6 +51,8 @@ import (
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/msgmux" "github.com/lightningnetwork/lnd/msgmux"
paymentsdb "github.com/lightningnetwork/lnd/payments/db" paymentsdb "github.com/lightningnetwork/lnd/payments/db"
paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
paymentsmig1sqlc "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb"
@@ -76,6 +78,10 @@ const (
// graphMigration is the version number for the graph migration // graphMigration is the version number for the graph migration
// that migrates the KV graph to the native SQL schema. // that migrates the KV graph to the native SQL schema.
graphMigration = 10 graphMigration = 10
// paymentMigration is the version number for the payments migration
// that migrates KV payments to the native SQL schema.
paymentMigration = 14
) )
// GrpcRegistrar is an interface that must be satisfied by an external subserver // GrpcRegistrar is an interface that must be satisfied by an external subserver
@@ -1153,6 +1159,31 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
return nil return nil
} }
paymentMig := func(tx *sqlc.Queries) error {
err := paymentsmig1.MigratePaymentsKVToSQL(
ctx,
dbs.ChanStateDB.Backend,
paymentsmig1sqlc.New(tx.GetTx()),
&paymentsmig1.SQLStoreConfig{
QueryCfg: queryCfg,
},
)
if err != nil {
return fmt.Errorf("failed to migrate "+
"payments to SQL: %w", err)
}
// Set the payments bucket tombstone to
// indicate that the migration has been
// completed.
d.logger.Debugf("Setting payments bucket " +
"tombstone")
return paymentsdb.SetPaymentsBucketTombstone(
dbs.ChanStateDB.Backend,
)
}
// Make sure we attach the custom migration function to // Make sure we attach the custom migration function to
// the correct migration version. // the correct migration version.
for i := 0; i < len(migrations); i++ { for i := 0; i < len(migrations); i++ {
@@ -1162,11 +1193,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
migrations[i].MigrationFn = invoiceMig migrations[i].MigrationFn = invoiceMig
continue continue
case graphMigration: case graphMigration:
migrations[i].MigrationFn = graphMig migrations[i].MigrationFn = graphMig
continue continue
case paymentMigration:
migrations[i].MigrationFn = paymentMig
continue
default: default:
} }
@@ -1265,6 +1302,27 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
return nil, nil, err return nil, nil, err
} }
// Check if the payments bucket tombstone is set. If it is, we
// need to return and ask the user switch back to using the
// native SQL store.
ripPayments, err := paymentsdb.GetPaymentsBucketTombstone(
dbs.ChanStateDB.Backend,
)
if err != nil {
err = fmt.Errorf("unable to check payments bucket "+
"tombstone: %w", err)
d.logger.Error(err)
return nil, nil, err
}
if ripPayments {
err = fmt.Errorf("payments bucket tombstoned, please " +
"switch back to native SQL")
d.logger.Error(err)
return nil, nil, err
}
dbs.InvoiceDB = dbs.ChanStateDB dbs.InvoiceDB = dbs.ChanStateDB
graphStore, err = graphdb.NewKVStore( graphStore, err = graphdb.NewKVStore(
@@ -217,6 +217,10 @@
db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308) db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308)
* [Finalize SQL implementation for * [Finalize SQL implementation for
payments db](https://github.com/lightningnetwork/lnd/pull/10373) payments db](https://github.com/lightningnetwork/lnd/pull/10373)
* [Add the KV-to-SQL payment
migration](https://github.com/lightningnetwork/lnd/pull/10485) with
comprehensive tests. The migration is currently dev-only, compiled behind
the `test_db_postgres`, `test_db_sqlite`, or `test_native_sql` build tags.
## Code Health ## Code Health
+1 -1
View File
@@ -141,7 +141,7 @@ require (
github.com/opencontainers/runc v1.1.14 // indirect github.com/opencontainers/runc v1.1.14 // indirect
github.com/ory/dockertest/v3 v3.10.0 // indirect github.com/ory/dockertest/v3 v3.10.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect
+7 -8
View File
@@ -31,16 +31,15 @@ func (t *testBatchedSQLQueries) ExecTx(ctx context.Context,
if err != nil { if err != nil {
return err return err
} }
defer func() {
if err != nil {
_ = tx.Rollback()
} else {
err = tx.Commit()
}
}()
reset() reset()
queries := sqlc.New(tx) queries := sqlc.New(tx)
return txBody(queries) if err := txBody(queries); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
} }
+5
View File
@@ -136,6 +136,11 @@ var (
ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " + ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " +
"found") "found")
// ErrNoDuplicateSequenceNumber is returned when a duplicate payment
// sub-bucket does not contain the sequence number key.
ErrNoDuplicateSequenceNumber = errors.New("duplicate payment " +
"sequence number not found")
// ErrNoSequenceNrIndex is returned when an attempt to lookup a payment // ErrNoSequenceNrIndex is returned when an attempt to lookup a payment
// index is made for a sequence number that is not indexed. // index is made for a sequence number that is not indexed.
// //
+1 -1
View File
@@ -1236,7 +1236,7 @@ func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash,
seqBytes := subBucket.Get(duplicatePaymentSequenceKey) seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
if seqBytes == nil { if seqBytes == nil {
return err return ErrNoDuplicateSequenceNumber
} }
// If this duplicate payment is not the sequence number we are // If this duplicate payment is not the sequence number we are
+71
View File
@@ -0,0 +1,71 @@
package paymentsdb
import (
"fmt"
"github.com/lightningnetwork/lnd/kvdb"
)
var (
// paymentsBucketTombstone is the key used to mark the payments bucket
// as permanently closed after a successful migration.
paymentsBucketTombstone = []byte("payments-tombstone")
)
// SetPaymentsBucketTombstone sets the tombstone key in the payments bucket to
// mark the bucket as permanently closed. This prevents it from being reopened
// in the future.
func SetPaymentsBucketTombstone(db kvdb.Backend) error {
return kvdb.Update(db, func(tx kvdb.RwTx) error {
// Access the top-level payments bucket.
payments := tx.ReadWriteBucket(paymentsRootBucket)
// In case the bucket doesn't exist, because we start
// immediately with the native SQL schema, we create it as well
// to make sure the user cannot switch back to the KV store.
if payments == nil {
var err error
payments, err = tx.CreateTopLevelBucket(
paymentsRootBucket,
)
if err != nil {
return fmt.Errorf("failed to create payments "+
"bucket: %w", err)
}
}
// Add the tombstone key to the payments bucket.
err := payments.Put(paymentsBucketTombstone, []byte("1"))
if err != nil {
return fmt.Errorf("failed to set tombstone: %w", err)
}
return nil
}, func() {})
}
// GetPaymentsBucketTombstone checks if the tombstone key exists in the payments
// bucket. It returns true if the tombstone is present and false otherwise.
func GetPaymentsBucketTombstone(db kvdb.Backend) (bool, error) {
var tombstoneExists bool
err := kvdb.View(db, func(tx kvdb.RTx) error {
// Access the top-level payments bucket.
payments := tx.ReadBucket(paymentsRootBucket)
if payments == nil {
tombstoneExists = false
return nil
}
// Check if the tombstone key exists.
tombstone := payments.Get(paymentsBucketTombstone)
tombstoneExists = tombstone != nil
return nil
}, func() {})
if err != nil {
return false, err
}
return tombstoneExists, nil
}
+2
View File
@@ -3,6 +3,7 @@ package paymentsdb
import ( import (
"github.com/btcsuite/btclog/v2" "github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
) )
// log is a logger that is initialized with no output filters. This // log is a logger that is initialized with no output filters. This
@@ -29,4 +30,5 @@ func DisableLog() {
// using btclog. // using btclog.
func UseLogger(logger btclog.Logger) { func UseLogger(logger btclog.Logger) {
log = logger log = logger
paymentsmig1.UseLogger(logger)
} }
+198
View File
@@ -0,0 +1,198 @@
package migration1
import (
"encoding/binary"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
)
// Big endian is the preferred byte order, due to cursor scans over
// integer keys iterating in order.
var byteOrder = binary.BigEndian
// UnknownElementType is an error returned when the codec is unable to encode
// or decode a particular type.
//
// NOTE: This is a frozen, self-contained copy of the serialization error type
// to ensure that this migration package has no dependency on live packages
// whose implementation may change after this migration was released.
type UnknownElementType struct {
method string
element interface{}
}
// NewUnknownElementType creates a new UnknownElementType error from the passed
// method name and element.
func NewUnknownElementType(method string, el interface{}) UnknownElementType {
return UnknownElementType{method: method, element: el}
}
// Error returns the name of the method that encountered the error, as well as
// the type that was unsupported.
func (e UnknownElementType) Error() string {
return fmt.Sprintf("Unknown type in %s: %T", e.method, e.element)
}
// WriteElement serializes a single element into the provided io.Writer using
// big-endian byte order. Only the types required by the migration1 package are
// supported. This is a frozen, self-contained copy of the serialization logic
// to ensure that this migration package has no dependency on live packages
// whose implementation may change after this migration was released.
func WriteElement(w io.Writer, element interface{}) error {
switch e := element.(type) {
case paymentIndexType:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
case uint8:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
case uint32:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
case uint64:
if err := binary.Write(w, byteOrder, e); err != nil {
return err
}
case lnwire.MilliSatoshi:
if err := binary.Write(w, byteOrder, uint64(e)); err != nil {
return err
}
case [32]byte:
if _, err := w.Write(e[:]); err != nil {
return err
}
case []byte:
if err := wire.WriteVarBytes(w, 0, e); err != nil {
return err
}
default:
return UnknownElementType{"WriteElement", e}
}
return nil
}
// WriteElements serializes a variadic list of elements into the given
// io.Writer.
func WriteElements(w io.Writer, elements ...interface{}) error {
for _, element := range elements {
if err := WriteElement(w, element); err != nil {
return err
}
}
return nil
}
// ReadElement deserializes a single element from the provided io.Reader using
// big-endian byte order. Only the types required by the migration1 package are
// supported. This is a frozen, self-contained copy of the deserialization
// logic to ensure that this migration package has no dependency on live
// packages whose implementation may change after this migration was released.
func ReadElement(r io.Reader, element interface{}) error {
switch e := element.(type) {
case *paymentIndexType:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}
case *uint8:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}
case *uint32:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}
case *uint64:
if err := binary.Read(r, byteOrder, e); err != nil {
return err
}
case *lnwire.MilliSatoshi:
var a uint64
if err := binary.Read(r, byteOrder, &a); err != nil {
return err
}
*e = lnwire.MilliSatoshi(a)
case *[32]byte:
if _, err := io.ReadFull(r, e[:]); err != nil {
return err
}
case *[]byte:
b, err := wire.ReadVarBytes(r, 0, 66000, "[]byte")
if err != nil {
return err
}
*e = b
default:
return UnknownElementType{"ReadElement", e}
}
return nil
}
// ReadElements deserializes the provided io.Reader into a variadic list of
// target elements.
func ReadElements(r io.Reader, elements ...interface{}) error {
for _, element := range elements {
if err := ReadElement(r, element); err != nil {
return err
}
}
return nil
}
// deserializeTime deserializes time as unix nanoseconds.
func deserializeTime(r io.Reader) (time.Time, error) {
var scratch [8]byte
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return time.Time{}, err
}
// Convert to time.Time. Interpret unix nano time zero as a zero
// time.Time value.
unixNano := byteOrder.Uint64(scratch[:])
if unixNano == 0 {
return time.Time{}, nil
}
return time.Unix(0, int64(unixNano)), nil
}
// serializeTime serializes time as unix nanoseconds.
func serializeTime(w io.Writer, t time.Time) error {
var scratch [8]byte
// Convert to unix nano seconds, but only if time is non-zero. Calling
// UnixNano() on a zero time yields an undefined result.
var unixNano int64
if !t.IsZero() {
unixNano = t.UnixNano()
}
byteOrder.PutUint64(scratch[:], uint64(unixNano))
_, err := w.Write(scratch[:])
return err
}
+154
View File
@@ -0,0 +1,154 @@
package migration1
import "errors"
var (
// ErrAlreadyPaid signals we have already paid this payment hash.
ErrAlreadyPaid = errors.New("invoice is already paid")
// ErrPaymentInFlight signals that payment for this payment hash is
// already "in flight" on the network.
ErrPaymentInFlight = errors.New("payment is in transition")
// ErrPaymentExists is returned when we try to initialize an already
// existing payment that is not failed.
ErrPaymentExists = errors.New("payment already exists")
// ErrPaymentInternal is returned when performing the payment has a
// conflicting state, such as,
// - payment has StatusSucceeded but remaining amount is not zero.
// - payment has StatusInitiated but remaining amount is zero.
// - payment has StatusFailed but remaining amount is zero.
ErrPaymentInternal = errors.New("internal error")
// ErrPaymentNotInitiated is returned if the payment wasn't initiated.
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
// ErrPaymentAlreadySucceeded is returned in the event we attempt to
// change the status of a payment already succeeded.
ErrPaymentAlreadySucceeded = errors.New("payment is already succeeded")
// ErrPaymentAlreadyFailed is returned in the event we attempt to alter
// a failed payment.
ErrPaymentAlreadyFailed = errors.New("payment has already failed")
// ErrUnknownPaymentStatus is returned when we do not recognize the
// existing state of a payment.
ErrUnknownPaymentStatus = errors.New("unknown payment status")
// ErrPaymentTerminal is returned if we attempt to alter a payment that
// already has reached a terminal condition.
ErrPaymentTerminal = errors.New("payment has reached terminal " +
"condition")
// ErrAttemptAlreadySettled is returned if we try to alter an already
// settled HTLC attempt.
ErrAttemptAlreadySettled = errors.New("attempt already settled")
// ErrAttemptAlreadyFailed is returned if we try to alter an already
// failed HTLC attempt.
ErrAttemptAlreadyFailed = errors.New("attempt already failed")
// ErrValueMismatch is returned if we try to register a non-MPP attempt
// with an amount that doesn't match the payment amount.
ErrValueMismatch = errors.New("attempted value doesn't match payment " +
"amount")
// ErrValueExceedsAmt is returned if we try to register an attempt that
// would take the total sent amount above the payment amount.
ErrValueExceedsAmt = errors.New("attempted value exceeds payment " +
"amount")
// ErrNonMPPayment is returned if we try to register an MPP attempt for
// a payment that already has a non-MPP attempt registered.
ErrNonMPPayment = errors.New("payment has non-MPP attempts")
// ErrMPPayment is returned if we try to register a non-MPP attempt for
// a payment that already has an MPP attempt registered.
ErrMPPayment = errors.New("payment has MPP attempts")
// ErrMPPRecordInBlindedPayment is returned if we try to register an
// attempt with an MPP record for a payment to a blinded path.
ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " +
"contain MPP records")
// ErrBlindedPaymentTotalAmountMismatch is returned if we try to
// register an HTLC shard to a blinded route where the total amount
// doesn't match existing shards.
ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " +
"total amount mismatch")
// ErrMixedBlindedAndNonBlindedPayments is returned if we try to
// register a non-blinded attempt to a payment which uses a blinded
// paths or vice versa.
ErrMixedBlindedAndNonBlindedPayments = errors.New("mixed blinded and " +
"non-blinded payments")
// ErrBlindedPaymentMissingTotalAmount is returned if we try to
// register a blinded payment attempt where the final hop doesn't set
// the total amount.
ErrBlindedPaymentMissingTotalAmount = errors.New("blinded payment " +
"final hop must set total amount")
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
// shard where the payment address doesn't match existing shards.
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
// ErrMPPTotalAmountMismatch is returned if we try to register an MPP
// shard where the total amount doesn't match existing shards.
ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " +
"mismatch")
// ErrPaymentPendingSettled is returned when we try to add a new
// attempt to a payment that has at least one of its HTLCs settled.
ErrPaymentPendingSettled = errors.New("payment has settled htlcs")
// ErrPaymentPendingFailed is returned when we try to add a new attempt
// to a payment that already has a failure reason.
ErrPaymentPendingFailed = errors.New("payment has failure reason")
// ErrSentExceedsTotal is returned if the payment's current total sent
// amount exceed the total amount.
ErrSentExceedsTotal = errors.New("total sent exceeds total amount")
// ErrNoAttemptInfo is returned when no attempt info is stored yet.
ErrNoAttemptInfo = errors.New("unable to find attempt info for " +
"inflight payment")
)
// KV backend specific errors.
var (
// ErrNoSequenceNumber is returned if we look up a payment which does
// not have a sequence number.
ErrNoSequenceNumber = errors.New("sequence number not found")
// ErrDuplicateNotFound is returned when we lookup a payment by its
// index and cannot find a payment with a matching sequence number.
ErrDuplicateNotFound = errors.New("duplicate payment not found")
// ErrNoDuplicateBucket is returned when we expect to find duplicates
// when looking up a payment from its index, but the payment does not
// have any.
ErrNoDuplicateBucket = errors.New("expected duplicate bucket")
// ErrNoDuplicateNestedBucket is returned if we do not find duplicate
// payments in their own sub-bucket.
ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " +
"found")
// ErrNoDuplicateSequenceNumber is returned when a duplicate payment
// sub-bucket does not contain the sequence number key.
ErrNoDuplicateSequenceNumber = errors.New("duplicate payment " +
"sequence number not found")
// ErrNoSequenceNrIndex is returned when an attempt to lookup a payment
// index is made for a sequence number that is not indexed.
//
// NOTE: Only used for the kv backend.
ErrNoSequenceNrIndex = errors.New("payment sequence number index " +
"does not exist")
// errMaxPaymentsReached is used internally to signal that the maximum
// number of payments has been reached during a paginated query.
errMaxPaymentsReached = errors.New("max payments reached")
)
+140
View File
@@ -0,0 +1,140 @@
package migration1
import (
"context"
"github.com/lightningnetwork/lnd/lntypes"
)
// DB represents the interface to the underlying payments database.
type DB interface {
PaymentReader
PaymentWriter
}
// PaymentReader represents the interface to read operations from the payments
// database.
type PaymentReader interface {
// QueryPayments queries the payments database and should support
// pagination.
QueryPayments(ctx context.Context, query Query) (Response, error)
// FetchPayment fetches the payment corresponding to the given payment
// hash.
FetchPayment(ctx context.Context,
paymentHash lntypes.Hash) (*MPPayment, error)
// FetchInFlightPayments returns all payments with status InFlight.
FetchInFlightPayments(ctx context.Context) ([]*MPPayment, error)
}
// PaymentWriter represents the interface to write operations to the payments
// database.
type PaymentWriter interface {
// DeletePayment deletes a payment from the DB given its payment hash.
DeletePayment(ctx context.Context, paymentHash lntypes.Hash,
failedAttemptsOnly bool) error
// DeletePayments deletes all payments from the DB given the specified
// flags.
DeletePayments(ctx context.Context, failedOnly,
failedAttemptsOnly bool) (int, error)
PaymentControl
}
// PaymentControl represents the interface to control the payment lifecycle and
// its database operations. This interface represents the control flow of how
// a payment should be handled in the database. They are not just writing
// operations but they inherently represent the flow of a payment. The methods
// are called in the following order.
//
// 1. InitPayment.
// 2. RegisterAttempt (a payment can have multiple attempts).
// 3. SettleAttempt or FailAttempt (attempts can also fail as long as the
// sending amount will be eventually settled).
// 4. Payment succeeds or "Fail" is called.
// 5. DeleteFailedAttempts is called which will delete all failed attempts
// for a payment to clean up the database.
type PaymentControl interface {
// InitPayment checks that no other payment with the same payment hash
// exists in the database before creating a new payment. However, it
// should allow the user making a subsequent payment if the payment is
// in a Failed state.
InitPayment(context.Context, lntypes.Hash, *PaymentCreationInfo) error
// RegisterAttempt atomically records the provided HTLCAttemptInfo.
//
// IMPORTANT: Callers MUST serialize calls to RegisterAttempt for the
// same payment hash. Concurrent calls will result in race conditions
// where both calls read the same initial payment state, validate
// against stale data, and could cause overpayment. For example:
// - Both goroutines fetch payment with 400 sats sent
// - Both validate sending 650 sats won't overpay (within limit)
// - Both commit successfully
// - Result: 1700 sats sent, exceeding the payment amount
// The payment router/controller layer is responsible for ensuring
// serialized access per payment hash.
RegisterAttempt(context.Context, lntypes.Hash,
*HTLCAttemptInfo) (*MPPayment, error)
// SettleAttempt marks the given attempt settled with the preimage. If
// this is a multi shard payment, this might implicitly mean the
// full payment succeeded.
//
// After invoking this method, InitPayment should always return an
// error to prevent us from making duplicate payments to the same
// payment hash. The provided preimage is atomically saved to the DB
// for record keeping.
SettleAttempt(context.Context, lntypes.Hash, uint64,
*HTLCSettleInfo) (*MPPayment, error)
// FailAttempt marks the given payment attempt failed.
FailAttempt(context.Context, lntypes.Hash, uint64,
*HTLCFailInfo) (*MPPayment, error)
// Fail transitions a payment into the Failed state, and records
// the ultimate reason the payment failed. Note that this should only
// be called when all active attempts are already failed. After
// invoking this method, InitPayment should return nil on its next call
// for this payment hash, allowing the user to make a subsequent
// payment.
Fail(context.Context, lntypes.Hash, FailureReason) (*MPPayment, error)
// DeleteFailedAttempts removes all failed HTLCs from the db. It should
// be called for a given payment whenever all inflight htlcs are
// completed, and the payment has reached a final terminal state.
DeleteFailedAttempts(context.Context, lntypes.Hash) error
}
// DBMPPayment is an interface that represents the payment state during a
// payment lifecycle.
type DBMPPayment interface {
// GetState returns the current state of the payment.
GetState() *MPPaymentState
// Terminated returns true if the payment is in a final state.
Terminated() bool
// GetStatus returns the current status of the payment.
GetStatus() PaymentStatus
// NeedWaitAttempts specifies whether the payment needs to wait for the
// outcome of an attempt.
NeedWaitAttempts() (bool, error)
// GetHTLCs returns all HTLCs of this payment.
GetHTLCs() []HTLCAttempt
// InFlightHTLCs returns all HTLCs that are in flight.
InFlightHTLCs() []HTLCAttempt
// AllowMoreAttempts is used to decide whether we can safely attempt
// more HTLCs for a given payment state. Return an error if the payment
// is in an unexpected state.
AllowMoreAttempts() (bool, error)
// TerminalInfo returns the settled HTLC attempt or the payment's
// failure reason.
TerminalInfo() (*HTLCAttempt, *FailureReason)
}
@@ -0,0 +1,249 @@
package migration1
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
)
var (
// duplicatePaymentsBucket is the name of a optional sub-bucket within
// the payment hash bucket, that is used to hold duplicate payments to a
// payment hash. This is needed to support information from earlier
// versions of lnd, where it was possible to pay to a payment hash more
// than once.
duplicatePaymentsBucket = []byte("payment-duplicate-bucket")
// duplicatePaymentSettleInfoKey is a key used in the payment's
// sub-bucket to store the settle info of the payment.
duplicatePaymentSettleInfoKey = []byte("payment-settle-info")
// duplicatePaymentAttemptInfoKey is a key used in the payment's
// sub-bucket to store the info about the latest attempt that was done
// for the payment in question.
duplicatePaymentAttemptInfoKey = []byte("payment-attempt-info")
// duplicatePaymentCreationInfoKey is a key used in the payment's
// sub-bucket to store the creation info of the payment.
duplicatePaymentCreationInfoKey = []byte("payment-creation-info")
// duplicatePaymentFailInfoKey is a key used in the payment's sub-bucket
// to store information about the reason a payment failed.
duplicatePaymentFailInfoKey = []byte("payment-fail-info")
// duplicatePaymentSequenceKey is a key used in the payment's sub-bucket
// to store the sequence number of the payment.
duplicatePaymentSequenceKey = []byte("payment-sequence-key")
)
// duplicateHTLCAttemptInfo contains static information about a specific HTLC
// attempt for a payment. This information is used by the router to handle any
// errors coming back after an attempt is made, and to query the switch about
// the status of the attempt.
type duplicateHTLCAttemptInfo struct {
// attemptID is the unique ID used for this attempt.
attemptID uint64
// sessionKey is the ephemeral key used for this attempt.
sessionKey [btcec.PrivKeyBytesLen]byte
// route is the route attempted to send the HTLC.
route Route
}
// fetchDuplicatePaymentStatus fetches the payment status of the payment. If
// the payment isn't found, it will return error `ErrPaymentNotInitiated`.
func fetchDuplicatePaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) {
if bucket.Get(duplicatePaymentSettleInfoKey) != nil {
return StatusSucceeded, nil
}
if bucket.Get(duplicatePaymentFailInfoKey) != nil {
return StatusFailed, nil
}
if bucket.Get(duplicatePaymentCreationInfoKey) != nil {
return StatusInFlight, nil
}
return 0, ErrPaymentNotInitiated
}
func deserializeDuplicateHTLCAttemptInfo(r io.Reader) (
*duplicateHTLCAttemptInfo, error) {
a := &duplicateHTLCAttemptInfo{}
err := ReadElements(r, &a.attemptID, &a.sessionKey)
if err != nil {
return nil, err
}
a.route, err = DeserializeRoute(r)
if err != nil {
return nil, err
}
return a, nil
}
func deserializeDuplicatePaymentCreationInfo(r io.Reader) (
*PaymentCreationInfo, error) {
var scratch [8]byte
c := &PaymentCreationInfo{}
if _, err := io.ReadFull(r, c.PaymentIdentifier[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return nil, err
}
c.Value = lnwire.MilliSatoshi(byteOrder.Uint64(scratch[:]))
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return nil, err
}
c.CreationTime = time.Unix(int64(byteOrder.Uint64(scratch[:])), 0)
if _, err := io.ReadFull(r, scratch[:4]); err != nil {
return nil, err
}
reqLen := byteOrder.Uint32(scratch[:4])
payReq := make([]byte, reqLen)
if reqLen > 0 {
if _, err := io.ReadFull(r, payReq); err != nil {
return nil, err
}
}
c.PaymentRequest = payReq
return c, nil
}
func fetchDuplicatePayment(bucket kvdb.RBucket) (*MPPayment, error) {
seqBytes := bucket.Get(duplicatePaymentSequenceKey)
if seqBytes == nil {
return nil, fmt.Errorf("sequence number not found")
}
sequenceNum := binary.BigEndian.Uint64(seqBytes)
// Get the payment status.
paymentStatus, err := fetchDuplicatePaymentStatus(bucket)
if err != nil {
return nil, err
}
// Get the PaymentCreationInfo.
b := bucket.Get(duplicatePaymentCreationInfoKey)
if b == nil {
return nil, fmt.Errorf("creation info not found")
}
r := bytes.NewReader(b)
creationInfo, err := deserializeDuplicatePaymentCreationInfo(r)
if err != nil {
return nil, err
}
// Get failure reason if available.
var failureReason *FailureReason
b = bucket.Get(duplicatePaymentFailInfoKey)
if b != nil {
reason := FailureReason(b[0])
failureReason = &reason
}
payment := &MPPayment{
SequenceNum: sequenceNum,
Info: creationInfo,
FailureReason: failureReason,
Status: paymentStatus,
}
// Get the HTLCAttemptInfo. It can be absent.
b = bucket.Get(duplicatePaymentAttemptInfoKey)
if b != nil {
r = bytes.NewReader(b)
attempt, err := deserializeDuplicateHTLCAttemptInfo(r)
if err != nil {
return nil, err
}
htlc := HTLCAttempt{
HTLCAttemptInfo: HTLCAttemptInfo{
AttemptID: attempt.attemptID,
Route: attempt.route,
sessionKey: attempt.sessionKey,
},
}
// Get the payment preimage. This is only found for
// successful payments.
b = bucket.Get(duplicatePaymentSettleInfoKey)
if b != nil {
var preimg lntypes.Preimage
copy(preimg[:], b)
htlc.Settle = &HTLCSettleInfo{
Preimage: preimg,
SettleTime: time.Time{},
}
} else {
// Otherwise the payment must have failed.
htlc.Failure = &HTLCFailInfo{
FailTime: time.Time{},
}
}
payment.HTLCs = []HTLCAttempt{htlc}
}
return payment, nil
}
func fetchDuplicatePayments(paymentHashBucket kvdb.RBucket) ([]*MPPayment,
error) {
var payments []*MPPayment
// For older versions of lnd, duplicate payments to a payment has was
// possible. These will be found in a sub-bucket indexed by their
// sequence number if available.
dup := paymentHashBucket.NestedReadBucket(duplicatePaymentsBucket)
if dup == nil {
return nil, nil
}
err := dup.ForEach(func(k, v []byte) error {
subBucket := dup.NestedReadBucket(k)
if subBucket == nil {
// We one bucket for each duplicate to be found.
return fmt.Errorf("non bucket element" +
"in duplicate bucket")
}
p, err := fetchDuplicatePayment(subBucket)
if err != nil {
return err
}
payments = append(payments, p)
return nil
})
if err != nil {
return nil, err
}
return payments, nil
}
File diff suppressed because it is too large. Load diff
+127
View File
@@ -0,0 +1,127 @@
package lnwire
import (
"encoding/binary"
"encoding/hex"
"io"
"math"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// MaxFundingTxOutputs is the maximum number of allowed outputs on a
// funding transaction within the protocol. This is due to the fact
// that we use 2-bytes to encode the index within the funding output
// during the funding workflow. Funding transaction with more outputs
// than this are considered invalid within the protocol.
MaxFundingTxOutputs = math.MaxUint16
)
// ChannelID is a series of 32-bytes that uniquely identifies all channels
// within the network. The ChannelID is computed using the outpoint of the
// funding transaction (the txid, and output index). Given a funding output the
// ChannelID can be calculated by XOR'ing the big-endian serialization of the
// txid and the big-endian serialization of the output index, truncated to
// 2 bytes.
type ChannelID [32]byte
// ConnectionWideID is an all-zero ChannelID, which is used to represent a
// message intended for all channels to specific peer.
var ConnectionWideID = ChannelID{}
// String returns the string representation of the ChannelID. This is just the
// hex string encoding of the ChannelID itself.
func (c ChannelID) String() string {
return hex.EncodeToString(c[:])
}
// Record returns a TLV record that can be used to encode/decode a ChannelID
// to/from a TLV stream.
func (c *ChannelID) Record() tlv.Record {
return tlv.MakeStaticRecord(0, c, 32, encodeChannelID, decodeChannelID)
}
func encodeChannelID(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*ChannelID); ok {
bigSize := [32]byte(*v)
return tlv.EBytes32(w, &bigSize, buf)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.ChannelID")
}
func decodeChannelID(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*ChannelID); ok {
var id [32]byte
err := tlv.DBytes32(r, &id, buf, l)
if err != nil {
return err
}
*v = id
return nil
}
return tlv.NewTypeForDecodingErr(val, "lnwire.ChannelID", l, l)
}
// NewChanIDFromOutPoint converts a target OutPoint into a ChannelID that is
// usable within the network. In order to convert the OutPoint into a ChannelID,
// we XOR the lower 2-bytes of the txid within the OutPoint with the big-endian
// serialization of the Index of the OutPoint, truncated to 2-bytes.
func NewChanIDFromOutPoint(op wire.OutPoint) ChannelID {
// First we'll copy the txid of the outpoint into our channel ID slice.
var cid ChannelID
copy(cid[:], op.Hash[:])
// With the txid copied over, we'll now XOR the lower 2-bytes of the
// partial channelID with big-endian serialization of output index.
xorTxid(&cid, uint16(op.Index))
return cid
}
// xorTxid performs the transformation needed to transform an OutPoint into a
// ChannelID. To do this, we expect the cid parameter to contain the txid
// unaltered and the outputIndex to be the output index
func xorTxid(cid *ChannelID, outputIndex uint16) {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], outputIndex)
cid[30] ^= buf[0]
cid[31] ^= buf[1]
}
// GenPossibleOutPoints generates all the possible outputs given a channel ID.
// In order to generate these possible outpoints, we perform a brute-force
// search through the candidate output index space, performing a reverse
// mapping from channelID back to OutPoint.
func (c *ChannelID) GenPossibleOutPoints() [MaxFundingTxOutputs]wire.OutPoint {
var possiblePoints [MaxFundingTxOutputs]wire.OutPoint
for i := uint16(0); i < MaxFundingTxOutputs; i++ {
cidCopy := *c
xorTxid(&cidCopy, i)
possiblePoints[i] = wire.OutPoint{
Hash: chainhash.Hash(cidCopy),
Index: uint32(i),
}
}
return possiblePoints
}
// IsChanPoint returns true if the OutPoint passed corresponds to the target
// ChannelID.
func (c ChannelID) IsChanPoint(op *wire.OutPoint) bool {
candidateCid := NewChanIDFromOutPoint(*op)
return candidateCid == c
}
@@ -0,0 +1,422 @@
package lnwire
import (
"bytes"
"fmt"
"io"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/tlv"
)
// ChanUpdateMsgFlags is a bitfield that signals whether optional fields are
// present in the ChannelUpdate.
type ChanUpdateMsgFlags uint8
const (
// ChanUpdateRequiredMaxHtlc is a bit that indicates whether the
// required htlc_maximum_msat field is present in this ChannelUpdate.
ChanUpdateRequiredMaxHtlc ChanUpdateMsgFlags = 1 << iota
)
// String returns the bitfield flags as a string.
func (c ChanUpdateMsgFlags) String() string {
return fmt.Sprintf("%08b", c)
}
// HasMaxHtlc returns true if the htlc_maximum_msat option bit is set in the
// message flags.
func (c ChanUpdateMsgFlags) HasMaxHtlc() bool {
return c&ChanUpdateRequiredMaxHtlc != 0
}
// ChanUpdateChanFlags is a bitfield that signals various options concerning a
// particular channel edge. Each bit is to be examined in order to determine
// how the ChannelUpdate message is to be interpreted.
type ChanUpdateChanFlags uint8
const (
// ChanUpdateDirection indicates the direction of a channel update. If
// this bit is set to 0 if Node1 (the node with the "smaller" Node ID)
// is updating the channel, and to 1 otherwise.
ChanUpdateDirection ChanUpdateChanFlags = 1 << iota
// ChanUpdateDisabled is a bit that indicates if the channel edge
// selected by the ChanUpdateDirection bit is to be treated as being
// disabled.
ChanUpdateDisabled
)
// IsDisabled determines whether the channel flags has the disabled bit set.
func (c ChanUpdateChanFlags) IsDisabled() bool {
return c&ChanUpdateDisabled == ChanUpdateDisabled
}
// String returns the bitfield flags as a string.
func (c ChanUpdateChanFlags) String() string {
return fmt.Sprintf("%08b", c)
}
// ChannelUpdate1 message is used after channel has been initially announced.
// Each side independently announces its fees and minimum expiry for HTLCs and
// other parameters. Also this message is used to redeclare initially set
// channel parameters.
type ChannelUpdate1 struct {
// Signature is used to validate the announced data and prove the
// ownership of node id.
Signature Sig
// ChainHash denotes the target chain that this channel was opened
// within. This value should be the genesis hash of the target chain.
// Along with the short channel ID, this uniquely identifies the
// channel globally in a blockchain.
ChainHash chainhash.Hash
// ShortChannelID is the unique description of the funding transaction.
ShortChannelID ShortChannelID
// Timestamp allows ordering in the case of multiple announcements. We
// should ignore the message if timestamp is not greater than
// the last-received.
Timestamp uint32
// MessageFlags is a bitfield that describes whether optional fields
// are present in this update. Currently, the least-significant bit
// must be set to 1 if the optional field MaxHtlc is present.
MessageFlags ChanUpdateMsgFlags
// ChannelFlags is a bitfield that describes additional meta-data
// concerning how the update is to be interpreted. Currently, the
// least-significant bit must be set to 0 if the creating node
// corresponds to the first node in the previously sent channel
// announcement and 1 otherwise. If the second bit is set, then the
// channel is set to be disabled.
ChannelFlags ChanUpdateChanFlags
// TimeLockDelta is the minimum number of blocks this node requires to
// be added to the expiry of HTLCs. This is a security parameter
// determined by the node operator. This value represents the required
// gap between the time locks of the incoming and outgoing HTLC's set
// to this node.
TimeLockDelta uint16
// HtlcMinimumMsat is the minimum HTLC value which will be accepted.
HtlcMinimumMsat MilliSatoshi
// BaseFee is the base fee that must be used for incoming HTLC's to
// this particular channel. This value will be tacked onto the required
// for a payment independent of the size of the payment.
BaseFee uint32
// FeeRate is the fee rate that will be charged per millionth of a
// satoshi.
FeeRate uint32
// HtlcMaximumMsat is the maximum HTLC value which will be accepted.
HtlcMaximumMsat MilliSatoshi
// InboundFee is an optional TLV record that contains the fee
// information for incoming HTLCs.
InboundFee tlv.OptionalRecordT[tlv.TlvType55555, Fee]
// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
ExtraOpaqueData ExtraOpaqueData
}
// A compile time check to ensure ChannelUpdate implements the lnwire.Message
// interface.
var _ Message = (*ChannelUpdate1)(nil)
// A compile time check to ensure ChannelUpdate1 implements the
// lnwire.SizeableMessage interface.
var _ SizeableMessage = (*ChannelUpdate1)(nil)
// Decode deserializes a serialized ChannelUpdate stored in the passed
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (a *ChannelUpdate1) Decode(r io.Reader, _ uint32) error {
err := ReadElements(r,
&a.Signature,
a.ChainHash[:],
&a.ShortChannelID,
&a.Timestamp,
&a.MessageFlags,
&a.ChannelFlags,
&a.TimeLockDelta,
&a.HtlcMinimumMsat,
&a.BaseFee,
&a.FeeRate,
)
if err != nil {
return err
}
// Now check whether the max HTLC field is present and read it if so.
if a.MessageFlags.HasMaxHtlc() {
if err := ReadElements(r, &a.HtlcMaximumMsat); err != nil {
return err
}
}
var tlvRecords ExtraOpaqueData
if err := ReadElements(r, &tlvRecords); err != nil {
return err
}
var inboundFee = a.InboundFee.Zero()
typeMap, err := tlvRecords.ExtractRecords(&inboundFee)
if err != nil {
return fmt.Errorf("%w: %w", ErrParsingExtraTLVBytes, err)
}
val, ok := typeMap[a.InboundFee.TlvType()]
if ok && val == nil {
a.InboundFee = tlv.SomeRecordT(inboundFee)
}
if len(tlvRecords) != 0 {
a.ExtraOpaqueData = tlvRecords
}
return nil
}
// Encode serializes the target ChannelUpdate into the passed io.Writer
// observing the protocol version specified.
//
// This is part of the lnwire.Message interface.
func (a *ChannelUpdate1) Encode(w *bytes.Buffer, pver uint32) error {
if err := WriteSig(w, a.Signature); err != nil {
return err
}
if err := WriteBytes(w, a.ChainHash[:]); err != nil {
return err
}
if err := WriteShortChannelID(w, a.ShortChannelID); err != nil {
return err
}
if err := WriteUint32(w, a.Timestamp); err != nil {
return err
}
if err := WriteChanUpdateMsgFlags(w, a.MessageFlags); err != nil {
return err
}
if err := WriteChanUpdateChanFlags(w, a.ChannelFlags); err != nil {
return err
}
if err := WriteUint16(w, a.TimeLockDelta); err != nil {
return err
}
if err := WriteMilliSatoshi(w, a.HtlcMinimumMsat); err != nil {
return err
}
if err := WriteUint32(w, a.BaseFee); err != nil {
return err
}
if err := WriteUint32(w, a.FeeRate); err != nil {
return err
}
// Now append optional fields if they are set. Currently, the only
// optional field is max HTLC.
if a.MessageFlags.HasMaxHtlc() {
err := WriteMilliSatoshi(w, a.HtlcMaximumMsat)
if err != nil {
return err
}
}
recordProducers := make([]tlv.RecordProducer, 0, 1)
a.InboundFee.WhenSome(func(fee tlv.RecordT[tlv.TlvType55555, Fee]) {
recordProducers = append(recordProducers, &fee)
})
err := EncodeMessageExtraData(&a.ExtraOpaqueData, recordProducers...)
if err != nil {
return err
}
// Finally, append any extra opaque data.
return WriteBytes(w, a.ExtraOpaqueData)
}
// MsgType returns the integer uniquely identifying this message type on the
// wire.
//
// This is part of the lnwire.Message interface.
func (a *ChannelUpdate1) MsgType() MessageType {
return MsgChannelUpdate
}
// DataToSign is used to retrieve part of the announcement message which should
// be signed.
func (a *ChannelUpdate1) DataToSign() ([]byte, error) {
// We should not include the signatures itself.
b := make([]byte, 0, MaxMsgBody)
buf := bytes.NewBuffer(b)
if err := WriteBytes(buf, a.ChainHash[:]); err != nil {
return nil, err
}
if err := WriteShortChannelID(buf, a.ShortChannelID); err != nil {
return nil, err
}
if err := WriteUint32(buf, a.Timestamp); err != nil {
return nil, err
}
if err := WriteChanUpdateMsgFlags(buf, a.MessageFlags); err != nil {
return nil, err
}
if err := WriteChanUpdateChanFlags(buf, a.ChannelFlags); err != nil {
return nil, err
}
if err := WriteUint16(buf, a.TimeLockDelta); err != nil {
return nil, err
}
if err := WriteMilliSatoshi(buf, a.HtlcMinimumMsat); err != nil {
return nil, err
}
if err := WriteUint32(buf, a.BaseFee); err != nil {
return nil, err
}
if err := WriteUint32(buf, a.FeeRate); err != nil {
return nil, err
}
// Now append optional fields if they are set. Currently, the only
// optional field is max HTLC.
if a.MessageFlags.HasMaxHtlc() {
err := WriteMilliSatoshi(buf, a.HtlcMaximumMsat)
if err != nil {
return nil, err
}
}
// Finally, append any extra opaque data.
if err := WriteBytes(buf, a.ExtraOpaqueData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// SCID returns the ShortChannelID of the channel that the update applies to.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) SCID() ShortChannelID {
return a.ShortChannelID
}
// IsNode1 is true if the update was produced by node 1 of the channel peers.
// Node 1 is the node with the lexicographically smaller public key.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) IsNode1() bool {
return a.ChannelFlags&ChanUpdateDirection == 0
}
// IsDisabled is true if the update is announcing that the channel should be
// considered disabled.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) IsDisabled() bool {
return a.ChannelFlags&ChanUpdateDisabled == ChanUpdateDisabled
}
// GetChainHash returns the hash of the chain that the message is referring to.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) GetChainHash() chainhash.Hash {
return a.ChainHash
}
// ForwardingPolicy returns the set of forwarding constraints of the update.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) ForwardingPolicy() *ForwardingPolicy {
return &ForwardingPolicy{
TimeLockDelta: a.TimeLockDelta,
BaseFee: MilliSatoshi(a.BaseFee),
FeeRate: MilliSatoshi(a.FeeRate),
MinHTLC: a.HtlcMinimumMsat,
HasMaxHTLC: a.MessageFlags.HasMaxHtlc(),
MaxHTLC: a.HtlcMaximumMsat,
}
}
// GossipVersion returns the gossip version that this message is part of.
//
// NOTE: this is part of the GossipMessage interface.
func (a *ChannelUpdate1) GossipVersion() GossipVersion {
return GossipVersion1
}
// CmpAge can be used to determine if the update is older or newer than the
// passed update. It returns 1 if this update is newer, -1 if it is older, and
// 0 if they are the same age.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) CmpAge(update ChannelUpdate) (CompareResult, error) {
other, ok := update.(*ChannelUpdate1)
if !ok {
return 0, fmt.Errorf("expected *ChannelUpdate1, got: %T",
update)
}
switch {
case a.Timestamp > other.Timestamp:
return GreaterThan, nil
case a.Timestamp < other.Timestamp:
return LessThan, nil
default:
return EqualTo, nil
}
}
// SetDisabledFlag can be used to adjust the disabled flag of an update.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) SetDisabledFlag(disabled bool) {
if disabled {
a.ChannelFlags |= ChanUpdateDisabled
} else {
a.ChannelFlags &= ^ChanUpdateDisabled
}
}
// SetSCID can be used to overwrite the SCID of the update.
//
// NOTE: this is part of the ChannelUpdate interface.
func (a *ChannelUpdate1) SetSCID(scid ShortChannelID) {
a.ShortChannelID = scid
}
// A compile time assertion to ensure ChannelUpdate1 implements the
// ChannelUpdate interface.
var _ ChannelUpdate = (*ChannelUpdate1)(nil)
// SerializedSize returns the serialized size of the message in bytes.
//
// This is part of the lnwire.SizeableMessage interface.
func (a *ChannelUpdate1) SerializedSize() (uint32, error) {
return MessageSerializedSize(a)
}
@@ -0,0 +1,278 @@
package lnwire
import (
"bytes"
"fmt"
"io"
"maps"
"sort"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// MinCustomRecordsTlvType is the minimum custom records TLV type as
// defined in BOLT 01.
MinCustomRecordsTlvType = 65536
)
// CustomRecords stores a set of custom key/value pairs. Map keys are TLV types
// which must be greater than or equal to MinCustomRecordsTlvType.
type CustomRecords map[uint64][]byte
// NewCustomRecords creates a new CustomRecords instance from a
// tlv.TypeMap.
func NewCustomRecords(tlvMap tlv.TypeMap) (CustomRecords, error) {
// Make comparisons in unit tests easy by returning nil if the map is
// empty.
if len(tlvMap) == 0 {
return nil, nil
}
customRecords := make(CustomRecords, len(tlvMap))
for k, v := range tlvMap {
customRecords[uint64(k)] = v
}
// Validate the custom records.
err := customRecords.Validate()
if err != nil {
return nil, fmt.Errorf("custom records from tlv map "+
"validation error: %w", err)
}
return customRecords, nil
}
// ParseCustomRecords creates a new CustomRecords instance from a tlv.Blob.
func ParseCustomRecords(b tlv.Blob) (CustomRecords, error) {
return ParseCustomRecordsFrom(bytes.NewReader(b))
}
// ParseCustomRecordsFrom creates a new CustomRecords instance from a reader.
func ParseCustomRecordsFrom(r io.Reader) (CustomRecords, error) {
typeMap, err := DecodeRecords(r)
if err != nil {
return nil, fmt.Errorf("error decoding HTLC record: %w", err)
}
return NewCustomRecords(typeMap)
}
// Validate checks that all custom records are in the custom type range.
func (c CustomRecords) Validate() error {
if c == nil {
return nil
}
for key := range c {
if key < MinCustomRecordsTlvType {
return fmt.Errorf("custom records entry with TLV "+
"type below min: %d", MinCustomRecordsTlvType)
}
}
return nil
}
// Copy returns a copy of the custom records.
func (c CustomRecords) Copy() CustomRecords {
if c == nil {
return nil
}
customRecords := make(CustomRecords, len(c))
for k, v := range c {
customRecords[k] = v
}
return customRecords
}
// MergedCopy creates a copy of the records and merges them with the given
// records. If the same key is present in both sets, the value from the other
// records will be used.
func (c CustomRecords) MergedCopy(other CustomRecords) CustomRecords {
copiedRecords := make(CustomRecords, len(c))
maps.Copy(copiedRecords, c)
maps.Copy(copiedRecords, other)
return copiedRecords
}
// ExtendRecordProducers extends the given records slice with the custom
// records. The resultant records slice will be sorted if the given records
// slice contains TLV types greater than or equal to MinCustomRecordsTlvType.
func (c CustomRecords) ExtendRecordProducers(
producers []tlv.RecordProducer) ([]tlv.RecordProducer, error) {
// If the custom records are nil or empty, there is nothing to do.
if len(c) == 0 {
return producers, nil
}
// Validate the custom records.
err := c.Validate()
if err != nil {
return nil, err
}
// Ensure that the existing records slice TLV types are not also present
// in the custom records. If they are, the resultant extended records
// slice would erroneously contain duplicate TLV types.
for _, rp := range producers {
record := rp.Record()
recordTlvType := uint64(record.Type())
_, foundDuplicateTlvType := c[recordTlvType]
if foundDuplicateTlvType {
return nil, fmt.Errorf("custom records contains a TLV "+
"type that is already present in the "+
"existing records: %d", recordTlvType)
}
}
// Convert the custom records map to a TLV record producer slice and
// append them to the exiting records slice.
customRecordProducers := RecordsAsProducers(tlv.MapToRecords(c))
producers = append(producers, customRecordProducers...)
// If the records slice which was given as an argument included TLV
// values greater than or equal to the minimum custom records TLV type
// we will sort the extended records slice to ensure that it is ordered
// correctly.
SortProducers(producers)
return producers, nil
}
// RecordProducers returns a slice of record producers for the custom records.
func (c CustomRecords) RecordProducers() []tlv.RecordProducer {
// If the custom records are nil or empty, return an empty slice.
if len(c) == 0 {
return nil
}
// Convert the custom records map to a TLV record producer slice.
records := tlv.MapToRecords(c)
return RecordsAsProducers(records)
}
// Serialize serializes the custom records into a byte slice.
func (c CustomRecords) Serialize() ([]byte, error) {
records := tlv.MapToRecords(c)
return EncodeRecords(records)
}
// SerializeTo serializes the custom records into the given writer.
func (c CustomRecords) SerializeTo(w io.Writer) error {
records := tlv.MapToRecords(c)
return EncodeRecordsTo(w, records)
}
// ProduceRecordsSorted converts a slice of record producers into a slice of
// records and then sorts it by type.
func ProduceRecordsSorted(recordProducers ...tlv.RecordProducer) []tlv.Record {
records := fn.Map(
recordProducers,
func(producer tlv.RecordProducer) tlv.Record {
return producer.Record()
},
)
// Ensure that the set of records are sorted before we attempt to
// decode from the stream, to ensure they're canonical.
tlv.SortRecords(records)
return records
}
// SortProducers sorts the given record producers by their type.
func SortProducers(producers []tlv.RecordProducer) {
sort.Slice(producers, func(i, j int) bool {
recordI := producers[i].Record()
recordJ := producers[j].Record()
return recordI.Type() < recordJ.Type()
})
}
// TlvMapToRecords converts a TLV map into a slice of records.
func TlvMapToRecords(tlvMap tlv.TypeMap) []tlv.Record {
tlvMapGeneric := make(map[uint64][]byte)
for k, v := range tlvMap {
tlvMapGeneric[uint64(k)] = v
}
return tlv.MapToRecords(tlvMapGeneric)
}
// RecordsAsProducers converts a slice of records into a slice of record
// producers.
func RecordsAsProducers(records []tlv.Record) []tlv.RecordProducer {
return fn.Map(records, func(record tlv.Record) tlv.RecordProducer {
return &record
})
}
// EncodeRecords encodes the given records into a byte slice.
func EncodeRecords(records []tlv.Record) ([]byte, error) {
var buf bytes.Buffer
if err := EncodeRecordsTo(&buf, records); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// EncodeRecordsTo encodes the given records into the given writer.
func EncodeRecordsTo(w io.Writer, records []tlv.Record) error {
tlvStream, err := tlv.NewStream(records...)
if err != nil {
return err
}
return tlvStream.Encode(w)
}
// DecodeRecords decodes the given byte slice into the given records and returns
// the rest as a TLV type map.
func DecodeRecords(r io.Reader,
records ...tlv.Record) (tlv.TypeMap, error) {
tlvStream, err := tlv.NewStream(records...)
if err != nil {
return nil, err
}
return tlvStream.DecodeWithParsedTypes(r)
}
// DecodeRecordsP2P decodes the given byte slice into the given records and
// returns the rest as a TLV type map. This function is identical to
// DecodeRecords except that the record size is capped at 65535.
func DecodeRecordsP2P(r *bytes.Reader,
records ...tlv.Record) (tlv.TypeMap, error) {
tlvStream, err := tlv.NewStream(records...)
if err != nil {
return nil, err
}
return tlvStream.DecodeWithParsedTypesP2P(r)
}
// AssertUniqueTypes asserts that the given records have unique types.
func AssertUniqueTypes(r []tlv.Record) error {
seen := make(fn.Set[tlv.Type], len(r))
for _, record := range r {
t := record.Type()
if seen.Contains(t) {
return fmt.Errorf("duplicate record type: %d", t)
}
seen.Add(t)
}
return nil
}
+143
View File
@@ -0,0 +1,143 @@
package lnwire
import (
"bytes"
"fmt"
"io"
)
var (
// ErrParsingExtraTLVBytes is returned when we attempt to parse
// extra opaque bytes as a TLV stream, but the parsing fails due to
// and invalid TLV stream.
ErrParsingExtraTLVBytes = fmt.Errorf("error parsing extra TLV bytes")
)
// FundingError represents a set of errors that can be encountered and sent
// during the funding workflow.
type FundingError uint8
const (
// ErrMaxPendingChannels is returned by remote peer when the number of
// active pending channels exceeds their maximum policy limit.
ErrMaxPendingChannels FundingError = 1
// ErrChanTooLarge is returned by a remote peer that receives a
// FundingOpen request for a channel that is above their current
// soft-limit.
ErrChanTooLarge FundingError = 2
)
// String returns a human readable version of the target FundingError.
func (e FundingError) String() string {
switch e {
case ErrMaxPendingChannels:
return "Number of pending channels exceed maximum"
case ErrChanTooLarge:
return "channel too large"
default:
return "unknown error"
}
}
// Error returns the human readable version of the target FundingError.
//
// NOTE: Satisfies the Error interface.
func (e FundingError) Error() string {
return e.String()
}
// ErrorData is a set of bytes associated with a particular sent error. A
// receiving node SHOULD only print out data verbatim if the string is composed
// solely of printable ASCII characters. For reference, the printable character
// set includes byte values 32 through 127 inclusive.
type ErrorData []byte
// Error represents a generic error bound to an exact channel. The message
// format is purposefully general in order to allow expression of a wide array
// of possible errors. Each Error message is directed at a particular open
// channel referenced by ChannelPoint.
type Error struct {
// ChanID references the active channel in which the error occurred
// within. If the ChanID is all zeros, then this error applies to the
// entire established connection.
ChanID ChannelID
// Data is the attached error data that describes the exact failure
// which caused the error message to be sent.
Data ErrorData
}
// NewError creates a new Error message.
func NewError() *Error {
return &Error{}
}
// A compile time check to ensure Error implements the lnwire.Message
// interface.
var _ Message = (*Error)(nil)
// A compile time check to ensure Error implements the lnwire.SizeableMessage
// interface.
var _ SizeableMessage = (*Error)(nil)
// Error returns the string representation to Error.
//
// NOTE: Satisfies the error interface.
func (c *Error) Error() string {
errMsg := "non-ascii data"
if isASCII(c.Data) {
errMsg = string(c.Data)
}
return fmt.Sprintf("chan_id=%v, err=%v", c.ChanID, errMsg)
}
// Decode deserializes a serialized Error message stored in the passed
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (c *Error) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
&c.ChanID,
&c.Data,
)
}
// Encode serializes the target Error into the passed io.Writer observing the
// protocol version specified.
//
// This is part of the lnwire.Message interface.
func (c *Error) Encode(w *bytes.Buffer, pver uint32) error {
if err := WriteBytes(w, c.ChanID[:]); err != nil {
return err
}
return WriteErrorData(w, c.Data)
}
// MsgType returns the integer uniquely identifying an Error message on the
// wire.
//
// This is part of the lnwire.Message interface.
func (c *Error) MsgType() MessageType {
return MsgError
}
// SerializedSize returns the serialized size of the message in bytes.
//
// This is part of the lnwire.SizeableMessage interface.
func (c *Error) SerializedSize() (uint32, error) {
return MessageSerializedSize(c)
}
// isASCII is a helper method that checks whether all bytes in `data` would be
// printable ASCII characters if interpreted as a string.
func isASCII(data []byte) bool {
for _, c := range data {
if c < 32 || c > 126 {
return false
}
}
return true
}
@@ -0,0 +1,309 @@
package lnwire
import (
"bytes"
"fmt"
"io"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/tlv"
)
// ExtraOpaqueData is the set of data that was appended to this message, some
// of which we may not actually know how to iterate or parse. By holding onto
// this data, we ensure that we're able to properly validate the set of
// signatures that cover these new fields, and ensure we're able to make
// upgrades to the network in a forwards compatible manner.
type ExtraOpaqueData []byte
// NewExtraOpaqueData creates a new ExtraOpaqueData instance from a tlv.TypeMap.
func NewExtraOpaqueData(tlvMap tlv.TypeMap) (ExtraOpaqueData, error) {
// If the tlv map is empty, we'll want to mirror the behavior of
// decoding an empty extra opaque data field (see Decode method).
if len(tlvMap) == 0 {
return make([]byte, 0), nil
}
// Convert the TLV map into a slice of records.
records := TlvMapToRecords(tlvMap)
// Encode the records into the extra data byte slice.
return EncodeRecords(records)
}
// Encode attempts to encode the raw extra bytes into the passed io.Writer.
func (e *ExtraOpaqueData) Encode(w *bytes.Buffer) error {
eBytes := []byte((*e)[:])
if err := WriteBytes(w, eBytes); err != nil {
return err
}
return nil
}
// Decode attempts to unpack the raw bytes encoded in the passed-in io.Reader as
// a set of extra opaque data.
func (e *ExtraOpaqueData) Decode(r io.Reader) error {
// First, we'll attempt to read a set of bytes contained within the
// passed io.Reader (if any exist).
rawBytes, err := io.ReadAll(r)
if err != nil {
return err
}
// If we _do_ have some bytes, then we'll swap out our backing pointer.
// This ensures that any struct that embeds this type will properly
// store the bytes once this method exits.
if len(rawBytes) > 0 {
*e = rawBytes
} else {
*e = make([]byte, 0)
}
return nil
}
// ValidateTLV checks that the raw bytes that make up the ExtraOpaqueData
// instance are a valid TLV stream.
func (e *ExtraOpaqueData) ValidateTLV() error {
// There is nothing to validate if the ExtraOpaqueData is nil or empty.
if e == nil || len(*e) == 0 {
return nil
}
tlvStream, err := tlv.NewStream()
if err != nil {
return err
}
// Ensure that the TLV stream is valid by attempting to decode it.
_, err = tlvStream.DecodeWithParsedTypesP2P(bytes.NewReader(*e))
if err != nil {
return fmt.Errorf("invalid TLV stream: %w: %v", err, *e)
}
return nil
}
// PackRecords attempts to encode the set of tlv records into the target
// ExtraOpaqueData instance. The records will be encoded as a raw TLV stream
// and stored within the backing slice pointer.
func (e *ExtraOpaqueData) PackRecords(
recordProducers ...tlv.RecordProducer) error {
// Assemble all the records passed in series, then encode them.
records := ProduceRecordsSorted(recordProducers...)
encoded, err := EncodeRecords(records)
if err != nil {
return err
}
*e = encoded
return nil
}
// ExtractRecords attempts to decode any types in the internal raw bytes as if
// it were a tlv stream. The set of raw parsed types is returned, and any
// passed records (if found in the stream) will be parsed into the proper
// tlv.Record.
func (e *ExtraOpaqueData) ExtractRecords(
recordProducers ...tlv.RecordProducer) (tlv.TypeMap, error) {
// First, assemble all the records passed in series.
records := ProduceRecordsSorted(recordProducers...)
extraBytesReader := bytes.NewReader(*e)
// Since ExtraOpaqueData is provided by a potentially malicious peer,
// pass it into the P2P decoding variant.
return DecodeRecordsP2P(extraBytesReader, records...)
}
// RecordProducers parses ExtraOpaqueData into a slice of TLV record producers
// by interpreting it as a TLV map.
func (e *ExtraOpaqueData) RecordProducers() ([]tlv.RecordProducer, error) {
var recordProducers []tlv.RecordProducer
// If the instance is nil or empty, return an empty slice.
if e == nil || len(*e) == 0 {
return recordProducers, nil
}
// Parse the extra opaque data as a TLV map.
tlvMap, err := e.ExtractRecords()
if err != nil {
return nil, err
}
// Convert the TLV map into a slice of record producers.
records := TlvMapToRecords(tlvMap)
return RecordsAsProducers(records), nil
}
// EncodeMessageExtraData encodes the given recordProducers into the given
// extraData.
func EncodeMessageExtraData(extraData *ExtraOpaqueData,
recordProducers ...tlv.RecordProducer) error {
// Treat extraData as a mutable reference.
if extraData == nil {
return fmt.Errorf("extra data cannot be nil")
}
// Pack in the series of TLV records into this message. The order we
// pass them in doesn't matter, as the method will ensure that things
// are all properly sorted.
return extraData.PackRecords(recordProducers...)
}
// ParseAndExtractCustomRecords parses the given extra data into the passed-in
// records, then returns any remaining records split into custom records and
// extra data.
func ParseAndExtractCustomRecords(allExtraData ExtraOpaqueData,
knownRecords ...tlv.RecordProducer) (CustomRecords,
fn.Set[tlv.Type], ExtraOpaqueData, error) {
extraDataTlvMap, err := allExtraData.ExtractRecords(knownRecords...)
if err != nil {
return nil, nil, nil, err
}
// Remove the known and now extracted records from the leftover extra
// data map.
parsedKnownRecords := make(fn.Set[tlv.Type], len(knownRecords))
for _, producer := range knownRecords {
r := producer.Record()
// Only remove the records if it was parsed (remainder is nil).
// We'll just store the type so we can tell the caller which
// records were actually parsed fully.
val, ok := extraDataTlvMap[r.Type()]
if ok && val == nil {
parsedKnownRecords.Add(r.Type())
delete(extraDataTlvMap, r.Type())
}
}
// Any records from the extra data TLV map which are in the custom
// records TLV type range will be included in the custom records field
// and removed from the extra data field.
customRecordsTlvMap := make(tlv.TypeMap, len(extraDataTlvMap))
for k, v := range extraDataTlvMap {
// Skip records that are not in the custom records TLV type
// range.
if k < MinCustomRecordsTlvType {
continue
}
// Include the record in the custom records map.
customRecordsTlvMap[k] = v
// Now that the record is included in the custom records map,
// we can remove it from the extra data TLV map.
delete(extraDataTlvMap, k)
}
// Set the custom records field to the custom records specific TLV
// record map.
customRecords, err := NewCustomRecords(customRecordsTlvMap)
if err != nil {
return nil, nil, nil, err
}
// Encode the remaining records back into the extra data field. These
// records are not in the custom records TLV type range and do not
// have associated fields in the struct that produced the records.
extraData, err := NewExtraOpaqueData(extraDataTlvMap)
if err != nil {
return nil, nil, nil, err
}
// Help with unit testing where we might have the empty value (nil) for
// the extra data instead of the default that's returned by the
// constructor (empty slice).
if len(extraData) == 0 {
extraData = nil
}
return customRecords, parsedKnownRecords, extraData, nil
}
// MergeAndEncode merges the known records with the extra data and custom
// records, then encodes the merged records into raw bytes.
func MergeAndEncode(knownRecords []tlv.RecordProducer,
extraData ExtraOpaqueData, customRecords CustomRecords) ([]byte,
error) {
// Construct a slice of all the records that we should include in the
// message extra data field. We will start by including any records from
// the extra data field.
mergedRecords, err := extraData.RecordProducers()
if err != nil {
return nil, err
}
// Merge the known and extra data records.
mergedRecords = append(mergedRecords, knownRecords...)
// Include custom records in the extra data wire field if they are
// present. Ensure that the custom records are validated before encoding
// them.
if err := customRecords.Validate(); err != nil {
return nil, fmt.Errorf("custom records validation error: %w",
err)
}
// Extend the message extra data records slice with TLV records from the
// custom records field.
mergedRecords = append(
mergedRecords, customRecords.RecordProducers()...,
)
// Now we can sort the records and make sure there are no records with
// the same type that would collide when encoding.
sortedRecords := ProduceRecordsSorted(mergedRecords...)
if err := AssertUniqueTypes(sortedRecords); err != nil {
return nil, err
}
return EncodeRecords(sortedRecords)
}
// ParseAndExtractExtraData parses the given extra data into the passed-in
// records, then returns any remaining records as extra data.
func ParseAndExtractExtraData(allTlvData ExtraOpaqueData,
knownRecords ...tlv.RecordProducer) (fn.Set[tlv.Type],
ExtraOpaqueData, error) {
extraDataTlvMap, err := allTlvData.ExtractRecords(knownRecords...)
if err != nil {
return nil, nil, err
}
// Remove the known and now extracted records from the leftover extra
// data map.
parsedKnownRecords := make(fn.Set[tlv.Type], len(knownRecords))
for _, producer := range knownRecords {
r := producer.Record()
// Only remove the records if it was parsed (remainder is nil).
// We'll just store the type so we can tell the caller which
// records were actually parsed fully.
val, ok := extraDataTlvMap[r.Type()]
if ok && val == nil {
parsedKnownRecords.Add(r.Type())
delete(extraDataTlvMap, r.Type())
}
}
// Encode the remaining records back into the extra data field. These
// records are not in the custom records TLV type range and do not
// have associated fields in the struct that produced the records.
extraData, err := NewExtraOpaqueData(extraDataTlvMap)
if err != nil {
return nil, nil, err
}
return parsedKnownRecords, extraData, nil
}
+898
View File
@@ -0,0 +1,898 @@
package lnwire
import (
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/lightningnetwork/lnd/tlv"
)
var (
// ErrFeaturePairExists signals an error in feature vector construction
// where the opposing bit in a feature pair has already been set.
ErrFeaturePairExists = errors.New("feature pair exists")
// ErrFeatureStandard is returned when attempts to modify LND's known
// set of features are made.
ErrFeatureStandard = errors.New("feature is used in standard " +
"protocol set")
// ErrFeatureBitMaximum is returned when a feature bit exceeds the
// maximum allowable value.
ErrFeatureBitMaximum = errors.New("feature bit exceeds allowed maximum")
)
// FeatureBit represents a feature that can be enabled in either a local or
// global feature vector at a specific bit position. Feature bits follow the
// "it's OK to be odd" rule, where features at even bit positions must be known
// to a node receiving them from a peer while odd bits do not. In accordance,
// feature bits are usually assigned in pairs, first being assigned an odd bit
// position which may later be changed to the preceding even position once
// knowledge of the feature becomes required on the network.
type FeatureBit uint16
const (
// DataLossProtectRequired is a feature bit that indicates that a peer
// *requires* the other party know about the data-loss-protect optional
// feature. If the remote peer does not know of such a feature, then
// the sending peer SHOULD disconnect them. The data-loss-protect
// feature allows a peer that's lost partial data to recover their
// settled funds of the latest commitment state.
DataLossProtectRequired FeatureBit = 0
// DataLossProtectOptional is an optional feature bit that indicates
// that the sending peer knows of this new feature and can activate it
// it. The data-loss-protect feature allows a peer that's lost partial
// data to recover their settled funds of the latest commitment state.
DataLossProtectOptional FeatureBit = 1
// InitialRoutingSync is a local feature bit meaning that the receiving
// node should send a complete dump of routing information when a new
// connection is established.
InitialRoutingSync FeatureBit = 3
// UpfrontShutdownScriptRequired is a feature bit which indicates that a
// peer *requires* that the remote peer accept an upfront shutdown script to
// which payout is enforced on cooperative closes.
UpfrontShutdownScriptRequired FeatureBit = 4
// UpfrontShutdownScriptOptional is an optional feature bit which indicates
// that the peer will accept an upfront shutdown script to which payout is
// enforced on cooperative closes.
UpfrontShutdownScriptOptional FeatureBit = 5
// GossipQueriesRequired is a feature bit that indicates that the
// receiving peer MUST know of the set of features that allows nodes to
// more efficiently query the network view of peers on the network for
// reconciliation purposes.
GossipQueriesRequired FeatureBit = 6
// GossipQueriesOptional is an optional feature bit that signals that
// the setting peer knows of the set of features that allows more
// efficient network view reconciliation.
GossipQueriesOptional FeatureBit = 7
// TLVOnionPayloadRequired is a feature bit that indicates a node is
// able to decode the new TLV information included in the onion packet.
TLVOnionPayloadRequired FeatureBit = 8
// TLVOnionPayloadOptional is an optional feature bit that indicates a
// node is able to decode the new TLV information included in the onion
// packet.
TLVOnionPayloadOptional FeatureBit = 9
// StaticRemoteKeyRequired is a required feature bit that signals that
// within one's commitment transaction, the key used for the remote
// party's non-delay output should not be tweaked.
StaticRemoteKeyRequired FeatureBit = 12
// StaticRemoteKeyOptional is an optional feature bit that signals that
// within one's commitment transaction, the key used for the remote
// party's non-delay output should not be tweaked.
StaticRemoteKeyOptional FeatureBit = 13
// PaymentAddrRequired is a required feature bit that signals that a
// node requires payment addresses, which are used to mitigate probing
// attacks on the receiver of a payment.
PaymentAddrRequired FeatureBit = 14
// PaymentAddrOptional is an optional feature bit that signals that a
// node supports payment addresses, which are used to mitigate probing
// attacks on the receiver of a payment.
PaymentAddrOptional FeatureBit = 15
// MPPRequired is a required feature bit that signals that the receiver
// of a payment requires settlement of an invoice with more than one
// HTLC.
MPPRequired FeatureBit = 16
// MPPOptional is an optional feature bit that signals that the receiver
// of a payment supports settlement of an invoice with more than one
// HTLC.
MPPOptional FeatureBit = 17
// WumboChannelsRequired is a required feature bit that signals that a
// node is willing to accept channels larger than 2^24 satoshis.
WumboChannelsRequired FeatureBit = 18
// WumboChannelsOptional is an optional feature bit that signals that a
// node is willing to accept channels larger than 2^24 satoshis.
WumboChannelsOptional FeatureBit = 19
// AnchorsRequired is a required feature bit that signals that the node
// requires channels to be made using commitments having anchor
// outputs.
AnchorsRequired FeatureBit = 20
// AnchorsOptional is an optional feature bit that signals that the
// node supports channels to be made using commitments having anchor
// outputs.
AnchorsOptional FeatureBit = 21
// AnchorsZeroFeeHtlcTxRequired is a required feature bit that signals
// that the node requires channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments.
AnchorsZeroFeeHtlcTxRequired FeatureBit = 22
// AnchorsZeroFeeHtlcTxOptional is an optional feature bit that signals
// that the node supports channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments.
AnchorsZeroFeeHtlcTxOptional FeatureBit = 23
// RouteBlindingRequired is a required feature bit that signals that
// the node supports blinded payments.
RouteBlindingRequired FeatureBit = 24
// RouteBlindingOptional is an optional feature bit that signals that
// the node supports blinded payments.
RouteBlindingOptional FeatureBit = 25
// ShutdownAnySegwitRequired is an required feature bit that signals
// that the sender is able to properly handle/parse segwit witness
// programs up to version 16. This enables utilization of Taproot
// addresses for cooperative closure addresses.
ShutdownAnySegwitRequired FeatureBit = 26
// ShutdownAnySegwitOptional is an optional feature bit that signals
// that the sender is able to properly handle/parse segwit witness
// programs up to version 16. This enables utilization of Taproot
// addresses for cooperative closure addresses.
ShutdownAnySegwitOptional FeatureBit = 27
// AMPRequired is a required feature bit that signals that the receiver
// of a payment supports accepts spontaneous payments, i.e.
// sender-generated preimages according to BOLT XX.
AMPRequired FeatureBit = 30
// AMPOptional is an optional feature bit that signals that the receiver
// of a payment supports accepts spontaneous payments, i.e.
// sender-generated preimages according to BOLT XX.
AMPOptional FeatureBit = 31
// QuiescenceRequired is a required feature bit that denotes that a
// connection established with this node must support the quiescence
// protocol if it wants to have a channel relationship.
QuiescenceRequired FeatureBit = 34
// QuiescenceOptional is an optional feature bit that denotes that a
// connection established with this node is permitted to use the
// quiescence protocol.
QuiescenceOptional FeatureBit = 35
// ExplicitChannelTypeRequired is a required bit that denotes that a
// connection established with this node is to use explicit channel
// commitment types for negotiation instead of the existing implicit
// negotiation methods. With this bit, there is no longer a "default"
// implicit channel commitment type, allowing a connection to
// open/maintain types of several channels over its lifetime.
ExplicitChannelTypeRequired = 44
// ExplicitChannelTypeOptional is an optional bit that denotes that a
// connection established with this node is to use explicit channel
// commitment types for negotiation instead of the existing implicit
// negotiation methods. With this bit, there is no longer a "default"
// implicit channel commitment type, allowing a connection to
// TODO: Decide on actual feature bit value.
ExplicitChannelTypeOptional = 45
// ScidAliasRequired is a required feature bit that signals that the
// node requires understanding of ShortChannelID aliases in the TLV
// segment of the channel_ready message.
ScidAliasRequired FeatureBit = 46
// ScidAliasOptional is an optional feature bit that signals that the
// node understands ShortChannelID aliases in the TLV segment of the
// channel_ready message.
ScidAliasOptional FeatureBit = 47
// PaymentMetadataRequired is a required bit that denotes that if an
// invoice contains metadata, it must be passed along with the payment
// htlc(s).
PaymentMetadataRequired = 48
// PaymentMetadataOptional is an optional bit that denotes that if an
// invoice contains metadata, it may be passed along with the payment
// htlc(s).
PaymentMetadataOptional = 49
// ZeroConfRequired is a required feature bit that signals that the
// node requires understanding of the zero-conf channel_type.
ZeroConfRequired FeatureBit = 50
// ZeroConfOptional is an optional feature bit that signals that the
// node understands the zero-conf channel type.
ZeroConfOptional FeatureBit = 51
// KeysendRequired is a required bit that indicates that the node is
// able and willing to accept keysend payments.
KeysendRequired = 54
// KeysendOptional is an optional bit that indicates that the node is
// able and willing to accept keysend payments.
KeysendOptional = 55
// RbfCoopCloseRequired is a required feature bit that signals that
// the new RBF-based co-op close protocol is supported.
RbfCoopCloseRequired = 60
// RbfCoopCloseOptional is an optional feature bit that signals that the
// new RBF-based co-op close protocol is supported.
RbfCoopCloseOptional = 61
// RbfCoopCloseRequiredStaging is a required feature bit that signals
// that the new RBF-based co-op close protocol is supported.
RbfCoopCloseRequiredStaging = 160
// RbfCoopCloseOptionalStaging is an optional feature bit that signals
// that the new RBF-based co-op close protocol is supported.
RbfCoopCloseOptionalStaging = 161
// ScriptEnforcedLeaseRequired is a required feature bit that signals
// that the node requires channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments, along with an
// additional CLTV constraint of a channel lease's expiration height
// applied to all outputs that pay directly to the channel initiator.
//
// TODO: Decide on actual feature bit value.
ScriptEnforcedLeaseRequired FeatureBit = 2022
// ScriptEnforcedLeaseOptional is an optional feature bit that signals
// that the node requires channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments, along with an
// additional CLTV constraint of a channel lease's expiration height
// applied to all outputs that pay directly to the channel initiator.
//
// TODO: Decide on actual feature bit value.
ScriptEnforcedLeaseOptional FeatureBit = 2023
// SimpleTaprootChannelsRequiredFinal is a required bit that indicates
// the node is able to create taproot-native channels. This is the
// final feature bit to be used once the channel type is finalized.
SimpleTaprootChannelsRequiredFinal = 80
// SimpleTaprootChannelsOptionalFinal is an optional bit that indicates
// the node is able to create taproot-native channels. This is the
// final feature bit to be used once the channel type is finalized.
SimpleTaprootChannelsOptionalFinal = 81
// SimpleTaprootChannelsRequiredStaging is a required bit that indicates
// the node is able to create taproot-native channels. This is a
// feature bit used in the wild while the channel type is still being
// finalized.
SimpleTaprootChannelsRequiredStaging = 180
// SimpleTaprootChannelsOptionalStaging is an optional bit that
// indicates the node is able to create taproot-native channels. This
// is a feature bit used in the wild while the channel type is still
// being finalized.
SimpleTaprootChannelsOptionalStaging = 181
// ExperimentalAccountabilityRequired is a required feature bit that
// indicates that the node will relay experimental accountability
// signals.
ExperimentalAccountabilityRequired FeatureBit = 260
// ExperimentalAccountabilityOptional is an optional feature bit that
// indicates that the node will relay experimental accountability
// signals.
ExperimentalAccountabilityOptional FeatureBit = 261
// Bolt11BlindedPathsRequired is a required feature bit that indicates
// that the node is able to understand the blinded path tagged field in
// a BOLT 11 invoice.
Bolt11BlindedPathsRequired = 262
// Bolt11BlindedPathsOptional is an optional feature bit that indicates
// that the node is able to understand the blinded path tagged field in
// a BOLT 11 invoice.
Bolt11BlindedPathsOptional = 263
// SimpleTaprootOverlayChansRequired is a required bit that indicates
// support for the special custom taproot overlay channel.
SimpleTaprootOverlayChansOptional = 2025
// SimpleTaprootOverlayChansRequired is a required bit that indicates
// support for the special custom taproot overlay channel.
SimpleTaprootOverlayChansRequired = 2026
// MaxBolt11Feature is the maximum feature bit value allowed in bolt 11
// invoices.
//
// The base 32 encoded tagged fields in invoices are limited to 10 bits
// to express the length of the field's data.
//nolint:ll
// See: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md#tagged-fields
//
// With a maximum length field of 1023 (2^10 -1) and 5 bit encoding,
// the highest feature bit that can be expressed is:
// 1023 * 5 - 1 = 5114.
MaxBolt11Feature = 5114
)
// IsRequired returns true if the feature bit is even, and false otherwise.
func (b FeatureBit) IsRequired() bool {
return b&0x01 == 0x00
}
// Features is a mapping of known feature bits to a descriptive name. All known
// feature bits must be assigned a name in this mapping, and feature bit pairs
// must be assigned together for correct behavior.
var Features = map[FeatureBit]string{
DataLossProtectRequired: "data-loss-protect",
DataLossProtectOptional: "data-loss-protect",
InitialRoutingSync: "initial-routing-sync",
UpfrontShutdownScriptRequired: "upfront-shutdown-script",
UpfrontShutdownScriptOptional: "upfront-shutdown-script",
GossipQueriesRequired: "gossip-queries",
GossipQueriesOptional: "gossip-queries",
TLVOnionPayloadRequired: "tlv-onion",
TLVOnionPayloadOptional: "tlv-onion",
StaticRemoteKeyOptional: "static-remote-key",
StaticRemoteKeyRequired: "static-remote-key",
PaymentAddrOptional: "payment-addr",
PaymentAddrRequired: "payment-addr",
MPPOptional: "multi-path-payments",
MPPRequired: "multi-path-payments",
AnchorsRequired: "anchor-commitments",
AnchorsOptional: "anchor-commitments",
AnchorsZeroFeeHtlcTxRequired: "anchors-zero-fee-htlc-tx",
AnchorsZeroFeeHtlcTxOptional: "anchors-zero-fee-htlc-tx",
WumboChannelsRequired: "wumbo-channels",
WumboChannelsOptional: "wumbo-channels",
AMPRequired: "amp",
AMPOptional: "amp",
QuiescenceRequired: "quiescence",
QuiescenceOptional: "quiescence",
PaymentMetadataOptional: "payment-metadata",
PaymentMetadataRequired: "payment-metadata",
ExplicitChannelTypeOptional: "explicit-commitment-type",
ExplicitChannelTypeRequired: "explicit-commitment-type",
KeysendOptional: "keysend",
KeysendRequired: "keysend",
ScriptEnforcedLeaseRequired: "script-enforced-lease",
ScriptEnforcedLeaseOptional: "script-enforced-lease",
ScidAliasRequired: "scid-alias",
ScidAliasOptional: "scid-alias",
ZeroConfRequired: "zero-conf",
ZeroConfOptional: "zero-conf",
RouteBlindingRequired: "route-blinding",
RouteBlindingOptional: "route-blinding",
ShutdownAnySegwitRequired: "shutdown-any-segwit",
ShutdownAnySegwitOptional: "shutdown-any-segwit",
SimpleTaprootChannelsRequiredFinal: "simple-taproot-chans",
SimpleTaprootChannelsOptionalFinal: "simple-taproot-chans",
SimpleTaprootChannelsRequiredStaging: "simple-taproot-chans-x",
SimpleTaprootChannelsOptionalStaging: "simple-taproot-chans-x",
SimpleTaprootOverlayChansOptional: "taproot-overlay-chans",
SimpleTaprootOverlayChansRequired: "taproot-overlay-chans",
ExperimentalAccountabilityRequired: "accountable-x",
ExperimentalAccountabilityOptional: "accountable-x",
Bolt11BlindedPathsOptional: "bolt-11-blinded-paths",
Bolt11BlindedPathsRequired: "bolt-11-blinded-paths",
RbfCoopCloseOptional: "rbf-coop-close",
RbfCoopCloseRequired: "rbf-coop-close",
RbfCoopCloseOptionalStaging: "rbf-coop-close-x",
RbfCoopCloseRequiredStaging: "rbf-coop-close-x",
}
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A
// RawFeatureVector itself just stores a set of bit flags but can be used to
// construct a FeatureVector which binds meaning to each bit. Feature vectors
// can be serialized and deserialized to/from a byte representation that is
// transmitted in Lightning network messages.
type RawFeatureVector struct {
features map[FeatureBit]struct{}
}
// NewRawFeatureVector creates a feature vector with all of the feature bits
// given as arguments enabled.
func NewRawFeatureVector(bits ...FeatureBit) *RawFeatureVector {
fv := &RawFeatureVector{features: make(map[FeatureBit]struct{})}
for _, bit := range bits {
fv.Set(bit)
}
return fv
}
// IsEmpty returns whether the feature vector contains any feature bits.
func (fv RawFeatureVector) IsEmpty() bool {
return len(fv.features) == 0
}
// OnlyContains determines whether only the specified feature bits are found.
func (fv RawFeatureVector) OnlyContains(bits ...FeatureBit) bool {
if len(bits) != len(fv.features) {
return false
}
for _, bit := range bits {
if !fv.IsSet(bit) {
return false
}
}
return true
}
// Equals determines whether two features vectors contain exactly the same
// features.
func (fv RawFeatureVector) Equals(other *RawFeatureVector) bool {
if len(fv.features) != len(other.features) {
return false
}
for bit := range fv.features {
if _, ok := other.features[bit]; !ok {
return false
}
}
return true
}
// Merge sets all feature bits in other on the receiver's feature vector.
func (fv *RawFeatureVector) Merge(other *RawFeatureVector) error {
for bit := range other.features {
err := fv.SafeSet(bit)
if err != nil {
return err
}
}
return nil
}
// ValidateUpdate checks whether a feature vector can safely be updated to the
// new feature vector provided, checking that it does not alter any of the
// "standard" features that are defined by LND. The new feature vector should
// be inclusive of all features in the original vector that it still wants to
// advertise, setting and unsetting updates as desired. Features in the vector
// are also checked against a maximum inclusive value, as feature vectors in
// different contexts have different maximum values.
func (fv *RawFeatureVector) ValidateUpdate(other *RawFeatureVector,
maximumValue FeatureBit) error {
// Run through the new set of features and check that we're not adding
// any feature bits that are defined but not set in LND.
for feature := range other.features {
if fv.IsSet(feature) {
continue
}
if feature > maximumValue {
return fmt.Errorf("can't set feature bit %d: %w %v",
feature, ErrFeatureBitMaximum,
maximumValue)
}
if name, known := Features[feature]; known {
return fmt.Errorf("can't set feature "+
"bit %d (%v): %w", feature, name,
ErrFeatureStandard)
}
}
// Check that the new feature vector for this set does not unset any
// features that are standard in LND by comparing the features in our
// current set to the omitted values in the new set.
for feature := range fv.features {
if other.IsSet(feature) {
continue
}
if name, known := Features[feature]; known {
return fmt.Errorf("can't unset feature "+
"bit %d (%v): %w", feature, name,
ErrFeatureStandard)
}
}
return nil
}
// ValidatePairs checks each feature bit in a raw vector to ensure that the
// opposing bit is not set, validating that the vector has either the optional
// or required bit set, not both.
func (fv *RawFeatureVector) ValidatePairs() error {
for feature := range fv.features {
if _, ok := fv.features[feature^1]; ok {
return ErrFeaturePairExists
}
}
return nil
}
// Clone makes a copy of a feature vector.
func (fv *RawFeatureVector) Clone() *RawFeatureVector {
newFeatures := NewRawFeatureVector()
for bit := range fv.features {
newFeatures.Set(bit)
}
return newFeatures
}
// IsSet returns whether a particular feature bit is enabled in the vector.
func (fv *RawFeatureVector) IsSet(feature FeatureBit) bool {
_, ok := fv.features[feature]
return ok
}
// Set marks a feature as enabled in the vector.
func (fv *RawFeatureVector) Set(feature FeatureBit) {
fv.features[feature] = struct{}{}
}
// SafeSet sets the chosen feature bit in the feature vector, but returns an
// error if the opposing feature bit is already set. This ensures both that we
// are creating properly structured feature vectors, and in some cases, that
// peers are sending properly encoded ones, i.e. it can't be both optional and
// required.
func (fv *RawFeatureVector) SafeSet(feature FeatureBit) error {
if _, ok := fv.features[feature^1]; ok {
return ErrFeaturePairExists
}
fv.Set(feature)
return nil
}
// Unset marks a feature as disabled in the vector.
func (fv *RawFeatureVector) Unset(feature FeatureBit) {
delete(fv.features, feature)
}
// SerializeSize returns the number of bytes needed to represent feature vector
// in byte format.
func (fv *RawFeatureVector) SerializeSize() int {
// We calculate byte-length via the largest bit index.
return fv.serializeSize(8)
}
// SerializeSize32 returns the number of bytes needed to represent feature
// vector in base32 format.
func (fv *RawFeatureVector) SerializeSize32() int {
// We calculate base32-length via the largest bit index.
return fv.serializeSize(5)
}
// serializeSize returns the number of bytes required to encode the feature
// vector using at most width bits per encoded byte.
func (fv *RawFeatureVector) serializeSize(width int) int {
// Find the largest feature bit index
max := -1
for feature := range fv.features {
index := int(feature)
if index > max {
max = index
}
}
if max == -1 {
return 0
}
return max/width + 1
}
// Encode writes the feature vector in byte representation. Every feature
// encoded as a bit, and the bit vector is serialized using the least number of
// bytes. Since the bit vector length is variable, the first two bytes of the
// serialization represent the length.
func (fv *RawFeatureVector) Encode(w io.Writer) error {
// Write length of feature vector.
var l [2]byte
length := fv.SerializeSize()
binary.BigEndian.PutUint16(l[:], uint16(length))
if _, err := w.Write(l[:]); err != nil {
return err
}
return fv.encode(w, length, 8)
}
// EncodeBase256 writes the feature vector in base256 representation. Every
// feature is encoded as a bit, and the bit vector is serialized using the least
// number of bytes.
func (fv *RawFeatureVector) EncodeBase256(w io.Writer) error {
length := fv.SerializeSize()
return fv.encode(w, length, 8)
}
// EncodeBase32 writes the feature vector in base32 representation. Every feature
// is encoded as a bit, and the bit vector is serialized using the least number of
// bytes.
func (fv *RawFeatureVector) EncodeBase32(w io.Writer) error {
length := fv.SerializeSize32()
return fv.encode(w, length, 5)
}
// encode writes the feature vector
func (fv *RawFeatureVector) encode(w io.Writer, length, width int) error {
// Generate the data and write it.
data := make([]byte, length)
for feature := range fv.features {
byteIndex := int(feature) / width
bitIndex := int(feature) % width
data[length-byteIndex-1] |= 1 << uint(bitIndex)
}
_, err := w.Write(data)
return err
}
// Decode reads the feature vector from its byte representation. Every feature
// is encoded as a bit, and the bit vector is serialized using the least number
// of bytes. Since the bit vector length is variable, the first two bytes of the
// serialization represent the length.
func (fv *RawFeatureVector) Decode(r io.Reader) error {
// Read the length of the feature vector.
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return err
}
length := binary.BigEndian.Uint16(l[:])
return fv.decode(r, int(length), 8)
}
// DecodeBase256 reads the feature vector from its base256 representation. Every
// feature encoded as a bit, and the bit vector is serialized using the least
// number of bytes.
func (fv *RawFeatureVector) DecodeBase256(r io.Reader, length int) error {
return fv.decode(r, length, 8)
}
// DecodeBase32 reads the feature vector from its base32 representation. Every
// feature encoded as a bit, and the bit vector is serialized using the least
// number of bytes.
func (fv *RawFeatureVector) DecodeBase32(r io.Reader, length int) error {
return fv.decode(r, length, 5)
}
// decode reads a feature vector from the next length bytes of the io.Reader,
// assuming each byte has width feature bits encoded per byte.
func (fv *RawFeatureVector) decode(r io.Reader, length, width int) error {
// Read the feature vector data.
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return err
}
// Set feature bits from parsed data.
bitsNumber := len(data) * width
for i := 0; i < bitsNumber; i++ {
byteIndex := int(i / width)
bitIndex := uint(i % width)
if (data[length-byteIndex-1]>>bitIndex)&1 == 1 {
fv.Set(FeatureBit(i))
}
}
return nil
}
// sizeFunc returns the length required to encode the feature vector.
func (fv *RawFeatureVector) sizeFunc() uint64 {
return uint64(fv.SerializeSize())
}
// Record returns a TLV record that can be used to encode/decode raw feature
// vectors. Note that the length of the feature vector is not included, because
// it is covered by the TLV record's length field.
func (fv *RawFeatureVector) Record() tlv.Record {
return tlv.MakeDynamicRecord(
0, fv, fv.sizeFunc, rawFeatureEncoder, rawFeatureDecoder,
)
}
// rawFeatureEncoder is a custom TLV encoder for raw feature vectors.
func rawFeatureEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
if v, ok := val.(*RawFeatureVector); ok {
// Encode the feature bits as a byte slice without its length
// prepended, as that's already taken care of by the TLV record.
fv := *v
return fv.encode(w, fv.SerializeSize(), 8)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.RawFeatureVector")
}
// rawFeatureDecoder is a custom TLV decoder for raw feature vectors.
func rawFeatureDecoder(r io.Reader, val interface{}, _ *[8]byte,
l uint64) error {
if v, ok := val.(*RawFeatureVector); ok {
fv := NewRawFeatureVector()
if err := fv.decode(r, int(l), 8); err != nil {
return err
}
*v = *fv
return nil
}
return tlv.NewTypeForEncodingErr(val, "lnwire.RawFeatureVector")
}
// FeatureVector represents a set of enabled features. The set stores
// information on enabled flags and metadata about the feature names. A feature
// vector is serializable to a compact byte representation that is included in
// Lightning network messages.
type FeatureVector struct {
*RawFeatureVector
featureNames map[FeatureBit]string
}
// NewFeatureVector constructs a new FeatureVector from a raw feature vector
// and mapping of feature definitions. If the feature vector argument is nil, a
// new one will be constructed with no enabled features.
func NewFeatureVector(featureVector *RawFeatureVector,
featureNames map[FeatureBit]string) *FeatureVector {
if featureVector == nil {
featureVector = NewRawFeatureVector()
}
return &FeatureVector{
RawFeatureVector: featureVector,
featureNames: featureNames,
}
}
// EmptyFeatureVector returns a feature vector with no bits set.
func EmptyFeatureVector() *FeatureVector {
return NewFeatureVector(nil, Features)
}
// Record implements the RecordProducer interface for FeatureVector. Note that
// it uses a zero-value type is used to produce the record, as we expect this
// type value to be overwritten when used in generic TLV record production.
// This allows a single Record function to serve in the many different contexts
// in which feature vectors are encoded. This record wraps the encoding/
// decoding for our raw feature vectors so that we can directly parse fully
// formed feature vector types.
func (fv *FeatureVector) Record() tlv.Record {
return tlv.MakeDynamicRecord(0, fv, fv.sizeFunc,
func(w io.Writer, val interface{}, buf *[8]byte) error {
if f, ok := val.(*FeatureVector); ok {
return rawFeatureEncoder(
w, f.RawFeatureVector, buf,
)
}
return tlv.NewTypeForEncodingErr(
val, "*lnwire.FeatureVector",
)
},
func(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if f, ok := val.(*FeatureVector); ok {
features := NewFeatureVector(nil, Features)
err := rawFeatureDecoder(
r, features.RawFeatureVector, buf, l,
)
if err != nil {
return err
}
*f = *features
return nil
}
return tlv.NewTypeForDecodingErr(
val, "*lnwire.FeatureVector", l, l,
)
},
)
}
// HasFeature returns whether a particular feature is included in the set. The
// feature can be seen as set either if the bit is set directly OR the queried
// bit has the same meaning as its corresponding even/odd bit, which is set
// instead. The second case is because feature bits are generally assigned in
// pairs where both the even and odd position represent the same feature.
func (fv *FeatureVector) HasFeature(feature FeatureBit) bool {
return fv.IsSet(feature) ||
(fv.isFeatureBitPair(feature) && fv.IsSet(feature^1))
}
// RequiresFeature returns true if the referenced feature vector *requires*
// that the given required bit be set. This method can be used with both
// optional and required feature bits as a parameter.
func (fv *FeatureVector) RequiresFeature(feature FeatureBit) bool {
// If we weren't passed a required feature bit, then we'll flip the
// lowest bit to query for the required version of the feature. This
// lets callers pass in both the optional and required bits.
if !feature.IsRequired() {
feature ^= 1
}
return fv.IsSet(feature)
}
// UnknownRequiredFeatures returns a list of feature bits set in the vector
// that are unknown and in an even bit position. Feature bits with an even
// index must be known to a node receiving the feature vector in a message.
func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
var unknown []FeatureBit
for feature := range fv.features {
if feature%2 == 0 && !fv.IsKnown(feature) {
unknown = append(unknown, feature)
}
}
return unknown
}
// UnknownFeatures returns a boolean if a feature vector contains *any*
// unknown features (even if they are odd).
func (fv *FeatureVector) UnknownFeatures() bool {
for feature := range fv.features {
if !fv.IsKnown(feature) {
return true
}
}
return false
}
// Name returns a string identifier for the feature represented by this bit. If
// the bit does not represent a known feature, this returns a string indicating
// as such.
func (fv *FeatureVector) Name(bit FeatureBit) string {
name, known := fv.featureNames[bit]
if !known {
return "unknown"
}
return name
}
// IsKnown returns whether this feature bit represents a known feature.
func (fv *FeatureVector) IsKnown(bit FeatureBit) bool {
_, known := fv.featureNames[bit]
return known
}
// isFeatureBitPair returns whether this feature bit and its corresponding
// even/odd bit both represent the same feature. This may often be the case as
// bits are generally assigned in pairs, first being assigned an odd bit
// position then being promoted to an even bit position once the network is
// ready.
func (fv *FeatureVector) isFeatureBitPair(bit FeatureBit) bool {
name1, known1 := fv.featureNames[bit]
name2, known2 := fv.featureNames[bit^1]
return known1 && known2 && name1 == name2
}
// Features returns the set of raw features contained in the feature vector.
func (fv *FeatureVector) Features() map[FeatureBit]struct{} {
fs := make(map[FeatureBit]struct{}, len(fv.RawFeatureVector.features))
for b := range fv.RawFeatureVector.features {
fs[b] = struct{}{}
}
return fs
}
// Clone copies a feature vector, carrying over its feature bits. The feature
// names are not copied.
func (fv *FeatureVector) Clone() *FeatureVector {
features := fv.RawFeatureVector.Clone()
return NewFeatureVector(features, fv.featureNames)
}
+174
View File
@@ -0,0 +1,174 @@
package lnwire
import (
"fmt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)
// GossipVersion is a version number that describes the version of the
// gossip protocol that a gossip message was gossiped on.
type GossipVersion uint8
const (
// GossipVersion1 is the initial version of the gossip protocol as
// defined in BOLT 7. This version of the protocol can only gossip P2WSH
// channels and makes use of ECDSA signatures.
GossipVersion1 GossipVersion = 1
// GossipVersion2 is the newest version of the gossip protocol. This
// version adds support for P2TR channels and makes use of Schnorr
// signatures. The BOLT number is TBD.
GossipVersion2 GossipVersion = 2
)
// String returns a string representation of the protocol version.
func (v GossipVersion) String() string {
return fmt.Sprintf("V%d", v)
}
// GossipMessage is an interface that must be satisfied by all messages that are
// part of the gossip protocol.
type GossipMessage interface {
// GossipVersion returns the version of the gossip protocol that a
// message is part of.
GossipVersion() GossipVersion
}
// AnnounceSignatures is an interface that represents a message used to
// exchange signatures of a ChannelAnnouncment message during the funding flow.
type AnnounceSignatures interface {
// SCID returns the ShortChannelID of the channel.
SCID() ShortChannelID
// ChanID returns the ChannelID identifying the channel.
ChanID() ChannelID
Message
GossipMessage
}
// ChannelAnnouncement is an interface that must be satisfied by any message
// used to announce and prove the existence of a channel.
type ChannelAnnouncement interface {
// SCID returns the short channel ID of the channel.
SCID() ShortChannelID
// GetChainHash returns the hash of the chain which this channel's
// funding transaction is confirmed in.
GetChainHash() chainhash.Hash
// Node1KeyBytes returns the bytes representing the public key of node
// 1 in the channel.
Node1KeyBytes() [33]byte
// Node2KeyBytes returns the bytes representing the public key of node
// 2 in the channel.
Node2KeyBytes() [33]byte
Message
GossipMessage
}
// CompareResult represents the result after comparing two things.
type CompareResult uint8
const (
// LessThan indicates that base object is less than the object it was
// compared to.
LessThan CompareResult = iota
// EqualTo indicates that the base object is equal to the object it was
// compared to.
EqualTo
// GreaterThan indicates that base object is greater than the object it
// was compared to.
GreaterThan
)
// ChannelUpdate is an interface that describes a message used to update the
// forwarding rules of a channel.
type ChannelUpdate interface {
// SCID returns the ShortChannelID of the channel that the update
// applies to.
SCID() ShortChannelID
// IsNode1 is true if the update was produced by node 1 of the channel
// peers. Node 1 is the node with the lexicographically smaller public
// key.
IsNode1() bool
// IsDisabled is true if the update is announcing that the channel
// should be considered disabled.
IsDisabled() bool
// GetChainHash returns the hash of the chain that the message is
// referring to.
GetChainHash() chainhash.Hash
// ForwardingPolicy returns the set of forwarding constraints of the
// update.
ForwardingPolicy() *ForwardingPolicy
// CmpAge can be used to determine if the update is older or newer than
// the passed update. It returns LessThan if this update is older than
// the passed update, GreaterThan if it is newer and EqualTo if they are
// the same age.
CmpAge(update ChannelUpdate) (CompareResult, error)
// SetDisabledFlag can be used to adjust the disabled flag of an update.
SetDisabledFlag(bool)
// SetSCID can be used to overwrite the SCID of the update.
SetSCID(scid ShortChannelID)
Message
GossipMessage
}
// NodeAnnouncement is an interface that must be satisfied by any message used
// to announce the existence of a node.
type NodeAnnouncement interface {
// NodePub returns the identity public key of the node.
NodePub() [33]byte
// NodeFeatures returns the set of features supported by the node.
NodeFeatures() *FeatureVector
// TimestampDesc returns a human-readable description of the
// timestamp of the announcement.
TimestampDesc() string
Message
GossipMessage
}
// ForwardingPolicy defines the set of forwarding constraints advertised in a
// ChannelUpdate message.
type ForwardingPolicy struct {
// TimeLockDelta is the minimum number of blocks that the node requires
// to be added to the expiry of HTLCs. This is a security parameter
// determined by the node operator. This value represents the required
// gap between the time locks of the incoming and outgoing HTLC's set
// to this node.
TimeLockDelta uint16
// BaseFee is the base fee that must be used for incoming HTLC's to
// this particular channel. This value will be tacked onto the required
// for a payment independent of the size of the payment.
BaseFee MilliSatoshi
// FeeRate is the fee rate that will be charged per millionth of a
// satoshi.
FeeRate MilliSatoshi
// HtlcMinimumMsat is the minimum HTLC value which will be accepted.
MinHTLC MilliSatoshi
// HasMaxHTLC is true if the MaxHTLC field is provided in the update.
HasMaxHTLC bool
// HtlcMaximumMsat is the maximum HTLC value which will be accepted.
MaxHTLC MilliSatoshi
}
+426
View File
@@ -0,0 +1,426 @@
package lnwire
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
)
const (
// MaxSliceLength is the maximum allowed length for any opaque byte
// slices in the wire protocol.
MaxSliceLength = 65535
// MaxMsgBody is the largest payload any message is allowed to provide.
// This is two less than the MaxSliceLength as each message has a 2
// byte type that precedes the message body.
MaxMsgBody = 65533
)
// PkScript is simple type definition which represents a raw serialized public
// key script.
type PkScript []byte
// WriteElement is a one-stop shop to write the big endian representation of
// any element which is to be serialized for the wire protocol.
//
// TODO(yy): rm this method once we finish dereferencing it from other
// packages.
func WriteElement(w *bytes.Buffer, element interface{}) error {
switch e := element.(type) {
case uint8:
var b [1]byte
b[0] = e
if _, err := w.Write(b[:]); err != nil {
return err
}
case uint16:
var b [2]byte
binary.BigEndian.PutUint16(b[:], e)
if _, err := w.Write(b[:]); err != nil {
return err
}
case ChanUpdateMsgFlags:
var b [1]byte
b[0] = uint8(e)
if _, err := w.Write(b[:]); err != nil {
return err
}
case ChanUpdateChanFlags:
var b [1]byte
b[0] = uint8(e)
if _, err := w.Write(b[:]); err != nil {
return err
}
case MilliSatoshi:
var b [8]byte
binary.BigEndian.PutUint64(b[:], uint64(e))
if _, err := w.Write(b[:]); err != nil {
return err
}
case btcutil.Amount:
var b [8]byte
binary.BigEndian.PutUint64(b[:], uint64(e))
if _, err := w.Write(b[:]); err != nil {
return err
}
case uint32:
var b [4]byte
binary.BigEndian.PutUint32(b[:], e)
if _, err := w.Write(b[:]); err != nil {
return err
}
case uint64:
var b [8]byte
binary.BigEndian.PutUint64(b[:], e)
if _, err := w.Write(b[:]); err != nil {
return err
}
case *btcec.PublicKey:
if e == nil {
return fmt.Errorf("cannot write nil pubkey")
}
var b [33]byte
serializedPubkey := e.SerializeCompressed()
copy(b[:], serializedPubkey)
if _, err := w.Write(b[:]); err != nil {
return err
}
case []Sig:
var b [2]byte
numSigs := uint16(len(e))
binary.BigEndian.PutUint16(b[:], numSigs)
if _, err := w.Write(b[:]); err != nil {
return err
}
for _, sig := range e {
if err := WriteElement(w, sig); err != nil {
return err
}
}
case Sig:
// Write buffer
if _, err := w.Write(e.bytes[:]); err != nil {
return err
}
case ErrorData:
var l [2]byte
binary.BigEndian.PutUint16(l[:], uint16(len(e)))
if _, err := w.Write(l[:]); err != nil {
return err
}
if _, err := w.Write(e[:]); err != nil {
return err
}
case [33]byte:
if _, err := w.Write(e[:]); err != nil {
return err
}
case []byte:
if _, err := w.Write(e[:]); err != nil {
return err
}
case *RawFeatureVector:
if e == nil {
return fmt.Errorf("cannot write nil feature vector")
}
if err := e.Encode(w); err != nil {
return err
}
case ChannelID:
if _, err := w.Write(e[:]); err != nil {
return err
}
case FailCode:
if err := WriteElement(w, uint16(e)); err != nil {
return err
}
case ShortChannelID:
// Check that field fit in 3 bytes and write the blockHeight
if e.BlockHeight > ((1 << 24) - 1) {
return errors.New("block height should fit in 3 bytes")
}
var blockHeight [4]byte
binary.BigEndian.PutUint32(blockHeight[:], e.BlockHeight)
if _, err := w.Write(blockHeight[1:]); err != nil {
return err
}
// Check that field fit in 3 bytes and write the txIndex
if e.TxIndex > ((1 << 24) - 1) {
return errors.New("tx index should fit in 3 bytes")
}
var txIndex [4]byte
binary.BigEndian.PutUint32(txIndex[:], e.TxIndex)
if _, err := w.Write(txIndex[1:]); err != nil {
return err
}
// Write the txPosition
var txPosition [2]byte
binary.BigEndian.PutUint16(txPosition[:], e.TxPosition)
if _, err := w.Write(txPosition[:]); err != nil {
return err
}
case bool:
var b [1]byte
if e {
b[0] = 1
}
if _, err := w.Write(b[:]); err != nil {
return err
}
case ExtraOpaqueData:
return e.Encode(w)
default:
return fmt.Errorf("unknown type in WriteElement: %T", e)
}
return nil
}
// WriteElements is writes each element in the elements slice to the passed
// buffer using WriteElement.
//
// TODO(yy): rm this method once we finish dereferencing it from other
// packages.
func WriteElements(buf *bytes.Buffer, elements ...interface{}) error {
for _, element := range elements {
err := WriteElement(buf, element)
if err != nil {
return err
}
}
return nil
}
// ReadElement is a one-stop utility function to deserialize any datastructure
// encoded using the serialization format of lnwire.
func ReadElement(r io.Reader, element interface{}) error {
var err error
switch e := element.(type) {
case *bool:
var b [1]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
if b[0] == 1 {
*e = true
}
case *uint8:
var b [1]uint8
if _, err := r.Read(b[:]); err != nil {
return err
}
*e = b[0]
case *uint16:
var b [2]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = binary.BigEndian.Uint16(b[:])
case *ChanUpdateMsgFlags:
var b [1]uint8
if _, err := r.Read(b[:]); err != nil {
return err
}
*e = ChanUpdateMsgFlags(b[0])
case *ChanUpdateChanFlags:
var b [1]uint8
if _, err := r.Read(b[:]); err != nil {
return err
}
*e = ChanUpdateChanFlags(b[0])
case *uint32:
var b [4]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = binary.BigEndian.Uint32(b[:])
case *uint64:
var b [8]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = binary.BigEndian.Uint64(b[:])
case *MilliSatoshi:
var b [8]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = MilliSatoshi(int64(binary.BigEndian.Uint64(b[:])))
case *btcutil.Amount:
var b [8]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return err
}
*e = btcutil.Amount(int64(binary.BigEndian.Uint64(b[:])))
case **btcec.PublicKey:
var b [btcec.PubKeyBytesLenCompressed]byte
if _, err = io.ReadFull(r, b[:]); err != nil {
return err
}
pubKey, err := btcec.ParsePubKey(b[:])
if err != nil {
return err
}
*e = pubKey
case *RawFeatureVector:
f := NewRawFeatureVector()
err = f.Decode(r)
if err != nil {
return err
}
*e = *f
case **RawFeatureVector:
f := NewRawFeatureVector()
err = f.Decode(r)
if err != nil {
return err
}
*e = f
case *[]Sig:
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return err
}
numSigs := binary.BigEndian.Uint16(l[:])
var sigs []Sig
if numSigs > 0 {
sigs = make([]Sig, numSigs)
for i := 0; i < int(numSigs); i++ {
if err := ReadElement(r, &sigs[i]); err != nil {
return err
}
}
}
*e = sigs
case *Sig:
if _, err := io.ReadFull(r, e.bytes[:]); err != nil {
return err
}
case *ErrorData:
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return err
}
errorLen := binary.BigEndian.Uint16(l[:])
*e = ErrorData(make([]byte, errorLen))
if _, err := io.ReadFull(r, *e); err != nil {
return err
}
case *[33]byte:
if _, err := io.ReadFull(r, e[:]); err != nil {
return err
}
case []byte:
if _, err := io.ReadFull(r, e); err != nil {
return err
}
case *FailCode:
if err := ReadElement(r, (*uint16)(e)); err != nil {
return err
}
case *ChannelID:
if _, err := io.ReadFull(r, e[:]); err != nil {
return err
}
case *ShortChannelID:
var blockHeight [4]byte
if _, err = io.ReadFull(r, blockHeight[1:]); err != nil {
return err
}
var txIndex [4]byte
if _, err = io.ReadFull(r, txIndex[1:]); err != nil {
return err
}
var txPosition [2]byte
if _, err = io.ReadFull(r, txPosition[:]); err != nil {
return err
}
*e = ShortChannelID{
BlockHeight: binary.BigEndian.Uint32(blockHeight[:]),
TxIndex: binary.BigEndian.Uint32(txIndex[:]),
TxPosition: binary.BigEndian.Uint16(txPosition[:]),
}
case *ExtraOpaqueData:
return e.Decode(r)
default:
return fmt.Errorf("unknown type in ReadElement: %T", e)
}
return nil
}
// ReadElements deserializes a variable number of elements into the passed
// io.Reader, with each element being deserialized according to the ReadElement
// function.
func ReadElements(r io.Reader, elements ...interface{}) error {
for _, element := range elements {
err := ReadElement(r, element)
if err != nil {
return err
}
}
return nil
}
+323
View File
@@ -0,0 +1,323 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// code derived from https://github .com/btcsuite/btcd/blob/master/wire/message.go
// Copyright (C) 2015-2022 The Lightning Network Developers
package lnwire
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
// MessageTypeSize is the size in bytes of the message type field in the header
// of all messages.
const MessageTypeSize = 2
// MessageType is the unique 2 byte big-endian integer that indicates the type
// of message on the wire. All messages have a very simple header which
// consists simply of 2-byte message type. We omit a length field, and checksum
// as the Lightning Protocol is intended to be encapsulated within a
// confidential+authenticated cryptographic messaging protocol.
type MessageType uint16
// The currently defined message types within this current version of the
// Lightning protocol.
const (
MsgWarning MessageType = 1
MsgStfu = 2
MsgInit = 16
MsgError = 17
MsgPing = 18
MsgPong = 19
MsgOpenChannel = 32
MsgAcceptChannel = 33
MsgFundingCreated = 34
MsgFundingSigned = 35
MsgChannelReady = 36
MsgShutdown = 38
MsgClosingSigned = 39
MsgClosingComplete = 40
MsgClosingSig = 41
MsgDynPropose = 111
MsgDynAck = 113
MsgDynReject = 115
MsgDynCommit = 117
MsgUpdateAddHTLC = 128
MsgUpdateFulfillHTLC = 130
MsgUpdateFailHTLC = 131
MsgCommitSig = 132
MsgRevokeAndAck = 133
MsgUpdateFee = 134
MsgUpdateFailMalformedHTLC = 135
MsgChannelReestablish = 136
MsgChannelAnnouncement = 256
MsgNodeAnnouncement = 257
MsgChannelUpdate = 258
MsgAnnounceSignatures = 259
MsgAnnounceSignatures2 = 260
MsgQueryShortChanIDs = 261
MsgReplyShortChanIDsEnd = 262
MsgQueryChannelRange = 263
MsgReplyChannelRange = 264
MsgGossipTimestampRange = 265
MsgChannelAnnouncement2 = 267
MsgNodeAnnouncement2 = 269
MsgChannelUpdate2 = 271
MsgOnionMessage = 513
MsgKickoffSig = 777
// MsgEnd defines the end of the official message range of the protocol.
// If a new message is added beyond this message, then this should be
// modified.
MsgEnd = 778
)
// IsChannelUpdate is a filter function that discerns channel update messages
// from the other messages in the Lightning Network Protocol.
func (t MessageType) IsChannelUpdate() bool {
switch t {
case MsgUpdateAddHTLC:
return true
case MsgUpdateFulfillHTLC:
return true
case MsgUpdateFailHTLC:
return true
case MsgUpdateFailMalformedHTLC:
return true
case MsgUpdateFee:
return true
default:
return false
}
}
// ErrorEncodeMessage is used when failed to encode the message payload.
func ErrorEncodeMessage(err error) error {
return fmt.Errorf("failed to encode message to buffer, got %w", err)
}
// ErrorWriteMessageType is used when failed to write the message type.
func ErrorWriteMessageType(err error) error {
return fmt.Errorf("failed to write message type, got %w", err)
}
// ErrorPayloadTooLarge is used when the payload size exceeds the
// MaxMsgBody.
func ErrorPayloadTooLarge(size int) error {
return fmt.Errorf(
"message payload is too large - encoded %d bytes, "+
"but maximum message payload is %d bytes",
size, MaxMsgBody,
)
}
// String return the string representation of message type.
func (t MessageType) String() string {
switch t {
case MsgWarning:
return "Warning"
case MsgStfu:
return "Stfu"
case MsgInit:
return "Init"
case MsgOpenChannel:
return "MsgOpenChannel"
case MsgAcceptChannel:
return "MsgAcceptChannel"
case MsgFundingCreated:
return "MsgFundingCreated"
case MsgFundingSigned:
return "MsgFundingSigned"
case MsgChannelReady:
return "ChannelReady"
case MsgShutdown:
return "Shutdown"
case MsgClosingSigned:
return "ClosingSigned"
case MsgDynPropose:
return "DynPropose"
case MsgDynAck:
return "DynAck"
case MsgDynReject:
return "DynReject"
case MsgDynCommit:
return "DynCommit"
case MsgKickoffSig:
return "KickoffSig"
case MsgUpdateAddHTLC:
return "UpdateAddHTLC"
case MsgUpdateFailHTLC:
return "UpdateFailHTLC"
case MsgUpdateFulfillHTLC:
return "UpdateFulfillHTLC"
case MsgCommitSig:
return "CommitSig"
case MsgRevokeAndAck:
return "RevokeAndAck"
case MsgUpdateFailMalformedHTLC:
return "UpdateFailMalformedHTLC"
case MsgChannelReestablish:
return "ChannelReestablish"
case MsgError:
return "Error"
case MsgChannelAnnouncement:
return "ChannelAnnouncement"
case MsgChannelUpdate:
return "ChannelUpdate"
case MsgNodeAnnouncement:
return "NodeAnnouncement1"
case MsgPing:
return "Ping"
case MsgAnnounceSignatures:
return "AnnounceSignatures"
case MsgPong:
return "Pong"
case MsgUpdateFee:
return "UpdateFee"
case MsgQueryShortChanIDs:
return "QueryShortChanIDs"
case MsgReplyShortChanIDsEnd:
return "ReplyShortChanIDsEnd"
case MsgQueryChannelRange:
return "QueryChannelRange"
case MsgReplyChannelRange:
return "ReplyChannelRange"
case MsgGossipTimestampRange:
return "GossipTimestampRange"
case MsgClosingComplete:
return "ClosingComplete"
case MsgClosingSig:
return "ClosingSig"
case MsgAnnounceSignatures2:
return "MsgAnnounceSignatures2"
case MsgChannelAnnouncement2:
return "ChannelAnnouncement2"
case MsgNodeAnnouncement2:
return "NodeAnnouncement2"
case MsgChannelUpdate2:
return "ChannelUpdate2"
case MsgOnionMessage:
return "OnionMessage"
default:
return "<unknown>"
}
}
// UnknownMessage is an implementation of the error interface that allows the
// creation of an error in response to an unknown message.
type UnknownMessage struct {
messageType MessageType
}
// Error returns a human readable string describing the error.
//
// This is part of the error interface.
func (u *UnknownMessage) Error() string {
return fmt.Sprintf("unable to parse message of unknown type: %v",
u.messageType)
}
// Serializable is an interface which defines a lightning wire serializable
// object.
type Serializable interface {
// Decode reads the bytes stream and converts it to the object.
Decode(io.Reader, uint32) error
// Encode converts object to the bytes stream and write it into the
// write buffer.
Encode(*bytes.Buffer, uint32) error
}
// Message is an interface that defines a lightning wire protocol message. The
// interface is general in order to allow implementing types full control over
// the representation of its data.
type Message interface {
Serializable
MsgType() MessageType
}
// LinkUpdater is an interface implemented by most messages in BOLT 2 that are
// allowed to update the channel state.
type LinkUpdater interface {
// All LinkUpdater messages are messages and so we embed the interface
// so that we can treat it as a message if all we know about it is that
// it is a LinkUpdater message.
Message
// TargetChanID returns the channel id of the link for which this
// message is intended.
TargetChanID() ChannelID
}
// SizeableMessage is an interface that extends the base Message interface with
// a method to calculate the serialized size of a message.
type SizeableMessage interface {
Message
// SerializedSize returns the serialized size of the message in bytes.
// The returned size includes the message type header bytes.
SerializedSize() (uint32, error)
}
// MessageSerializedSize calculates the serialized size of a message in bytes.
// This is a helper function that can be used by all message types to implement
// the SerializedSize method.
func MessageSerializedSize(msg Message) (uint32, error) {
var buf bytes.Buffer
// Encode the message to the buffer.
if err := msg.Encode(&buf, 0); err != nil {
return 0, err
}
// Add the size of the message type.
return uint32(buf.Len()) + MessageTypeSize, nil
}
// WriteMessage writes a lightning Message to a buffer including the necessary
// header information and returns the number of bytes written. If any error is
// encountered, the buffer passed will be reset to its original state since we
// don't want any broken bytes left. In other words, no bytes will be written
// if there's an error. Either all or none of the message bytes will be written
// to the buffer.
//
// NOTE: this method is not concurrent safe.
func WriteMessage(buf *bytes.Buffer, msg Message, pver uint32) (int, error) {
// Record the size of the bytes already written in buffer.
oldByteSize := buf.Len()
// cleanBrokenBytes is a helper closure that helps reset the buffer to
// its original state. It truncates all the bytes written in current
// scope.
var cleanBrokenBytes = func(b *bytes.Buffer) int {
b.Truncate(oldByteSize)
return 0
}
// Write the message type.
var mType [2]byte
binary.BigEndian.PutUint16(mType[:], uint16(msg.MsgType()))
msgTypeBytes, err := buf.Write(mType[:])
if err != nil {
return cleanBrokenBytes(buf), ErrorWriteMessageType(err)
}
// Use the write buffer to encode our message.
if err := msg.Encode(buf, pver); err != nil {
return cleanBrokenBytes(buf), ErrorEncodeMessage(err)
}
// Enforce maximum overall message payload. The write buffer now has
// the size of len(originalBytes) + len(payload) + len(type). We want
// to enforce the payload here, so we subtract it by the length of the
// type and old bytes.
lenp := buf.Len() - oldByteSize - msgTypeBytes
if lenp > MaxMsgBody {
return cleanBrokenBytes(buf), ErrorPayloadTooLarge(lenp)
}
return buf.Len() - oldByteSize, nil
}
+92
View File
@@ -0,0 +1,92 @@
package lnwire
import (
"fmt"
"io"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// mSatScale is a value that's used to scale satoshis to milli-satoshis, and
// the other way around.
mSatScale uint64 = 1000
// MaxMilliSatoshi is the maximum number of msats that can be expressed
// in this data type.
MaxMilliSatoshi = ^MilliSatoshi(0)
)
// MilliSatoshi are the native unit of the Lightning Network. A milli-satoshi
// is simply 1/1000th of a satoshi. There are 1000 milli-satoshis in a single
// satoshi. Within the network, all HTLC payments are denominated in
// milli-satoshis. As milli-satoshis aren't deliverable on the native
// blockchain, before settling to broadcasting, the values are rounded down to
// the nearest satoshi.
type MilliSatoshi uint64
// NewMSatFromSatoshis creates a new MilliSatoshi instance from a target amount
// of satoshis.
func NewMSatFromSatoshis(sat btcutil.Amount) MilliSatoshi {
return MilliSatoshi(uint64(sat) * mSatScale)
}
// ToBTC converts the target MilliSatoshi amount to its corresponding value
// when expressed in BTC.
func (m MilliSatoshi) ToBTC() float64 {
sat := m.ToSatoshis()
return sat.ToBTC()
}
// ToSatoshis converts the target MilliSatoshi amount to satoshis. Simply, this
// sheds a factor of 1000 from the mSAT amount in order to convert it to SAT.
func (m MilliSatoshi) ToSatoshis() btcutil.Amount {
return btcutil.Amount(uint64(m) / mSatScale)
}
// String returns the string representation of the mSAT amount.
func (m MilliSatoshi) String() string {
return fmt.Sprintf("%v mSAT", uint64(m))
}
// TODO(roasbeef): extend with arithmetic operations?
// Record returns a TLV record that can be used to encode/decode a MilliSatoshi
// to/from a TLV stream.
func (m *MilliSatoshi) Record() tlv.Record {
msat := uint64(*m)
return tlv.MakeDynamicRecord(
0, m, tlv.SizeBigSize(&msat), encodeMilliSatoshis,
decodeMilliSatoshis,
)
}
func encodeMilliSatoshis(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*MilliSatoshi); ok {
bigSize := uint64(*v)
return tlv.EBigSize(w, &bigSize, buf)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.MilliSatoshi")
}
func decodeMilliSatoshis(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*MilliSatoshi); ok {
var bigSize uint64
err := tlv.DBigSize(r, &bigSize, buf, l)
if err != nil {
return err
}
*v = MilliSatoshi(bigSize)
return nil
}
return tlv.NewTypeForDecodingErr(val, "lnwire.MilliSatoshi", l, l)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,105 @@
package lnwire
import (
"fmt"
"io"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// AliasScidRecordType is the type of the experimental record to denote
// the alias being used in an option_scid_alias channel.
AliasScidRecordType tlv.Type = 1
)
// ShortChannelID represents the set of data which is needed to retrieve all
// necessary data to validate the channel existence.
type ShortChannelID struct {
// BlockHeight is the height of the block where funding transaction
// located.
//
// NOTE: This field is limited to 3 bytes.
BlockHeight uint32
// TxIndex is a position of funding transaction within a block.
//
// NOTE: This field is limited to 3 bytes.
TxIndex uint32
// TxPosition indicating transaction output which pays to the channel.
TxPosition uint16
}
// NewShortChanIDFromInt returns a new ShortChannelID which is the decoded
// version of the compact channel ID encoded within the uint64. The format of
// the compact channel ID is as follows: 3 bytes for the block height, 3 bytes
// for the transaction index, and 2 bytes for the output index.
func NewShortChanIDFromInt(chanID uint64) ShortChannelID {
return ShortChannelID{
BlockHeight: uint32(chanID >> 40),
TxIndex: uint32(chanID>>16) & 0xFFFFFF,
TxPosition: uint16(chanID),
}
}
// ToUint64 converts the ShortChannelID into a compact format encoded within a
// uint64 (8 bytes).
func (c ShortChannelID) ToUint64() uint64 {
// TODO(roasbeef): explicit error on overflow?
return ((uint64(c.BlockHeight) << 40) | (uint64(c.TxIndex) << 16) |
(uint64(c.TxPosition)))
}
// String generates a human-readable representation of the channel ID.
func (c ShortChannelID) String() string {
return fmt.Sprintf("%d:%d:%d", c.BlockHeight, c.TxIndex, c.TxPosition)
}
// AltString generates a human-readable representation of the channel ID
// with 'x' as a separator.
func (c ShortChannelID) AltString() string {
return fmt.Sprintf("%dx%dx%d", c.BlockHeight, c.TxIndex, c.TxPosition)
}
// Record returns a TLV record that can be used to encode/decode a
// ShortChannelID to/from a TLV stream.
func (c *ShortChannelID) Record() tlv.Record {
return tlv.MakeStaticRecord(
AliasScidRecordType, c, 8, EShortChannelID, DShortChannelID,
)
}
// IsDefault returns true if the ShortChannelID represents the zero value for
// its type.
func (c ShortChannelID) IsDefault() bool {
return c == ShortChannelID{}
}
// EShortChannelID is an encoder for ShortChannelID. It is exported so other
// packages can use the encoding scheme.
func EShortChannelID(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*ShortChannelID); ok {
return tlv.EUint64T(w, v.ToUint64(), buf)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.ShortChannelID")
}
// DShortChannelID is a decoder for ShortChannelID. It is exported so other
// packages can use the decoding scheme.
func DShortChannelID(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*ShortChannelID); ok {
var scid uint64
// tlv.DUint64 forces the length to be 8 bytes.
err := tlv.DUint64(r, &scid, buf, l)
if err != nil {
return err
}
*v = NewShortChanIDFromInt(scid)
return nil
}
return tlv.NewTypeForDecodingErr(val, "lnwire.ShortChannelID", l, 8)
}
+292
View File
@@ -0,0 +1,292 @@
package lnwire
import (
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/tlv"
)
var (
errSigTooShort = errors.New("malformed signature: too short")
errBadLength = errors.New("malformed signature: bad length")
errBadRLength = errors.New("malformed signature: bogus R length")
errBadSLength = errors.New("malformed signature: bogus S length")
errRTooLong = errors.New("R is over 32 bytes long without padding")
errSTooLong = errors.New("S is over 32 bytes long without padding")
)
// sigType represents the type of signature that is carried within the Sig.
// Today this can either be an ECDSA sig or a schnorr sig. Both of these can
// fit cleanly into 64 bytes.
type sigType uint
const (
// sigTypeECDSA represents an ECDSA signature.
sigTypeECDSA sigType = iota
// sigTypeSchnorr represents a schnorr signature.
sigTypeSchnorr
)
// Sig is a fixed-sized ECDSA signature or 64-byte schnorr signature. For the
// ECDSA sig, unlike Bitcoin, we use fixed sized signatures on the wire,
// instead of DER encoded signatures. This type provides several methods to
// convert to/from a regular Bitcoin DER encoded signature (raw bytes and
// *ecdsa.Signature).
type Sig struct {
bytes [64]byte
sigType sigType
}
// ForceSchnorr forces the signature to be interpreted as a schnorr signature.
// This is useful when reading an HTLC sig off the wire for a taproot channel.
// In this case, in order to obtain an input.Signature, we need to know that
// the sig is a schnorr sig.
func (s *Sig) ForceSchnorr() {
s.sigType = sigTypeSchnorr
}
// RawBytes returns the raw bytes of signature.
func (s *Sig) RawBytes() []byte {
return s.bytes[:]
}
// Copy copies the signature into a new Sig instance.
func (s *Sig) Copy() Sig {
var sCopy Sig
copy(sCopy.bytes[:], s.bytes[:])
sCopy.sigType = s.sigType
return sCopy
}
// Record returns a Record that can be used to encode or decode the backing
// object.
//
// This returns a record that serializes the sig as a 64-byte fixed size
// signature.
func (s *Sig) Record() tlv.Record {
// We set a type here as zero as it isn't needed when used as a
// RecordT.
return tlv.MakePrimitiveRecord(0, &s.bytes)
}
// NewSigFromWireECDSA returns a Sig instance based on an ECDSA signature
// that's already in the 64-byte format we expect.
func NewSigFromWireECDSA(sig []byte) (Sig, error) {
if len(sig) != 64 {
return Sig{}, fmt.Errorf("%w: %v bytes", errSigTooShort,
len(sig))
}
var s Sig
copy(s.bytes[:], sig)
return s, nil
}
// NewSigFromECDSARawSignature returns a Sig from a Bitcoin raw signature
// encoded in the canonical DER encoding.
func NewSigFromECDSARawSignature(sig []byte) (Sig, error) {
var b [64]byte
// Check the total length is above the minimal.
if len(sig) < ecdsa.MinSigLen {
return Sig{}, errSigTooShort
}
// The DER representation is laid out as:
// 0x30 <length> 0x02 <length r> r 0x02 <length s> s
// which means the length of R is the 4th byte and the length of S is
// the second byte after R ends. 0x02 signifies a length-prefixed,
// zero-padded, big-endian bigint. 0x30 signifies a DER signature.
// See the Serialize() method for ecdsa.Signature for details.
// Reading <length>, remaining: [0x02 <length r> r 0x02 <length s> s]
sigLen := int(sig[1])
// siglen should be less than the entire message and greater than
// the minimal message size.
if sigLen+2 > len(sig) || sigLen+2 < ecdsa.MinSigLen {
return Sig{}, errBadLength
}
// Reading <length r>, remaining: [r 0x02 <length s> s]
rLen := int(sig[3])
// rLen must be positive and must be able to fit in other elements.
// Assuming s is one byte, then we have 0x30, <length>, 0x20,
// <length r>, 0x20, <length s>, s, a total of 7 bytes.
if rLen <= 0 || rLen+7 > len(sig) {
return Sig{}, errBadRLength
}
// Reading <length s>, remaining: [s]
sLen := int(sig[5+rLen])
// S should be the rest of the string.
// sLen must be positive and must be able to fit in other elements.
// We know r is rLen bytes, and we have 0x30, <length>, 0x20,
// <length r>, 0x20, <length s>, a total of rLen+6 bytes.
if sLen <= 0 || sLen+rLen+6 > len(sig) {
return Sig{}, errBadSLength
}
// Check to make sure R and S can both fit into their intended buffers.
// We check S first because these code blocks decrement sLen and rLen
// in the case of a 33-byte 0-padded integer returned from Serialize()
// and rLen is used in calculating array indices for S. We can track
// this with additional variables, but it's more efficient to just
// check S first.
if sLen > 32 {
if (sLen > 33) || (sig[6+rLen] != 0x00) {
return Sig{}, errSTooLong
}
sLen--
copy(b[64-sLen:], sig[7+rLen:])
} else {
copy(b[64-sLen:], sig[6+rLen:])
}
// Do the same for R as we did for S
if rLen > 32 {
if (rLen > 33) || (sig[4] != 0x00) {
return Sig{}, errRTooLong
}
rLen--
copy(b[32-rLen:], sig[5:5+rLen])
} else {
copy(b[32-rLen:], sig[4:4+rLen])
}
return Sig{
bytes: b,
sigType: sigTypeECDSA,
}, nil
}
// NewSigFromSchnorrRawSignature converts a raw schnorr signature into an
// lnwire.Sig.
func NewSigFromSchnorrRawSignature(sig []byte) (Sig, error) {
var s Sig
copy(s.bytes[:], sig)
s.sigType = sigTypeSchnorr
return s, nil
}
// NewSigFromSignature creates a new signature as used on the wire, from an
// existing ecdsa.Signature or schnorr.Signature.
func NewSigFromSignature(e input.Signature) (Sig, error) {
if e == nil {
return Sig{}, fmt.Errorf("cannot decode empty signature")
}
// Nil is still a valid interface, apparently. So we need a more
// explicit check here.
if ecsig, ok := e.(*ecdsa.Signature); ok && ecsig == nil {
return Sig{}, fmt.Errorf("cannot decode empty signature")
}
switch ecSig := e.(type) {
// If this is a schnorr signature, then we can just pack it as normal,
// since the default encoding is already 64 bytes.
case *schnorr.Signature:
return NewSigFromSchnorrRawSignature(e.Serialize())
// For ECDSA signatures, we'll need to do a bit more work to map the
// signature into a compact 64 byte form.
case *ecdsa.Signature:
// Serialize the signature with all the checks that entails.
return NewSigFromECDSARawSignature(e.Serialize())
default:
return Sig{}, fmt.Errorf("unknown wire sig type: %T", ecSig)
}
}
// ToSignature converts the fixed-sized signature to a input.Signature which
// can be used for signature validation checks.
func (s *Sig) ToSignature() (input.Signature, error) {
switch s.sigType {
case sigTypeSchnorr:
return schnorr.ParseSignature(s.bytes[:])
case sigTypeECDSA:
// Parse the signature with strict checks.
sigBytes := s.ToSignatureBytes()
sig, err := ecdsa.ParseDERSignature(sigBytes)
if err != nil {
return nil, err
}
return sig, nil
default:
return nil, fmt.Errorf("unknown sig type: %v", s.sigType)
}
}
// ToSignatureBytes serializes the target fixed-sized signature into the
// encoding of the primary domain for the signature. For ECDSA signatures, this
// is the raw bytes of a DER encoding.
func (s *Sig) ToSignatureBytes() []byte {
switch s.sigType {
// For ECDSA signatures, we'll convert to DER encoding.
case sigTypeECDSA:
// Extract canonically-padded bigint representations from buffer
r := extractCanonicalPadding(s.bytes[0:32])
s := extractCanonicalPadding(s.bytes[32:64])
rLen := uint8(len(r))
sLen := uint8(len(s))
// Create a canonical serialized signature. DER format is:
// 0x30 <length> 0x02 <length r> r 0x02 <length s> s
sigBytes := make([]byte, 6+rLen+sLen)
sigBytes[0] = 0x30 // DER signature magic value
sigBytes[1] = 4 + rLen + sLen // Length of rest of signature
sigBytes[2] = 0x02 // Big integer magic value
sigBytes[3] = rLen // Length of R
sigBytes[rLen+4] = 0x02 // Big integer magic value
sigBytes[rLen+5] = sLen // Length of S
copy(sigBytes[4:], r) // Copy R
copy(sigBytes[rLen+6:], s) // Copy S
return sigBytes
// For schnorr signatures, we can use the same internal 64 bytes.
case sigTypeSchnorr:
// We'll make a copy of the signature so we don't return a
// reference into the raw slice.
var sig [64]byte
copy(sig[:], s.bytes[:])
return sig[:]
default:
// TODO(roasbeef): can only be called via public methods so
// never reachable?
panic("sig type not set")
}
}
// extractCanonicalPadding is a utility function to extract the canonical
// padding of a big-endian integer from the wire encoding (a 0-padded
// big-endian integer) such that it passes btcec.canonicalPadding test.
func extractCanonicalPadding(b []byte) []byte {
for i := 0; i < len(b); i++ {
// Found first non-zero byte.
if b[i] > 0 {
// If the MSB is set, we need zero padding.
if b[i]&0x80 == 0x80 {
return append([]byte{0x00}, b[i:]...)
}
return b[i:]
}
}
return []byte{0x00}
}
@@ -0,0 +1,60 @@
package lnwire
import (
"io"
"github.com/lightningnetwork/lnd/tlv"
)
const (
FeeRecordType tlv.Type = 55555
)
// Fee represents a fee schedule.
type Fee struct {
BaseFee int32
FeeRate int32
}
// Record returns a TLV record that can be used to encode/decode the fee
// type from a given TLV stream.
func (l *Fee) Record() tlv.Record {
return tlv.MakeStaticRecord(
FeeRecordType, l, 8, feeEncoder, feeDecoder,
)
}
// feeEncoder is a custom TLV encoder for the fee record.
func feeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
v, ok := val.(*Fee)
if !ok {
return tlv.NewTypeForEncodingErr(val, "lnwire.Fee")
}
if err := tlv.EUint32T(w, uint32(v.BaseFee), buf); err != nil {
return err
}
return tlv.EUint32T(w, uint32(v.FeeRate), buf)
}
// feeDecoder is a custom TLV decoder for the fee record.
func feeDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
v, ok := val.(*Fee)
if !ok || l != 8 {
return tlv.NewTypeForDecodingErr(val, "lnwire.Fee", l, 8)
}
var baseFee, feeRate uint32
if err := tlv.DUint32(r, &baseFee, buf, 4); err != nil {
return err
}
if err := tlv.DUint32(r, &feeRate, buf, 4); err != nil {
return err
}
v.FeeRate = int32(feeRate)
v.BaseFee = int32(baseFee)
return nil
}
@@ -0,0 +1,226 @@
package lnwire
import (
"bytes"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// OnionPacketSize is the size of the serialized Sphinx onion packet
// included in each UpdateAddHTLC message. The breakdown of the onion
// packet is as follows: 1-byte version, 33-byte ephemeral public key
// (for ECDH), 1300-bytes of per-hop data, and a 32-byte HMAC over the
// entire packet.
OnionPacketSize = 1366
// ExperimentalAccountableType is the TLV type used for a custom
// record that sets an experimental accountable value.
ExperimentalAccountableType tlv.Type = 106823
// ExperimentalUnaccountable is the value that the experimental
// accountable field contains when a htlc is not accountable.
ExperimentalUnaccountable = 0
// ExperimentalAccountable is the value that the experimental
// accountable field contains when a htlc is accountable. We're using a
// single byte to represent our accountable value, but limit the value
// to using the first three bits (max value = 00000111). Interpreted as
// a uint8 (an alias for byte in go), we can just define this constant
// as 7.
ExperimentalAccountable = 7
)
type (
// BlindingPointTlvType is the type for ephemeral pubkeys used in
// route blinding.
BlindingPointTlvType = tlv.TlvType0
// BlindingPointRecord holds an optional blinding point on update add
// htlc.
//nolint:ll
BlindingPointRecord = tlv.OptionalRecordT[BlindingPointTlvType, *btcec.PublicKey]
)
// UpdateAddHTLC is the message sent by Alice to Bob when she wishes to add an
// HTLC to his remote commitment transaction. In addition to information
// detailing the value, the ID, expiry, and the onion blob is also included
// which allows Bob to derive the next hop in the route. The HTLC added by this
// message is to be added to the remote node's "pending" HTLCs. A subsequent
// CommitSig message will move the pending HTLC to the newly created commitment
// transaction, marking them as "staged".
type UpdateAddHTLC struct {
// ChanID is the particular active channel that this UpdateAddHTLC is
// bound to.
ChanID ChannelID
// ID is the identification server for this HTLC. This value is
// explicitly included as it allows nodes to survive single-sided
// restarts. The ID value for this sides starts at zero, and increases
// with each offered HTLC.
ID uint64
// Amount is the amount of millisatoshis this HTLC is worth.
Amount MilliSatoshi
// PaymentHash is the payment hash to be included in the HTLC this
// request creates. The pre-image to this HTLC must be revealed by the
// upstream peer in order to fully settle the HTLC.
PaymentHash [32]byte
// Expiry is the number of blocks after which this HTLC should expire.
// It is the receiver's duty to ensure that the outgoing HTLC has a
// sufficient expiry value to allow her to redeem the incoming HTLC.
Expiry uint32
// OnionBlob is the raw serialized mix header used to route an HTLC in
// a privacy-preserving manner. The mix header is defined currently to
// be parsed as a 4-tuple: (groupElement, routingInfo, headerMAC,
// body). First the receiving node should use the groupElement, and
// its current onion key to derive a shared secret with the source.
// Once the shared secret has been derived, the headerMAC should be
// checked FIRST. Note that the MAC only covers the routingInfo field.
// If the MAC matches, and the shared secret is fresh, then the node
// should strip off a layer of encryption, exposing the next hop to be
// used in the subsequent UpdateAddHTLC message.
OnionBlob [OnionPacketSize]byte
// BlindingPoint is the ephemeral pubkey used to optionally blind the
// next hop for this htlc.
BlindingPoint BlindingPointRecord
// CustomRecords maps TLV types to byte slices, storing arbitrary data
// intended for inclusion in the ExtraData field of the UpdateAddHTLC
// message.
CustomRecords CustomRecords
// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
ExtraData ExtraOpaqueData
}
// NewUpdateAddHTLC returns a new empty UpdateAddHTLC message.
func NewUpdateAddHTLC() *UpdateAddHTLC {
return &UpdateAddHTLC{}
}
// A compile time check to ensure UpdateAddHTLC implements the lnwire.Message
// interface.
var _ Message = (*UpdateAddHTLC)(nil)
// Decode deserializes a serialized UpdateAddHTLC message stored in the passed
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
// msgExtraData is a temporary variable used to read the message extra
// data field from the reader.
var msgExtraData ExtraOpaqueData
if err := ReadElements(r,
&c.ChanID,
&c.ID,
&c.Amount,
c.PaymentHash[:],
&c.Expiry,
c.OnionBlob[:],
&msgExtraData,
); err != nil {
return err
}
// Extract TLV records from the extra data field.
blindingRecord := c.BlindingPoint.Zero()
customRecords, parsed, extraData, err := ParseAndExtractCustomRecords(
msgExtraData, &blindingRecord,
)
if err != nil {
return err
}
// Assign the parsed records back to the message.
if parsed.Contains(blindingRecord.TlvType()) {
c.BlindingPoint = tlv.SomeRecordT(blindingRecord)
}
c.CustomRecords = customRecords
c.ExtraData = extraData
return nil
}
// Encode serializes the target UpdateAddHTLC into the passed io.Writer
// observing the protocol version specified.
//
// This is part of the lnwire.Message interface.
func (c *UpdateAddHTLC) Encode(w *bytes.Buffer, pver uint32) error {
if err := WriteChannelID(w, c.ChanID); err != nil {
return err
}
if err := WriteUint64(w, c.ID); err != nil {
return err
}
if err := WriteMilliSatoshi(w, c.Amount); err != nil {
return err
}
if err := WriteBytes(w, c.PaymentHash[:]); err != nil {
return err
}
if err := WriteUint32(w, c.Expiry); err != nil {
return err
}
if err := WriteBytes(w, c.OnionBlob[:]); err != nil {
return err
}
// Only include blinding point in extra data if present.
var records []tlv.RecordProducer
c.BlindingPoint.WhenSome(
func(b tlv.RecordT[BlindingPointTlvType, *btcec.PublicKey]) {
records = append(records, &b)
},
)
extraData, err := MergeAndEncode(records, c.ExtraData, c.CustomRecords)
if err != nil {
return err
}
return WriteBytes(w, extraData)
}
// MsgType returns the integer uniquely identifying this message type on the
// wire.
//
// This is part of the lnwire.Message interface.
func (c *UpdateAddHTLC) MsgType() MessageType {
return MsgUpdateAddHTLC
}
// TargetChanID returns the channel id of the link for which this message is
// intended.
//
// NOTE: Part of peer.LinkUpdater interface.
func (c *UpdateAddHTLC) TargetChanID() ChannelID {
return c.ChanID
}
// SerializedSize returns the serialized size of the message in bytes.
//
// This is part of the lnwire.SizeableMessage interface.
func (c *UpdateAddHTLC) SerializedSize() (uint32, error) {
return MessageSerializedSize(c)
}
// A compile time check to ensure UpdateAddHTLC implements the
// lnwire.SizeableMessage interface.
var _ SizeableMessage = (*UpdateAddHTLC)(nil)
+185
View File
@@ -0,0 +1,185 @@
package lnwire
import (
"bytes"
"encoding/binary"
"errors"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
)
var (
// ErrNilFeatureVector is returned when the supplied feature is nil.
ErrNilFeatureVector = errors.New("cannot write nil feature vector")
// ErrNilPublicKey is returned when a nil pubkey is used.
ErrNilPublicKey = errors.New("cannot write nil pubkey")
)
// WriteBytes appends the given bytes to the provided buffer.
func WriteBytes(buf *bytes.Buffer, b []byte) error {
_, err := buf.Write(b)
return err
}
// WriteUint8 appends the uint8 to the provided buffer.
func WriteUint8(buf *bytes.Buffer, n uint8) error {
_, err := buf.Write([]byte{n})
return err
}
// WriteUint16 appends the uint16 to the provided buffer. It encodes the
// integer using big endian byte order.
func WriteUint16(buf *bytes.Buffer, n uint16) error {
var b [2]byte
binary.BigEndian.PutUint16(b[:], n)
_, err := buf.Write(b[:])
return err
}
// WriteUint32 appends the uint32 to the provided buffer. It encodes the
// integer using big endian byte order.
func WriteUint32(buf *bytes.Buffer, n uint32) error {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
_, err := buf.Write(b[:])
return err
}
// WriteUint64 appends the uint64 to the provided buffer. It encodes the
// integer using big endian byte order.
func WriteUint64(buf *bytes.Buffer, n uint64) error {
var b [8]byte
binary.BigEndian.PutUint64(b[:], n)
_, err := buf.Write(b[:])
return err
}
// WriteSatoshi appends the Satoshi value to the provided buffer.
func WriteSatoshi(buf *bytes.Buffer, amount btcutil.Amount) error {
return WriteUint64(buf, uint64(amount))
}
// WriteMilliSatoshi appends the MilliSatoshi value to the provided buffer.
func WriteMilliSatoshi(buf *bytes.Buffer, amount MilliSatoshi) error {
return WriteUint64(buf, uint64(amount))
}
// WritePublicKey appends the compressed public key to the provided buffer.
func WritePublicKey(buf *bytes.Buffer, pub *btcec.PublicKey) error {
if pub == nil {
return ErrNilPublicKey
}
serializedPubkey := pub.SerializeCompressed()
return WriteBytes(buf, serializedPubkey)
}
// WriteChannelID appends the ChannelID to the provided buffer.
func WriteChannelID(buf *bytes.Buffer, channelID ChannelID) error {
return WriteBytes(buf, channelID[:])
}
// WriteShortChannelID appends the ShortChannelID to the provided buffer. It
// encodes the BlockHeight and TxIndex each using 3 bytes with big endian byte
// order, and encodes txPosition using 2 bytes with big endian byte order.
func WriteShortChannelID(buf *bytes.Buffer, shortChanID ShortChannelID) error {
// Check that field fit in 3 bytes and write the blockHeight
if shortChanID.BlockHeight > ((1 << 24) - 1) {
return errors.New("block height should fit in 3 bytes")
}
var blockHeight [4]byte
binary.BigEndian.PutUint32(blockHeight[:], shortChanID.BlockHeight)
if _, err := buf.Write(blockHeight[1:]); err != nil {
return err
}
// Check that field fit in 3 bytes and write the txIndex
if shortChanID.TxIndex > ((1 << 24) - 1) {
return errors.New("tx index should fit in 3 bytes")
}
var txIndex [4]byte
binary.BigEndian.PutUint32(txIndex[:], shortChanID.TxIndex)
if _, err := buf.Write(txIndex[1:]); err != nil {
return err
}
// Write the TxPosition
return WriteUint16(buf, shortChanID.TxPosition)
}
// WriteSig appends the signature to the provided buffer.
func WriteSig(buf *bytes.Buffer, sig Sig) error {
return WriteBytes(buf, sig.bytes[:])
}
// WriteSigs appends the slice of signatures to the provided buffer with its
// length.
func WriteSigs(buf *bytes.Buffer, sigs []Sig) error {
// Write the length of the sigs.
if err := WriteUint16(buf, uint16(len(sigs))); err != nil {
return err
}
for _, sig := range sigs {
if err := WriteSig(buf, sig); err != nil {
return err
}
}
return nil
}
// WriteFailCode appends the FailCode to the provided buffer.
func WriteFailCode(buf *bytes.Buffer, e FailCode) error {
return WriteUint16(buf, uint16(e))
}
// WriteRawFeatureVector encodes the feature using the feature's Encode method
// and appends the data to the provided buffer. An error will return if the
// passed feature is nil.
func WriteRawFeatureVector(buf *bytes.Buffer, feature *RawFeatureVector) error {
if feature == nil {
return ErrNilFeatureVector
}
return feature.Encode(buf)
}
// WriteChanUpdateMsgFlags appends the update flag to the provided buffer.
func WriteChanUpdateMsgFlags(buf *bytes.Buffer, f ChanUpdateMsgFlags) error {
return WriteUint8(buf, uint8(f))
}
// WriteChanUpdateChanFlags appends the update flag to the provided buffer.
func WriteChanUpdateChanFlags(buf *bytes.Buffer, f ChanUpdateChanFlags) error {
return WriteUint8(buf, uint8(f))
}
// WriteErrorData appends the data to the provided buffer.
func WriteErrorData(buf *bytes.Buffer, data ErrorData) error {
return writeDataWithLength(buf, data)
}
// WriteBool appends the boolean to the provided buffer.
func WriteBool(buf *bytes.Buffer, b bool) error {
if b {
return WriteBytes(buf, []byte{1})
}
return WriteBytes(buf, []byte{0})
}
// writeDataWithLength writes the data and its length to the buffer.
func writeDataWithLength(buf *bytes.Buffer, data []byte) error {
var l [2]byte
binary.BigEndian.PutUint16(l[:], uint16(len(data)))
if _, err := buf.Write(l[:]); err != nil {
return err
}
_, err := buf.Write(data)
return err
}
+32
View File
@@ -0,0 +1,32 @@
package migration1
import (
"github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// Subsystem defines the logging identifier for this subsystem.
const Subsystem = "PYDB"
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}
@@ -0,0 +1,373 @@
//go:build test_db_postgres || test_db_sqlite
package migration1
import (
"context"
"fmt"
"os"
"path"
"strings"
"testing"
"time"
"github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/kvdb/postgres"
"github.com/lightningnetwork/lnd/kvdb/sqlbase"
"github.com/lightningnetwork/lnd/kvdb/sqlite"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// TestMigrationWithExternalDB tests the migration of the payment store from a
// bolt backed channel.db or a kvdb channel.sqlite to a SQL database. Note that
// this test does not attempt to be a complete migration test for all payment
// store types but rather is added as a tool for developers and users to debug
// payment migration issues with an actual channel.db/channel.sqlite file.
//
// NOTE: To use this test, place either of those files in the
// payments/db/migration1/testdata directory, uncomment the "Skipf" line, and
// set the "fileName" variable to the name of the channel database file you
// want to use for the migration test.
func TestMigrationWithExternalDB(t *testing.T) {
ctx := context.Background()
// NOTE: comment this line out to run the test.
t.Skipf("skipping test meant for local debugging only")
// NOTE: set this to the name of the channel database file you want
// to use for the migration test. This may be either a bbolt ".db" file
// or a SQLite ".sqlite" file. If you want to migrate from a
// bbolt channel.db file, set this to "channel.db".
const fileName = "channel.db"
// NOTE: if set, this test will prefer migrating from a Postgres-backed
// kvdb source instead of a local file. Leave empty to use fileName.
const postgresKVDSN = ""
const postgresKVPfx = "channeldb"
const logSequenceOrder = false
// Determine if we are using a SQLite file or a Bolt DB file.
isSqlite := strings.HasSuffix(fileName, ".sqlite")
// Set up logging for the test.
logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
UseLogger(logger)
// migrate runs the migration from the kvdb store to the SQL store
// and then performs a batched deep comparison of every migrated
// payment to ensure all data (including HTLC details) was preserved
// correctly.
migrate := func(t *testing.T, kvBackend kvdb.Backend) {
sqlStore := setupTestSQLDB(t)
// Run migration in a transaction.
err := sqlStore.db.ExecTx(
ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error {
migTx, ok := tx.(SQLMigrationQueries)
if !ok {
return fmt.Errorf("db does not " +
"implement SQLMigrationQueries")
}
return MigratePaymentsKVToSQL(
ctx, kvBackend, migTx, &SQLStoreConfig{
QueryCfg: sqlStore.cfg.QueryCfg,
},
)
}, sqldb.NoOpReset,
)
require.NoError(t, err)
t.Log("========================================")
t.Log(" Deep Validation")
t.Log("========================================")
// Deep compare all migrated payments in batches.
deepValidateAllPayments(
t, ctx, kvBackend, sqlStore,
)
_ = logSequenceOrder
}
connectPostgres := func(t *testing.T, dsn, prefix string) kvdb.Backend {
dsn = strings.TrimSpace(dsn)
if dsn == "" {
t.Fatalf("missing postgres kvdb dsn")
}
prefix = strings.TrimSpace(prefix)
if prefix == "" {
prefix = "channeldb"
}
const (
timeout = 10 * time.Second
maxConns = 5
)
sqlbase.Init(maxConns)
dbCfg := &postgres.Config{
Dsn: dsn,
Timeout: timeout,
MaxConnections: maxConns,
}
kvStore, err := kvdb.Open(
kvdb.PostgresBackendName, ctx, dbCfg, prefix,
)
require.NoError(t, err)
t.Cleanup(func() { _ = kvStore.Close() })
return kvStore
}
connectPostgresKV := func(t *testing.T) kvdb.Backend {
return connectPostgres(t, postgresKVDSN, postgresKVPfx)
}
connectBBolt := func(t *testing.T, dbPath string) kvdb.Backend {
cfg := &kvdb.BoltBackendConfig{
DBPath: dbPath,
DBFileName: fileName,
NoFreelistSync: true,
AutoCompact: false,
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
DBTimeout: kvdb.DefaultDBTimeout,
ReadOnly: true,
}
kvStore, err := kvdb.GetBoltBackend(cfg)
require.NoError(t, err)
t.Cleanup(func() { _ = kvStore.Close() })
return kvStore
}
connectSQLite := func(t *testing.T, dbPath string) kvdb.Backend {
const (
timeout = 10 * time.Second
maxConns = 5
)
sqlbase.Init(maxConns)
cfg := &sqlite.Config{
Timeout: timeout,
BusyTimeout: timeout,
MaxConnections: maxConns,
}
kvStore, err := kvdb.Open(
kvdb.SqliteBackendName, ctx, cfg,
dbPath, fileName,
// NOTE: we use the raw string here else we get an
// import cycle if we try to import lncfg.NSChannelDB.
"channeldb",
)
require.NoError(t, err)
t.Cleanup(func() { _ = kvStore.Close() })
return kvStore
}
tests := []struct {
name string
dbPath string
}{
{
name: "testdata",
dbPath: "testdata",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if postgresKVDSN != "" {
migrate(t, connectPostgresKV(t))
return
}
chanDBPath := path.Join(test.dbPath, fileName)
t.Logf("Connecting to channel DB at: %s", chanDBPath)
connectDB := connectBBolt
if isSqlite {
connectDB = connectSQLite
}
migrate(t, connectDB(t, test.dbPath))
})
}
}
// kvPaymentRef holds a KV payment and its hash for batched deep validation.
type kvPaymentRef struct {
hash lntypes.Hash
payment *MPPayment
}
// deepValidateAllPayments iterates all KV payments in batches and performs a
// deep comparison against their SQL counterparts. For each batch, it fetches
// the full SQL payment data (including HTLCs, routes, custom records) via
// batch queries and compares field-by-field with the KV data.
func deepValidateAllPayments(t *testing.T, ctx context.Context,
kvBackend kvdb.Backend, sqlStore *SQLStore) {
t.Helper()
batchSize := int(sqlStore.cfg.QueryCfg.MaxBatchSize)
// Get total payment count from SQL so we can show progress.
var totalPayments int64
err := sqlStore.db.ExecTx(
ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
var err error
totalPayments, err = db.CountPayments(ctx)
return err
}, sqldb.NoOpReset,
)
require.NoError(t, err)
var totalValidated int
// Iterate all KV payments, collecting them into batches. For each
// full batch, perform a batched deep comparison against SQL.
var batch []kvPaymentRef
err = kvdb.View(kvBackend, func(kvTx kvdb.RTx) error {
paymentsBucket := kvTx.ReadBucket(paymentsRootBucket)
if paymentsBucket == nil {
return nil
}
return paymentsBucket.ForEach(func(k, v []byte) error {
bucket := paymentsBucket.NestedReadBucket(k)
if bucket == nil {
return nil
}
payment, err := fetchPayment(bucket)
if err != nil {
return err
}
var hash lntypes.Hash
copy(hash[:], k)
batch = append(batch, kvPaymentRef{
hash: hash,
payment: payment,
})
if len(batch) >= batchSize {
deepCompareBatch(
t, ctx, sqlStore, batch,
)
totalValidated += len(batch)
t.Logf("Deep validated %d/%d payments",
totalValidated, totalPayments)
batch = batch[:0]
}
return nil
})
}, func() {
batch = nil
totalValidated = 0
})
require.NoError(t, err)
// Validate any remaining payments in the last batch.
if len(batch) > 0 {
deepCompareBatch(t, ctx, sqlStore, batch)
totalValidated += len(batch)
}
t.Logf("Deep validated %d/%d payments (complete)",
totalValidated, totalPayments)
}
// deepCompareBatch performs a batched deep comparison of KV payments against
// their SQL counterparts. It fetches SQL payment data in a single read
// transaction using batch queries.
func deepCompareBatch(t *testing.T, ctx context.Context,
sqlStore *SQLStore, batch []kvPaymentRef) {
t.Helper()
err := sqlStore.db.ExecTx(
ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
// Look up each payment by hash to get the SQL row
// with payment ID.
ids := make([]int64, 0, len(batch))
rowsByHash := make(
map[lntypes.Hash]sqlc.FetchPaymentsByIDsRow,
len(batch),
)
for _, ref := range batch {
row, err := fetchPaymentByHash(
ctx, db, ref.hash,
)
if err != nil {
return fmt.Errorf("fetch SQL payment "+
"%x: %w", ref.hash[:8], err)
}
ids = append(ids, row.Payment.ID)
}
// Batch-fetch full payment details.
rows, err := db.FetchPaymentsByIDs(ctx, ids)
if err != nil {
return fmt.Errorf("batch fetch payments: %w",
err)
}
for _, row := range rows {
var hash lntypes.Hash
copy(hash[:], row.PaymentIdentifier)
rowsByHash[hash] = row
}
batchData, err := batchLoadPaymentDetailsData(
ctx, sqlStore.cfg.QueryCfg, db, ids,
)
if err != nil {
return fmt.Errorf("batch load details: %w",
err)
}
// Deep compare each payment.
for _, ref := range batch {
row, ok := rowsByHash[ref.hash]
require.True(t, ok,
"SQL payment %x not in batch "+
"results", ref.hash[:8])
sqlPayment, err := buildPaymentFromBatchData(
row, batchData,
)
require.NoError(t, err,
"build SQL payment %x",
ref.hash[:8])
normalizePaymentData(ref.payment)
normalizePaymentData(sqlPayment)
require.Equal(
t, ref.payment, sqlPayment,
"payment mismatch %x",
ref.hash[:8],
)
}
return nil
}, sqldb.NoOpReset,
)
require.NoError(t, err)
}
@@ -0,0 +1,575 @@
package migration1
import (
"bytes"
"context"
"database/sql"
"fmt"
"reflect"
"sort"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/payments/db/migration1/record"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/pmezard/go-difflib/difflib"
)
// migratedPaymentRef is a reference to a migrated payment.
type migratedPaymentRef struct {
Hash lntypes.Hash
PaymentID int64
}
// validateMigratedPaymentBatch performs a structural validation pass by
// comparing key fields (hash, amount, fail reason, HTLC count) of KV payments
// with their SQL counterparts. If a structural mismatch is detected, a full
// deep comparison is performed to produce a detailed diff for debugging.
func validateMigratedPaymentBatch(ctx context.Context,
kvBackend kvdb.Backend, sqlDB SQLMigrationQueries,
cfg *SQLStoreConfig, batch []migratedPaymentRef) error {
if len(batch) == 0 {
return nil
}
if cfg == nil || cfg.QueryCfg == nil {
return fmt.Errorf("missing SQL store config for validation")
}
paymentIDs := make([]int64, 0, len(batch))
for _, item := range batch {
paymentIDs = append(paymentIDs, item.PaymentID)
}
rows, err := sqlDB.FetchPaymentsByIDsMig(ctx, paymentIDs)
if err != nil {
return fmt.Errorf("fetch SQL payments: %w", err)
}
if len(rows) != len(paymentIDs) {
return fmt.Errorf("SQL payment batch mismatch: got=%d want=%d",
len(rows), len(paymentIDs))
}
// Perform the structural check by comparing key fields from the KV
// store with the SQL store.
err = kvBackend.View(func(kvTx kvdb.RTx) error {
paymentsBucket := kvTx.ReadBucket(paymentsRootBucket)
if paymentsBucket == nil {
return fmt.Errorf("no payments bucket")
}
for _, row := range rows {
hash := row.PaymentIdentifier
var paymentHash lntypes.Hash
copy(paymentHash[:], hash)
paymentBucket := paymentsBucket.NestedReadBucket(hash)
if paymentBucket == nil {
return fmt.Errorf("missing payment bucket %x",
hash[:8])
}
kvPayment, err := fetchPayment(paymentBucket)
if err != nil {
return fmt.Errorf("fetch KV payment %x: %w",
hash[:8], err)
}
err = structuralCompare(kvPayment, row)
if err != nil {
// On structural mismatch, perform a deep
// comparison to produce a detailed diff.
deepErr := deepComparePayment(
ctx, cfg, sqlDB, row.ID,
paymentHash, kvPayment,
)
if deepErr != nil {
return deepErr
}
// If deep comparison passes but structural
// failed, report the structural error as it
// indicates an unexpected inconsistency.
return err
}
err = compareDuplicatePayments(
ctx, paymentBucket, sqlDB, row.ID,
paymentHash,
)
if err != nil {
return err
}
}
return nil
}, func() {})
if err != nil {
return err
}
return nil
}
// structuralCompare performs a fast structural comparison between a KV payment
// and a SQL payment row, checking key fields: payment identifier, amount,
// failure reason, and HTLC attempt count.
func structuralCompare(kvPayment *MPPayment,
sqlRow sqlc.FetchPaymentsByIDsMigRow) error {
// Compare payment identifier.
kvHash := kvPayment.Info.PaymentIdentifier[:]
if !bytes.Equal(kvHash, sqlRow.PaymentIdentifier) {
return fmt.Errorf("payment identifier mismatch: kv=%x sql=%x",
kvHash[:8], sqlRow.PaymentIdentifier[:8])
}
// Compare amount.
kvAmount := int64(kvPayment.Info.Value)
if kvAmount != sqlRow.AmountMsat {
return fmt.Errorf("amount mismatch for %x: kv=%d sql=%d",
sqlRow.PaymentIdentifier[:8], kvAmount,
sqlRow.AmountMsat)
}
// Compare failure reason.
var kvFailReason sql.NullInt32
if kvPayment.FailureReason != nil {
kvFailReason = sql.NullInt32{
Int32: int32(*kvPayment.FailureReason),
Valid: true,
}
}
if kvFailReason != sqlRow.FailReason {
return fmt.Errorf("fail reason mismatch for %x: kv=%v sql=%v",
sqlRow.PaymentIdentifier[:8], kvFailReason,
sqlRow.FailReason)
}
// Compare HTLC attempt count.
kvHTLCCount := int64(len(kvPayment.HTLCs))
if kvHTLCCount != sqlRow.HtlcAttemptCount {
return fmt.Errorf("HTLC count mismatch for %x: kv=%d sql=%d",
sqlRow.PaymentIdentifier[:8], kvHTLCCount,
sqlRow.HtlcAttemptCount)
}
return nil
}
// deepComparePayment performs a full deep comparison between a KV payment and
// its SQL counterpart, producing a detailed diff on mismatch.
func deepComparePayment(ctx context.Context, cfg *SQLStoreConfig,
sqlDB SQLQueries, paymentID int64, paymentHash lntypes.Hash,
kvPayment *MPPayment) error {
batchData, err := batchLoadPaymentDetailsData(
ctx, cfg.QueryCfg, sqlDB, []int64{paymentID},
)
if err != nil {
return fmt.Errorf("load payment data for deep compare %x: %w",
paymentHash[:8], err)
}
byIDRows, err := sqlDB.FetchPaymentsByIDs(ctx, []int64{paymentID})
if err != nil {
return fmt.Errorf("fetch payment by ID for deep compare "+
"%x: %w", paymentHash[:8], err)
}
if len(byIDRows) != 1 {
return fmt.Errorf("expected 1 payment for deep compare, "+
"got %d", len(byIDRows))
}
sqlPayment, err := buildPaymentFromBatchData(byIDRows[0], batchData)
if err != nil {
return fmt.Errorf("build SQL payment %x: %w",
paymentHash[:8], err)
}
normalizePaymentForCompare(kvPayment)
normalizePaymentForCompare(sqlPayment)
if !reflect.DeepEqual(kvPayment, sqlPayment) {
dumpCfg := spew.ConfigState{
DisablePointerAddresses: true,
DisableCapacities: true,
DisableMethods: true,
SortKeys: true,
}
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(
dumpCfg.Sdump(kvPayment),
),
B: difflib.SplitLines(
dumpCfg.Sdump(sqlPayment),
),
FromFile: "kv",
ToFile: "sql",
Context: 3,
}
diffText, _ := difflib.GetUnifiedDiffString(diff)
return fmt.Errorf("payment mismatch %x\n%s",
paymentHash[:8], diffText)
}
return nil
}
// normalizePaymentForCompare normalizes fields that are expected to differ
// between KV and SQL representations before deep comparison.
func normalizePaymentForCompare(payment *MPPayment) {
if payment == nil {
return
}
// SequenceNum will not be equal because the kv db can have already
// payments deleted during its lifetime.
payment.SequenceNum = 0
// We normalize timestamps before deep-comparing KV vs SQL objects.
//
// - **Microseconds**: SQL backends typically persist timestamps at
// microsecond precision (e.g. Postgres), while KV (Go `time.Time`)
// can contain nanoseconds. Truncating avoids false mismatches caused
// solely by differing storage precision.
//
// - **Local timezone**: when reading from SQL, timestamps are typically
// materialized in the local timezone by the SQL layer (and/or
// converters). Converting both sides to `time.Local` ensures the
// comparison is consistent across KV and SQL representations.
trunc := func(t time.Time) time.Time {
if t.IsZero() {
return t
}
return time.Unix(0, t.UnixNano()).
In(time.Local).
Truncate(time.Microsecond)
}
// Normalize PaymentCreationInfo fields.
if payment.Info != nil {
payment.Info.CreationTime = trunc(
payment.Info.CreationTime,
)
if len(payment.Info.PaymentRequest) == 0 {
payment.Info.PaymentRequest = []byte{}
}
if len(payment.Info.FirstHopCustomRecords) == 0 {
payment.Info.FirstHopCustomRecords = lnwire.
CustomRecords{}
}
}
// Normalize HTLCAttemptInfo so nil is converted to an empty slice.
if len(payment.HTLCs) == 0 {
payment.HTLCs = []HTLCAttempt{}
}
// Normalize HTLC attempt ordering; SQL/KV may return attempts
// in different orders.
sort.SliceStable(payment.HTLCs, func(i, j int) bool {
return payment.HTLCs[i].AttemptID < payment.HTLCs[j].AttemptID
})
// Normalize HTLCAttemptInfo fields.
for i := range payment.HTLCs {
htlc := &payment.HTLCs[i]
htlc.AttemptTime = trunc(htlc.AttemptTime)
if htlc.Settle != nil {
htlc.Settle.SettleTime = trunc(
htlc.Settle.SettleTime,
)
}
if htlc.Failure != nil {
htlc.Failure.FailTime = trunc(
htlc.Failure.FailTime,
)
}
// Clear cached fields not persisted in storage.
htlc.onionBlob = [1366]byte{}
htlc.circuit = nil
htlc.cachedSessionKey = nil
// For legacy payments, the HTLC Hash field may be nil in the
// bbolt backend. During migration, the SQL code uses the
// parent payment hash as fallback. To ensure the comparison
// between bbolt and SQL data succeeds, we apply the same
// fallback here.
//
// See also: patchLegacyPaymentHash in payment_lifecycle.go.
if htlc.Hash == nil && payment.Info != nil {
htlc.Hash = &payment.Info.PaymentIdentifier
}
if len(htlc.Route.FirstHopWireCustomRecords) == 0 {
htlc.Route.FirstHopWireCustomRecords =
lnwire.CustomRecords{}
}
for j := range htlc.Route.Hops {
if len(htlc.Route.Hops[j].CustomRecords) == 0 {
htlc.Route.Hops[j].CustomRecords =
record.CustomSet{}
}
// LegacyPayload was a hint used by the KV store to
// determine how to serialize and deserialize the hop
// payload (i.e. whether to use the legacy format or
// TLV). The SQL store does not serialize hop data at
// all — each field is stored natively in its own
// column — so this flag has no meaning there and is
// never persisted.
htlc.Route.Hops[j].LegacyPayload = false
}
}
}
// duplicateRecord is a record that represents a duplicate payment.
type duplicateRecord struct {
AmountMsat int64
CreatedAt time.Time
FailReason sql.NullInt32
SettlePreimage []byte
SettleTime sql.NullTime
}
// compareDuplicatePayments validates migrated duplicate rows against KV data.
func compareDuplicatePayments(ctx context.Context, paymentBucket kvdb.RBucket,
sqlDB SQLQueries, paymentID int64, hash lntypes.Hash) error {
// Fetch the duplicate payments from the KV store.
kvDuplicates, err := fetchDuplicateRecords(paymentBucket)
if err != nil {
return fmt.Errorf("fetch KV duplicates %x: %w",
hash[:8], err)
}
// Fetch the duplicate payments from the SQL store.
sqlDuplicates, err := sqlDB.FetchPaymentDuplicates(ctx, paymentID)
if err != nil {
return fmt.Errorf("fetch SQL duplicates %x: %w",
hash[:8], err)
}
if len(kvDuplicates) != len(sqlDuplicates) {
return fmt.Errorf("duplicate count mismatch %x: kv=%d "+
"sql=%d", hash[:8], len(kvDuplicates),
len(sqlDuplicates))
}
kvNormalized := normalizeDuplicateRecords(kvDuplicates)
sqlNormalized := normalizeDuplicateRecords(
dbDuplicatesToDuplicateRecords(sqlDuplicates),
)
sortDuplicates(kvNormalized)
sortDuplicates(sqlNormalized)
if !reflect.DeepEqual(kvNormalized, sqlNormalized) {
dumpCfg := spew.ConfigState{
DisablePointerAddresses: true,
DisableCapacities: true,
DisableMethods: true,
SortKeys: true,
}
diff := difflib.UnifiedDiff{
A: difflib.SplitLines(
dumpCfg.Sdump(kvNormalized),
),
B: difflib.SplitLines(
dumpCfg.Sdump(sqlNormalized),
),
FromFile: "kv",
ToFile: "sql",
Context: 3,
}
diffText, _ := difflib.GetUnifiedDiffString(diff)
return fmt.Errorf("duplicate mismatch %x\n%s",
hash[:8], diffText)
}
return nil
}
// fetchDuplicateRecords reads duplicate payment records from the KV bucket.
func fetchDuplicateRecords(paymentBucket kvdb.RBucket) ([]duplicateRecord,
error) {
dupBucket := paymentBucket.NestedReadBucket(duplicatePaymentsBucket)
if dupBucket == nil {
return nil, nil
}
var duplicates []duplicateRecord
err := dupBucket.ForEach(func(seqBytes, _ []byte) error {
if len(seqBytes) != 8 {
return nil
}
subBucket := dupBucket.NestedReadBucket(seqBytes)
if subBucket == nil {
return nil
}
creationData := subBucket.Get(duplicatePaymentCreationInfoKey)
if creationData == nil {
return fmt.Errorf("missing duplicate creation info")
}
creationInfo, err := deserializeDuplicatePaymentCreationInfo(
bytes.NewReader(creationData),
)
if err != nil {
return fmt.Errorf("deserialize duplicate creation "+
"info: %w", err)
}
settleData := subBucket.Get(duplicatePaymentSettleInfoKey)
failReasonData := subBucket.Get(duplicatePaymentFailInfoKey)
if settleData != nil && len(failReasonData) > 0 {
return fmt.Errorf("duplicate has both settle and " +
"fail info")
}
var (
failReason sql.NullInt32
settlePreimage []byte
settleTime sql.NullTime
)
switch {
case settleData != nil:
settlePreimage, settleTime, err =
parseDuplicateSettleData(settleData)
if err != nil {
return err
}
case len(failReasonData) > 0:
failReason = sql.NullInt32{
Int32: int32(failReasonData[0]),
Valid: true,
}
default:
// If the duplicate has no settle or fail info, it is
// considered failed. Every duplicate payment must have
// either a settle or fail info in the sql database. So
// we set the fail reason to error to mimic the behavior
// for the kv store.
failReason = sql.NullInt32{
Int32: int32(FailureReasonError),
Valid: true,
}
}
duplicates = append(duplicates, duplicateRecord{
AmountMsat: int64(creationInfo.Value),
CreatedAt: normalizeTimeForSQL(
creationInfo.CreationTime,
),
FailReason: failReason,
SettlePreimage: settlePreimage,
SettleTime: settleTime,
})
return nil
})
if err != nil {
return nil, err
}
return duplicates, nil
}
// dbDuplicatesToDuplicateRecords maps SQL duplicate rows into comparable
// duplicate records.
func dbDuplicatesToDuplicateRecords(
rows []sqlc.PaymentDuplicate) []duplicateRecord {
duplicates := make([]duplicateRecord, 0, len(rows))
for _, row := range rows {
duplicates = append(duplicates, duplicateRecord{
AmountMsat: row.AmountMsat,
CreatedAt: row.CreatedAt,
FailReason: row.FailReason,
SettlePreimage: row.SettlePreimage,
SettleTime: row.SettleTime,
})
}
return duplicates
}
// normalizeDuplicateRecords normalizes time precision and empty fields.
func normalizeDuplicateRecords(records []duplicateRecord) []duplicateRecord {
if len(records) == 0 {
return []duplicateRecord{}
}
trunc := func(t time.Time) time.Time {
if t.IsZero() {
return t
}
return t.In(time.Local).Truncate(time.Microsecond)
}
for i := range records {
records[i].CreatedAt = trunc(records[i].CreatedAt)
if records[i].SettleTime.Valid {
records[i].SettleTime.Time = trunc(
records[i].SettleTime.Time,
)
}
if len(records[i].SettlePreimage) == 0 {
records[i].SettlePreimage = []byte{}
}
}
return records
}
// sortDuplicates orders records deterministically for deep comparison.
func sortDuplicates(records []duplicateRecord) {
sort.SliceStable(records, func(i, j int) bool {
ai := records[i]
aj := records[j]
// Duplicates are "duplicates" because they share the same
// payment identifier. So ordering can be stable using
// timestamp + amount.
if !ai.CreatedAt.Equal(aj.CreatedAt) {
return ai.CreatedAt.Before(aj.CreatedAt)
}
return ai.AmountMsat < aj.AmountMsat
})
}
// validatePaymentCounts compares the number of migrated payments with the SQL
// payment count to catch missing rows.
func validatePaymentCounts(ctx context.Context, sqlDB SQLQueries,
expectedCount int64) error {
sqlCount, err := sqlDB.CountPayments(ctx)
if err != nil {
return fmt.Errorf("count SQL payments: %w", err)
}
if expectedCount != sqlCount {
return fmt.Errorf("payment count mismatch: kv=%d sql=%d",
expectedCount, sqlCount)
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package migration1
// StoreOptions holds parameters for the KVStore.
type StoreOptions struct {
// NoMigration allows to open the database in readonly mode
NoMigration bool
}
// DefaultOptions returns a StoreOptions populated with default values.
func DefaultOptions() *StoreOptions {
return &StoreOptions{
NoMigration: false,
}
}
// OptionModifier is a function signature for modifying the default
// StoreOptions.
type OptionModifier func(*StoreOptions)
// WithNoMigration allows the database to be opened in read only mode by
// disabling migrations.
func WithNoMigration(b bool) OptionModifier {
return func(o *StoreOptions) {
o.NoMigration = b
}
}
+689
View File
@@ -0,0 +1,689 @@
package migration1
import (
"errors"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
)
// FailureReason encodes the reason a payment ultimately failed.
type FailureReason byte
const (
// FailureReasonTimeout indicates that the payment did timeout before a
// successful payment attempt was made.
FailureReasonTimeout FailureReason = 0
// FailureReasonNoRoute indicates no successful route to the
// destination was found during path finding.
FailureReasonNoRoute FailureReason = 1
// FailureReasonError indicates that an unexpected error happened during
// payment.
FailureReasonError FailureReason = 2
// FailureReasonPaymentDetails indicates that either the hash is unknown
// or the final cltv delta or amount is incorrect.
FailureReasonPaymentDetails FailureReason = 3
// FailureReasonInsufficientBalance indicates that we didn't have enough
// balance to complete the payment.
FailureReasonInsufficientBalance FailureReason = 4
// FailureReasonCanceled indicates that the payment was canceled by the
// user.
FailureReasonCanceled FailureReason = 5
// TODO(joostjager): Add failure reasons for:
// LocalLiquidityInsufficient, RemoteCapacityInsufficient.
)
// Error returns a human-readable error string for the FailureReason.
func (r FailureReason) Error() string {
return r.String()
}
// String returns a human-readable FailureReason.
func (r FailureReason) String() string {
switch r {
case FailureReasonTimeout:
return "timeout"
case FailureReasonNoRoute:
return "no_route"
case FailureReasonError:
return "error"
case FailureReasonPaymentDetails:
return "incorrect_payment_details"
case FailureReasonInsufficientBalance:
return "insufficient_balance"
case FailureReasonCanceled:
return "canceled"
}
return "unknown"
}
// PaymentCreationInfo is the information necessary to have ready when
// initiating a payment, moving it into state InFlight.
type PaymentCreationInfo struct {
// PaymentIdentifier is the hash this payment is paying to in case of
// non-AMP payments, and the SetID for AMP payments.
PaymentIdentifier lntypes.Hash
// Value is the amount we are paying.
Value lnwire.MilliSatoshi
// CreationTime is the time when this payment was initiated.
CreationTime time.Time
// PaymentRequest is the full payment request, if any.
PaymentRequest []byte
// FirstHopCustomRecords are the TLV records that are to be sent to the
// first hop of this payment. These records will be transmitted via the
// wire message (UpdateAddHTLC) only and therefore do not affect the
// onion payload size.
FirstHopCustomRecords lnwire.CustomRecords
}
// String returns a human-readable description of the payment creation info.
func (p *PaymentCreationInfo) String() string {
return fmt.Sprintf("payment_id=%v, amount=%v, created_at=%v",
p.PaymentIdentifier, p.Value, p.CreationTime)
}
// HTLCAttemptInfo contains static information about a specific HTLC attempt
// for a payment. This information is used by the router to handle any errors
// coming back after an attempt is made, and to query the switch about the
// status of the attempt.
type HTLCAttemptInfo struct {
// AttemptID is the unique ID used for this attempt.
AttemptID uint64
// sessionKey is the raw bytes ephemeral key used for this attempt.
// These bytes are lazily read off disk to save ourselves the expensive
// EC operations used by btcec.PrivKeyFromBytes.
sessionKey [btcec.PrivKeyBytesLen]byte
// cachedSessionKey is our fully deserialized sesionKey. This value
// may be nil if the attempt has just been read from disk and its
// session key has not been used yet.
cachedSessionKey *btcec.PrivateKey
// Route is the route attempted to send the HTLC.
Route Route
// AttemptTime is the time at which this HTLC was attempted.
AttemptTime time.Time
// Hash is the hash used for this single HTLC attempt. For AMP payments
// this will differ across attempts, for non-AMP payments each attempt
// will use the same hash. This can be nil for older payment attempts,
// in which the payment's PaymentHash in the PaymentCreationInfo should
// be used.
Hash *lntypes.Hash
// onionBlob is the cached value for onion blob created from the sphinx
// construction.
onionBlob [lnwire.OnionPacketSize]byte
// circuit is the cached value for sphinx circuit.
circuit *sphinx.Circuit
}
// SessionKey returns the ephemeral key used for a htlc attempt. This function
// performs expensive ec-ops to obtain the session key if it is not cached.
func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey {
if h.cachedSessionKey == nil {
h.cachedSessionKey, _ = btcec.PrivKeyFromBytes(
h.sessionKey[:],
)
}
return h.cachedSessionKey
}
// HTLCAttempt contains information about a specific HTLC attempt for a given
// payment. It contains the HTLCAttemptInfo used to send the HTLC, as well
// as a timestamp and any known outcome of the attempt.
type HTLCAttempt struct {
HTLCAttemptInfo
// Settle is the preimage of a successful payment. This serves as a
// proof of payment. It will only be non-nil for settled payments.
//
// NOTE: Can be nil if payment is not settled.
Settle *HTLCSettleInfo
// Fail is a failure reason code indicating the reason the payment
// failed. It is only non-nil for failed payments.
//
// NOTE: Can be nil if payment is not failed.
Failure *HTLCFailInfo
}
// HTLCSettleInfo encapsulates the information that augments an HTLCAttempt in
// the event that the HTLC is successful.
type HTLCSettleInfo struct {
// Preimage is the preimage of a successful HTLC. This serves as a proof
// of payment.
Preimage lntypes.Preimage
// SettleTime is the time at which this HTLC was settled.
SettleTime time.Time
}
// HTLCFailReason is the reason an htlc failed.
type HTLCFailReason byte
const (
// HTLCFailUnknown is recorded for htlcs that failed with an unknown
// reason.
HTLCFailUnknown HTLCFailReason = 0
// HTLCFailUnreadable is recorded for htlcs that had a failure message
// that couldn't be decrypted.
HTLCFailUnreadable HTLCFailReason = 1
// HTLCFailInternal is recorded for htlcs that failed because of an
// internal error.
HTLCFailInternal HTLCFailReason = 2
// HTLCFailMessage is recorded for htlcs that failed with a network
// failure message.
HTLCFailMessage HTLCFailReason = 3
)
// HTLCFailInfo encapsulates the information that augments an HTLCAttempt in the
// event that the HTLC fails.
type HTLCFailInfo struct {
// FailTime is the time at which this HTLC was failed.
FailTime time.Time
// Message is the wire message that failed this HTLC. This field will be
// populated when the failure reason is HTLCFailMessage.
Message lnwire.FailureMessage
// Reason is the failure reason for this HTLC.
Reason HTLCFailReason
// The position in the path of the intermediate or final node that
// generated the failure message. Position zero is the sender node. This
// field will be populated when the failure reason is either
// HTLCFailMessage or HTLCFailUnknown.
FailureSourceIndex uint32
}
// MPPaymentState wraps a series of info needed for a given payment, which is
// used by both MPP and AMP. This is a memory representation of the payment's
// current state and is updated whenever the payment is read from disk.
type MPPaymentState struct {
// NumAttemptsInFlight specifies the number of HTLCs the payment is
// waiting results for.
NumAttemptsInFlight int
// RemainingAmt specifies how much more money to be sent.
RemainingAmt lnwire.MilliSatoshi
// FeesPaid specifies the total fees paid so far that can be used to
// calculate remaining fee budget.
FeesPaid lnwire.MilliSatoshi
// HasSettledHTLC is true if at least one of the payment's HTLCs is
// settled.
HasSettledHTLC bool
// PaymentFailed is true if the payment has been marked as failed with
// a reason.
PaymentFailed bool
}
// MPPayment is a wrapper around a payment's PaymentCreationInfo and
// HTLCAttempts. All payments will have the PaymentCreationInfo set, any
// HTLCs made in attempts to be completed will populated in the HTLCs slice.
// Each populated HTLCAttempt represents an attempted HTLC, each of which may
// have the associated Settle or Fail struct populated if the HTLC is no longer
// in-flight.
type MPPayment struct {
// SequenceNum is a unique identifier used to sort the payments in
// order of creation.
SequenceNum uint64
// Info holds all static information about this payment, and is
// populated when the payment is initiated.
Info *PaymentCreationInfo
// HTLCs holds the information about individual HTLCs that we send in
// order to make the payment.
HTLCs []HTLCAttempt
// FailureReason is the failure reason code indicating the reason the
// payment failed.
//
// NOTE: Will only be set once the daemon has given up on the payment
// altogether.
FailureReason *FailureReason
// Status is the current PaymentStatus of this payment.
Status PaymentStatus
// State is the current state of the payment that holds a number of key
// insights and is used to determine what to do on each payment loop
// iteration.
State *MPPaymentState
}
// Terminated returns a bool to specify whether the payment is in a terminal
// state.
func (m *MPPayment) Terminated() bool {
// If the payment is in terminal state, it cannot be updated.
return m.Status.updatable() != nil
}
// TerminalInfo returns any HTLC settle info recorded. If no settle info is
// recorded, any payment level failure will be returned. If neither a settle
// nor a failure is recorded, both return values will be nil.
func (m *MPPayment) TerminalInfo() (*HTLCAttempt, *FailureReason) {
for _, h := range m.HTLCs {
if h.Settle != nil {
return &h, nil
}
}
return nil, m.FailureReason
}
// SentAmt returns the sum of sent amount and fees for HTLCs that are either
// settled or still in flight.
func (m *MPPayment) SentAmt() (lnwire.MilliSatoshi, lnwire.MilliSatoshi) {
var sent, fees lnwire.MilliSatoshi
for _, h := range m.HTLCs {
if h.Failure != nil {
continue
}
// The attempt was not failed, meaning the amount was
// potentially sent to the receiver.
sent += h.Route.ReceiverAmt()
fees += h.Route.TotalFees()
}
return sent, fees
}
// InFlightHTLCs returns the HTLCs that are still in-flight, meaning they have
// not been settled or failed.
func (m *MPPayment) InFlightHTLCs() []HTLCAttempt {
var inflights []HTLCAttempt
for _, h := range m.HTLCs {
if h.Settle != nil || h.Failure != nil {
continue
}
inflights = append(inflights, h)
}
return inflights
}
// GetAttempt returns the specified htlc attempt on the payment.
func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) {
// TODO(yy): iteration can be slow, make it into a tree or use BS.
for _, htlc := range m.HTLCs {
htlc := htlc
if htlc.AttemptID == id {
return &htlc, nil
}
}
return nil, errors.New("htlc attempt not found on payment")
}
// Registrable returns an error to specify whether adding more HTLCs to the
// payment with its current status is allowed. A payment can accept new HTLC
// registrations when it's newly created, or none of its HTLCs is in a terminal
// state.
func (m *MPPayment) Registrable() error {
// If updating the payment is not allowed, we can't register new HTLCs.
// Otherwise, the status must be either `StatusInitiated` or
// `StatusInFlight`.
if err := m.Status.updatable(); err != nil {
return err
}
// Exit early if this is not inflight.
if m.Status != StatusInFlight {
return nil
}
// There are still inflight HTLCs and we need to check whether there
// are settled HTLCs or the payment is failed. If we already have
// settled HTLCs, we won't allow adding more HTLCs.
if m.State.HasSettledHTLC {
return ErrPaymentPendingSettled
}
// If the payment is already failed, we won't allow adding more HTLCs.
if m.State.PaymentFailed {
return ErrPaymentPendingFailed
}
// Otherwise we can add more HTLCs.
return nil
}
// setState creates and attaches a new MPPaymentState to the payment. It also
// updates the payment's status based on its current state.
func (m *MPPayment) setState() error {
// Fetch the total amount and fees that has already been sent in
// settled and still in-flight shards.
sentAmt, fees := m.SentAmt()
// Sanity check we haven't sent a value larger than the payment amount.
totalAmt := m.Info.Value
if sentAmt > totalAmt {
return fmt.Errorf("%w: sent=%v, total=%v",
ErrSentExceedsTotal, sentAmt, totalAmt)
}
// Get any terminal info for this payment.
settle, failure := m.TerminalInfo()
// Now determine the payment's status.
status, err := decidePaymentStatus(m.HTLCs, m.FailureReason)
if err != nil {
return err
}
// Update the payment state and status.
m.State = &MPPaymentState{
NumAttemptsInFlight: len(m.InFlightHTLCs()),
RemainingAmt: totalAmt - sentAmt,
FeesPaid: fees,
HasSettledHTLC: settle != nil,
PaymentFailed: failure != nil,
}
m.Status = status
return nil
}
// SetState calls the internal method setState. This is a temporary method
// to be used by the tests in routing. Once the tests are updated to use mocks,
// this method can be removed.
//
// TODO(yy): delete.
func (m *MPPayment) SetState() error {
return m.setState()
}
// NeedWaitAttempts decides whether we need to hold creating more HTLC attempts
// and wait for the results of the payment's inflight HTLCs. Return an error if
// the payment is in an unexpected state.
func (m *MPPayment) NeedWaitAttempts() (bool, error) {
// Check when the remainingAmt is not zero, which means we have more
// money to be sent.
if m.State.RemainingAmt != 0 {
switch m.Status {
// If the payment is newly created, no need to wait for HTLC
// results.
case StatusInitiated:
return false, nil
// If we have inflight HTLCs, we'll check if we have terminal
// states to decide if we need to wait.
case StatusInFlight:
// We still have money to send, and one of the HTLCs is
// settled. We'd stop sending money and wait for all
// inflight HTLC attempts to finish.
if m.State.HasSettledHTLC {
log.Warnf("payment=%v has remaining amount "+
"%v, yet at least one of its HTLCs is "+
"settled", m.Info.PaymentIdentifier,
m.State.RemainingAmt)
return true, nil
}
// The payment has a failure reason though we still
// have money to send, we'd stop sending money and wait
// for all inflight HTLC attempts to finish.
if m.State.PaymentFailed {
return true, nil
}
// Otherwise we don't need to wait for inflight HTLCs
// since we still have money to be sent.
return false, nil
// We need to send more money, yet the payment is already
// succeeded. Return an error in this case as the receiver is
// violating the protocol.
case StatusSucceeded:
return false, fmt.Errorf("%w: parts of the payment "+
"already succeeded but still have remaining "+
"amount %v", ErrPaymentInternal,
m.State.RemainingAmt)
// The payment is failed and we have no inflight HTLCs, no need
// to wait.
case StatusFailed:
return false, nil
// Unknown payment status.
default:
return false, fmt.Errorf("%w: %s",
ErrUnknownPaymentStatus, m.Status)
}
}
// Now we determine whether we need to wait when the remainingAmt is
// already zero.
switch m.Status {
// When the payment is newly created, yet the payment has no remaining
// amount, return an error.
case StatusInitiated:
return false, fmt.Errorf("%w: %v",
ErrPaymentInternal, m.Status)
// If the payment is inflight, we must wait.
//
// NOTE: an edge case is when all HTLCs are failed while the payment is
// not failed we'd still be in this inflight state. However, since the
// remainingAmt is zero here, it means we cannot be in that state as
// otherwise the remainingAmt would not be zero.
case StatusInFlight:
return true, nil
// If the payment is already succeeded, no need to wait.
case StatusSucceeded:
return false, nil
// If the payment is already failed, yet the remaining amount is zero,
// return an error as this indicates an error state. We will only each
// this status when there are no inflight HTLCs and the payment is
// marked as failed with a reason, which means the remainingAmt must
// not be zero because our sentAmt is zero.
case StatusFailed:
return false, fmt.Errorf("%w: %v",
ErrPaymentInternal, m.Status)
// Unknown payment status.
default:
return false, fmt.Errorf("%w: %s",
ErrUnknownPaymentStatus, m.Status)
}
}
// GetState returns the internal state of the payment.
func (m *MPPayment) GetState() *MPPaymentState {
return m.State
}
// GetStatus returns the current status of the payment.
func (m *MPPayment) GetStatus() PaymentStatus {
return m.Status
}
// GetHTLCs returns all the HTLCs for this payment.
func (m *MPPayment) GetHTLCs() []HTLCAttempt {
return m.HTLCs
}
// AllowMoreAttempts is used to decide whether we can safely attempt more HTLCs
// for a given payment state. Return an error if the payment is in an
// unexpected state.
func (m *MPPayment) AllowMoreAttempts() (bool, error) {
// Now check whether the remainingAmt is zero or not. If we don't have
// any remainingAmt, no more HTLCs should be made.
if m.State.RemainingAmt == 0 {
// If the payment is newly created, yet we don't have any
// remainingAmt, return an error.
if m.Status == StatusInitiated {
return false, fmt.Errorf("%w: initiated payment has "+
"zero remainingAmt",
ErrPaymentInternal)
}
// Otherwise, exit early since all other statuses with zero
// remainingAmt indicate no more HTLCs can be made.
return false, nil
}
// Otherwise, the remaining amount is not zero, we now decide whether
// to make more attempts based on the payment's current status.
//
// If at least one of the payment's attempts is settled, yet we haven't
// sent all the amount, it indicates something is wrong with the peer
// as the preimage is received. In this case, return an error state.
if m.Status == StatusSucceeded {
return false, fmt.Errorf("%w: payment already succeeded but "+
"still have remaining amount %v",
ErrPaymentInternal, m.State.RemainingAmt)
}
// Now check if we can register a new HTLC.
err := m.Registrable()
if err != nil {
log.Warnf("Payment(%v): cannot register HTLC attempt: %v, "+
"current status: %s", m.Info.PaymentIdentifier,
err, m.Status)
return false, nil
}
// Now we know we can register new HTLCs.
return true, nil
}
// verifyAttempt validates that a new HTLC attempt is compatible with the
// existing payment and its in-flight HTLCs. This function checks:
// 1. MPP (Multi-Path Payment) compatibility between attempts
// 2. Blinded payment consistency
// 3. Amount validation
// 4. Total payment amount limits
func verifyAttempt(payment *MPPayment, attempt *HTLCAttemptInfo) error {
// If the final hop has encrypted data, then we know this is a
// blinded payment. In blinded payments, MPP records are not set
// for split payments and the recipient is responsible for using
// a consistent PathID across the various encrypted data
// payloads that we received from them for this payment. All we
// need to check is that the total amount field for each HTLC
// in the split payment is correct.
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
// For blinded payments, the last hop must set the total amount.
if isBlinded {
if attempt.Route.FinalHop().TotalAmtMsat == 0 {
return ErrBlindedPaymentMissingTotalAmount
}
}
// Make sure any existing shards match the new one with regards
// to MPP options.
mpp := attempt.Route.FinalHop().MPP
// MPP records should not be set for attempts to blinded paths.
if isBlinded && mpp != nil {
return ErrMPPRecordInBlindedPayment
}
for _, h := range payment.InFlightHTLCs() {
hMpp := h.Route.FinalHop().MPP
hBlinded := len(h.Route.FinalHop().EncryptedData) != 0
// If this is a blinded payment, then no existing HTLCs
// should have MPP records.
if isBlinded && hMpp != nil {
return ErrMPPRecordInBlindedPayment
}
// If the payment is blinded (previous attempts used blinded
// paths) and the attempt is not, or vice versa, return an
// error.
if isBlinded != hBlinded {
return ErrMixedBlindedAndNonBlindedPayments
}
// If this is a blinded payment, then we just need to
// check that the TotalAmtMsat field for this shard
// is equal to that of any other shard in the same
// payment.
if isBlinded {
if attempt.Route.FinalHop().TotalAmtMsat !=
h.Route.FinalHop().TotalAmtMsat {
return ErrBlindedPaymentTotalAmountMismatch
}
continue
}
switch {
// We tried to register a non-MPP attempt for a MPP
// payment.
case mpp == nil && hMpp != nil:
return ErrMPPayment
// We tried to register a MPP shard for a non-MPP
// payment.
case mpp != nil && hMpp == nil:
return ErrNonMPPayment
// Non-MPP payment, nothing more to validate.
case mpp == nil:
continue
}
// Check that MPP options match.
if mpp.PaymentAddr() != hMpp.PaymentAddr() {
return ErrMPPPaymentAddrMismatch
}
if mpp.TotalMsat() != hMpp.TotalMsat() {
return ErrMPPTotalAmountMismatch
}
}
// If this is a non-MPP attempt, it must match the total amount
// exactly. Note that a blinded payment is considered an MPP
// attempt.
amt := attempt.Route.ReceiverAmt()
if !isBlinded && mpp == nil && amt != payment.Info.Value {
return ErrValueMismatch
}
// Ensure we aren't sending more than the total payment amount.
sentAmt, _ := payment.SentAmt()
if sentAmt+amt > payment.Info.Value {
return fmt.Errorf("%w: attempted=%v, payment amount=%v",
ErrValueExceedsAmt, sentAmt+amt, payment.Info.Value)
}
return nil
}
+257
View File
@@ -0,0 +1,257 @@
package migration1
import (
"fmt"
)
// PaymentStatus represent current status of payment.
type PaymentStatus byte
const (
// NOTE: PaymentStatus = 0 was previously used for status unknown and
// is now deprecated.
// StatusInitiated is the status where a payment has just been
// initiated.
StatusInitiated PaymentStatus = 1
// StatusInFlight is the status where a payment has been initiated, but
// a response has not been received.
StatusInFlight PaymentStatus = 2
// StatusSucceeded is the status where a payment has been initiated and
// the payment was completed successfully.
StatusSucceeded PaymentStatus = 3
// StatusFailed is the status where a payment has been initiated and a
// failure result has come back.
StatusFailed PaymentStatus = 4
)
// errPaymentStatusUnknown is returned when a payment has an unknown status.
var errPaymentStatusUnknown = fmt.Errorf("unknown payment status")
// String returns readable representation of payment status.
func (ps PaymentStatus) String() string {
switch ps {
case StatusInitiated:
return "Initiated"
case StatusInFlight:
return "In Flight"
case StatusSucceeded:
return "Succeeded"
case StatusFailed:
return "Failed"
default:
return "Unknown"
}
}
// initializable returns an error to specify whether initiating the payment
// with its current status is allowed. A payment can only be initialized if it
// hasn't been created yet or already failed.
func (ps PaymentStatus) initializable() error {
switch ps {
// The payment has been created already. We will disallow creating it
// again in case other goroutines have already been creating HTLCs for
// it.
case StatusInitiated:
return ErrPaymentExists
// We already have an InFlight payment on the network. We will disallow
// any new payments.
case StatusInFlight:
return ErrPaymentInFlight
// The payment has been attempted and is succeeded so we won't allow
// creating it again.
case StatusSucceeded:
return ErrAlreadyPaid
// We allow retrying failed payments.
case StatusFailed:
return nil
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// removable returns an error to specify whether deleting the payment with its
// current status is allowed. A payment cannot be safely deleted if it has
// inflight HTLCs.
func (ps PaymentStatus) removable() error {
switch ps {
// The payment has been created but has no HTLCs and can be removed.
case StatusInitiated:
return nil
// There are still inflight HTLCs and the payment needs to wait for the
// final outcomes.
case StatusInFlight:
return ErrPaymentInFlight
// The payment has been attempted and is succeeded and is allowed to be
// removed.
case StatusSucceeded:
return nil
// Failed payments are allowed to be removed.
case StatusFailed:
return nil
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// updatable returns an error to specify whether the payment's HTLCs can be
// updated. A payment can update its HTLCs when it has inflight HTLCs.
func (ps PaymentStatus) updatable() error {
switch ps {
// Newly created payments can be updated.
case StatusInitiated:
return nil
// Inflight payments can be updated.
case StatusInFlight:
return nil
// If the payment has a terminal condition, we won't allow any updates.
case StatusSucceeded:
return ErrPaymentAlreadySucceeded
case StatusFailed:
return ErrPaymentAlreadyFailed
default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus,
ps)
}
}
// decidePaymentStatus uses the payment's DB state to determine a memory status
// that's used by the payment router to decide following actions.
// Together, we use four variables to determine the payment's status,
// - inflight: whether there are any pending HTLCs.
// - settled: whether any of the HTLCs has been settled.
// - htlc failed: whether any of the HTLCs has been failed.
// - payment failed: whether the payment has been marked as failed.
//
// Based on the above variables, we derive the status using the following
// table,
// | inflight | settled | htlc failed | payment failed | status |
// |:--------:|:-------:|:-----------:|:--------------:|:--------------------:|
// | true | true | true | true | StatusInFlight |
// | true | true | true | false | StatusInFlight |
// | true | true | false | true | StatusInFlight |
// | true | true | false | false | StatusInFlight |
// | true | false | true | true | StatusInFlight |
// | true | false | true | false | StatusInFlight |
// | true | false | false | true | StatusInFlight |
// | true | false | false | false | StatusInFlight |
// | false | true | true | true | StatusSucceeded |
// | false | true | true | false | StatusSucceeded |
// | false | true | false | true | StatusSucceeded |
// | false | true | false | false | StatusSucceeded |
// | false | false | true | true | StatusFailed |
// | false | false | true | false | StatusInFlight |
// | false | false | false | true | StatusFailed |
// | false | false | false | false | StatusInitiated |
//
// When `inflight`, `settled`, `htlc failed`, and `payment failed` are false,
// this indicates the payment is newly created and hasn't made any HTLCs yet.
// When `inflight` and `settled` are false, `htlc failed` is true yet `payment
// failed` is false, this indicates all the payment's HTLCs have occurred a
// temporarily failure and the payment is still in-flight.
func decidePaymentStatus(htlcs []HTLCAttempt,
reason *FailureReason) (PaymentStatus, error) {
var (
inflight bool
htlcSettled bool
htlcFailed bool
paymentFailed bool
)
// If we have a failure reason, the payment is failed.
if reason != nil {
paymentFailed = true
}
// Go through all HTLCs for this payment, check whether we have any
// settled HTLC, and any still in-flight.
for _, h := range htlcs {
if h.Failure != nil {
htlcFailed = true
continue
}
if h.Settle != nil {
htlcSettled = true
continue
}
// If any of the HTLCs are not failed nor settled, we
// still have inflight HTLCs.
inflight = true
}
// Use the DB state to determine the status of the payment.
switch {
// If we have inflight HTLCs, no matter we have settled or failed
// HTLCs, or the payment failed, we still consider it inflight so we
// inform upper systems to wait for the results.
case inflight:
return StatusInFlight, nil
// If we have no in-flight HTLCs, and at least one of the HTLCs is
// settled, the payment succeeded.
//
// NOTE: when reaching this case, paymentFailed could be true, which
// means we have a conflicting state for this payment. We choose to
// mark the payment as succeeded because it's the receiver's
// responsibility to only settle the payment iff all HTLCs are
// received.
case htlcSettled:
return StatusSucceeded, nil
// If we have no in-flight HTLCs, and the payment failure is set, the
// payment is considered failed.
//
// NOTE: when reaching this case, settled must be false.
case paymentFailed:
return StatusFailed, nil
// If we have no in-flight HTLCs, yet the payment is NOT failed, it
// means all the HTLCs are failed. In this case we can attempt more
// HTLCs.
//
// NOTE: when reaching this case, both settled and paymentFailed must
// be false.
case htlcFailed:
return StatusInFlight, nil
// If none of the HTLCs is either settled or failed, and we have no
// inflight HTLCs, this means the payment has no HTLCs created yet.
//
// NOTE: when reaching this case, both settled and paymentFailed must
// be false.
case !htlcFailed:
return StatusInitiated, nil
// Otherwise an impossible state is reached.
//
// NOTE: we should never end up here.
default:
log.Error("Impossible payment state reached")
return 0, fmt.Errorf("%w: payment is corrupted",
errPaymentStatusUnknown)
}
}
+75
View File
@@ -0,0 +1,75 @@
package migration1
const (
// DefaultMaxPayments is the default maximum number of payments returned
// in the payments query pagination.
DefaultMaxPayments = 100
)
// Query represents a query to the payments database starting or ending
// at a certain offset index. The number of retrieved records can be limited.
type Query struct {
// IndexOffset determines the starting point of the payments query and
// is always exclusive. In normal order, the query starts at the next
// higher (available) index compared to IndexOffset. In reversed order,
// the query ends at the next lower (available) index compared to the
// IndexOffset. In the case of a zero index_offset, the query will start
// with the oldest payment when paginating forwards, or will end with
// the most recent payment when paginating backwards.
IndexOffset uint64
// MaxPayments is the maximal number of payments returned in the
// payments query.
MaxPayments uint64
// Reversed gives a meaning to the IndexOffset. If reversed is set to
// true, the query will fetch payments with indices lower than the
// IndexOffset, otherwise, it will return payments with indices greater
// than the IndexOffset.
Reversed bool
// If IncludeIncomplete is true, then return payments that have not yet
// fully completed. This means that pending payments, as well as failed
// payments will show up if this field is set to true.
IncludeIncomplete bool
// CountTotal indicates that all payments currently present in the
// payment index (complete and incomplete) should be counted.
CountTotal bool
// CreationDateStart, expressed in Unix seconds, if set, filters out
// all payments with a creation date greater than or equal to it.
CreationDateStart int64
// CreationDateEnd, expressed in Unix seconds, if set, filters out all
// payments with a creation date less than or equal to it.
CreationDateEnd int64
}
// Response contains the result of a query to the payments database.
// It includes the set of payments that match the query and integers which
// represent the index of the first and last item returned in the series of
// payments. These integers allow callers to resume their query in the event
// that the query's response exceeds the max number of returnable events.
type Response struct {
// Payments is the set of payments returned from the database for the
// Query.
Payments []*MPPayment
// FirstIndexOffset is the index of the first element in the set of
// returned MPPayments. Callers can use this to resume their query
// in the event that the slice has too many events to fit into a single
// response. The offset can be used to continue reverse pagination.
FirstIndexOffset uint64
// LastIndexOffset is the index of the last element in the set of
// returned MPPayments. Callers can use this to resume their query
// in the event that the slice has too many events to fit into a single
// response. The offset can be used to continue forward pagination.
LastIndexOffset uint64
// TotalCount represents the total number of payments that are currently
// stored in the payment database. This will only be set if the
// CountTotal field in the query was set to true.
TotalCount uint64
}
+121
View File
@@ -0,0 +1,121 @@
package record
import (
"fmt"
"io"
"github.com/lightningnetwork/lnd/tlv"
)
// AMPOnionType is the type used in the onion to reference the AMP fields:
// root_share, set_id, and child_index.
const AMPOnionType tlv.Type = 14
// AMP is a record that encodes the fields necessary for atomic multi-path
// payments.
type AMP struct {
rootShare [32]byte
setID [32]byte
childIndex uint32
}
// MaxAmpPayLoadSize is an AMP Record which when serialized to a tlv record uses
// the maximum payload size. The `childIndex` is created randomly and is a
// 4 byte `varint` type so we make sure we use an index which will be encoded in
// 4 bytes.
var MaxAmpPayLoadSize = AMP{
rootShare: [32]byte{},
setID: [32]byte{},
childIndex: 0x80000000,
}
// NewAMP generate a new AMP record with the given root_share, set_id, and
// child_index.
func NewAMP(rootShare, setID [32]byte, childIndex uint32) *AMP {
return &AMP{
rootShare: rootShare,
setID: setID,
childIndex: childIndex,
}
}
// RootShare returns the root share contained in the AMP record.
func (a *AMP) RootShare() [32]byte {
return a.rootShare
}
// SetID returns the set id contained in the AMP record.
func (a *AMP) SetID() [32]byte {
return a.setID
}
// ChildIndex returns the child index contained in the AMP record.
func (a *AMP) ChildIndex() uint32 {
return a.childIndex
}
// AMPEncoder writes the AMP record to the provided io.Writer.
func AMPEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*AMP); ok {
if err := tlv.EBytes32(w, &v.rootShare, buf); err != nil {
return err
}
if err := tlv.EBytes32(w, &v.setID, buf); err != nil {
return err
}
return tlv.ETUint32T(w, v.childIndex, buf)
}
return tlv.NewTypeForEncodingErr(val, "AMP")
}
const (
// minAMPLength is the minimum length of a serialized AMP TLV record,
// which occurs when the truncated encoding of child_index takes 0
// bytes, leaving only the root_share and set_id.
minAMPLength = 64
// maxAMPLength is the maximum length of a serialized AMP TLV record,
// which occurs when the truncated encoding of a child_index takes 2
// bytes.
maxAMPLength = 68
)
// AMPDecoder reads the AMP record from the provided io.Reader.
func AMPDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if v, ok := val.(*AMP); ok && minAMPLength <= l && l <= maxAMPLength {
if err := tlv.DBytes32(r, &v.rootShare, buf, 32); err != nil {
return err
}
if err := tlv.DBytes32(r, &v.setID, buf, 32); err != nil {
return err
}
return tlv.DTUint32(r, &v.childIndex, buf, l-minAMPLength)
}
return tlv.NewTypeForDecodingErr(val, "AMP", l, maxAMPLength)
}
// Record returns a tlv.Record that can be used to encode or decode this record.
func (a *AMP) Record() tlv.Record {
return tlv.MakeDynamicRecord(
AMPOnionType, a, a.PayloadSize, AMPEncoder, AMPDecoder,
)
}
// PayloadSize returns the size this record takes up in encoded form.
func (a *AMP) PayloadSize() uint64 {
return 32 + 32 + tlv.SizeTUint32(a.childIndex)
}
// String returns a human-readable description of the amp payload fields.
func (a *AMP) String() string {
if a == nil {
return "<nil>"
}
return fmt.Sprintf("root_share=%x set_id=%x child_index=%d",
a.rootShare, a.setID, a.childIndex)
}
@@ -0,0 +1,440 @@
package record
import (
"bytes"
"encoding/binary"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// AverageDummyHopPayloadSize is the size of a standard blinded path dummy hop
// payload. In most cases, this is larger than the other payload types and so
// to make sure that a sender cannot use this fact to know if a dummy hop is
// present or not, we'll make sure to always pad all payloads to at least this
// size.
const AverageDummyHopPayloadSize = 51
// BlindedRouteData contains the information that is included in a blinded
// route encrypted data blob that is created by the recipient to provide
// forwarding information.
type BlindedRouteData struct {
// Padding is an optional set of bytes that a recipient can use to pad
// the data so that the encrypted recipient data blobs are all the same
// length.
Padding tlv.OptionalRecordT[tlv.TlvType1, []byte]
// ShortChannelID is the channel ID of the next hop.
ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID]
// NextNodeID is the node ID of the next node on the path. In the
// context of blinded path payments, this is used to indicate the
// presence of dummy hops that need to be peeled from the onion.
NextNodeID tlv.OptionalRecordT[tlv.TlvType4, *btcec.PublicKey]
// PathID is a secret set of bytes that the blinded path creator will
// set so that they can check the value on decryption to ensure that the
// path they created was used for the intended purpose.
PathID tlv.OptionalRecordT[tlv.TlvType6, []byte]
// NextBlindingOverride is a blinding point that should be switched
// in for the next hop. This is used to combine two blinded paths into
// one (which primarily is used in onion messaging, but in theory
// could be used for payments as well).
NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey]
// RelayInfo provides the relay parameters for the hop.
RelayInfo tlv.OptionalRecordT[tlv.TlvType10, PaymentRelayInfo]
// Constraints provides the payment relay constraints for the hop.
Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints]
// Features is the set of features the payment requires.
Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector]
}
// NewNonFinalBlindedRouteData creates the data that's provided for hops within
// a blinded route.
func NewNonFinalBlindedRouteData(chanID lnwire.ShortChannelID,
blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo,
constraints *PaymentConstraints,
features *lnwire.FeatureVector) *BlindedRouteData {
info := &BlindedRouteData{
ShortChannelID: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType2](chanID),
),
RelayInfo: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType10](relayInfo),
),
}
if blindingOverride != nil {
info.NextBlindingOverride = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType8](blindingOverride))
}
if constraints != nil {
info.Constraints = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](*constraints))
}
if features != nil {
info.Features = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType14](*features),
)
}
return info
}
// NewFinalHopBlindedRouteData creates the data that's provided for the final
// hop in a blinded route.
func NewFinalHopBlindedRouteData(constraints *PaymentConstraints,
pathID []byte) *BlindedRouteData {
var data BlindedRouteData
if pathID != nil {
data.PathID = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType6](pathID),
)
}
if constraints != nil {
data.Constraints = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](*constraints))
}
return &data
}
// NewDummyHopRouteData creates the data that's provided for any hop preceding
// a dummy hop. The presence of such a payload indicates to the reader that
// they are the intended recipient and should peel the remainder of the onion.
func NewDummyHopRouteData(ourPubKey *btcec.PublicKey,
relayInfo PaymentRelayInfo,
constraints PaymentConstraints) *BlindedRouteData {
return &BlindedRouteData{
NextNodeID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType4](ourPubKey),
),
RelayInfo: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType10](relayInfo),
),
Constraints: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](constraints),
),
}
}
// DecodeBlindedRouteData decodes the data provided within a blinded route.
func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
var (
d BlindedRouteData
padding = d.Padding.Zero()
scid = d.ShortChannelID.Zero()
nextNodeID = d.NextNodeID.Zero()
pathID = d.PathID.Zero()
blindingOverride = d.NextBlindingOverride.Zero()
relayInfo = d.RelayInfo.Zero()
constraints = d.Constraints.Zero()
features = d.Features.Zero()
)
var tlvRecords lnwire.ExtraOpaqueData
if err := lnwire.ReadElements(r, &tlvRecords); err != nil {
return nil, err
}
typeMap, err := tlvRecords.ExtractRecords(
&padding, &scid, &nextNodeID, &pathID, &blindingOverride,
&relayInfo, &constraints, &features,
)
if err != nil {
return nil, err
}
val, ok := typeMap[d.Padding.TlvType()]
if ok && val == nil {
d.Padding = tlv.SomeRecordT(padding)
}
if val, ok := typeMap[d.ShortChannelID.TlvType()]; ok && val == nil {
d.ShortChannelID = tlv.SomeRecordT(scid)
}
if val, ok := typeMap[d.NextNodeID.TlvType()]; ok && val == nil {
d.NextNodeID = tlv.SomeRecordT(nextNodeID)
}
if val, ok := typeMap[d.PathID.TlvType()]; ok && val == nil {
d.PathID = tlv.SomeRecordT(pathID)
}
val, ok = typeMap[d.NextBlindingOverride.TlvType()]
if ok && val == nil {
d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride)
}
if val, ok := typeMap[d.RelayInfo.TlvType()]; ok && val == nil {
d.RelayInfo = tlv.SomeRecordT(relayInfo)
}
if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil {
d.Constraints = tlv.SomeRecordT(constraints)
}
if val, ok := typeMap[d.Features.TlvType()]; ok && val == nil {
d.Features = tlv.SomeRecordT(features)
}
return &d, nil
}
// EncodeBlindedRouteData encodes the blinded route data provided.
func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
var (
e lnwire.ExtraOpaqueData
recordProducers = make([]tlv.RecordProducer, 0, 5)
)
data.Padding.WhenSome(func(p tlv.RecordT[tlv.TlvType1, []byte]) {
recordProducers = append(recordProducers, &p)
})
data.ShortChannelID.WhenSome(func(scid tlv.RecordT[tlv.TlvType2,
lnwire.ShortChannelID]) {
recordProducers = append(recordProducers, &scid)
})
data.NextNodeID.WhenSome(func(f tlv.RecordT[tlv.TlvType4,
*btcec.PublicKey]) {
recordProducers = append(recordProducers, &f)
})
data.PathID.WhenSome(func(pathID tlv.RecordT[tlv.TlvType6, []byte]) {
recordProducers = append(recordProducers, &pathID)
})
data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8,
*btcec.PublicKey]) {
recordProducers = append(recordProducers, &pk)
})
data.RelayInfo.WhenSome(func(r tlv.RecordT[tlv.TlvType10,
PaymentRelayInfo]) {
recordProducers = append(recordProducers, &r)
})
data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12,
PaymentConstraints]) {
recordProducers = append(recordProducers, &cs)
})
data.Features.WhenSome(func(f tlv.RecordT[tlv.TlvType14,
lnwire.FeatureVector]) {
recordProducers = append(recordProducers, &f)
})
if err := e.PackRecords(recordProducers...); err != nil {
return nil, err
}
return e[:], nil
}
// PadBy adds "n" padding bytes to the BlindedRouteData using the Padding field.
// Callers should be aware that the total payload size will change by more than
// "n" since the "n" bytes will be prefixed by BigSize type and length fields.
// Callers may need to call PadBy iteratively until each encrypted data packet
// is the same size and so each call will overwrite the Padding record.
// Note that calling PadBy with an n value of 0 will still result in a zero
// length TLV entry being added.
func (b *BlindedRouteData) PadBy(n int) {
b.Padding = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType1](make([]byte, n)),
)
}
// PaymentRelayInfo describes the relay policy for a blinded path.
type PaymentRelayInfo struct {
// CltvExpiryDelta is the expiry delta for the payment.
CltvExpiryDelta uint16
// FeeRate is the fee rate that will be charged per millionth of a
// satoshi.
FeeRate uint32
// BaseFee is the per-htlc fee charged in milli-satoshis.
BaseFee lnwire.MilliSatoshi
}
// Record creates a tlv.Record that encodes the payment relay (type 10) type for
// an encrypted blob payload.
func (i *PaymentRelayInfo) Record() tlv.Record {
return tlv.MakeDynamicRecord(
10, &i, func() uint64 {
// uint16 + uint32 + tuint32
return 2 + 4 + tlv.SizeTUint32(uint32(i.BaseFee))
}, encodePaymentRelay, decodePaymentRelay,
)
}
func encodePaymentRelay(w io.Writer, val interface{}, buf *[8]byte) error {
if t, ok := val.(**PaymentRelayInfo); ok {
relayInfo := *t
// Just write our first 6 bytes directly.
binary.BigEndian.PutUint16(buf[:2], relayInfo.CltvExpiryDelta)
binary.BigEndian.PutUint32(buf[2:6], relayInfo.FeeRate)
if _, err := w.Write(buf[0:6]); err != nil {
return err
}
baseFee := uint32(relayInfo.BaseFee)
// We can safely reuse buf here because we overwrite its
// contents.
return tlv.ETUint32(w, &baseFee, buf)
}
return tlv.NewTypeForEncodingErr(val, "**hop.PaymentRelayInfo")
}
func decodePaymentRelay(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if t, ok := val.(**PaymentRelayInfo); ok && l <= 10 {
scratch := make([]byte, l)
n, err := io.ReadFull(r, scratch)
if err != nil {
return err
}
// We expect at least 6 bytes, because we have 2 bytes for
// cltv delta and 4 bytes for fee rate.
if n < 6 {
return tlv.NewTypeForDecodingErr(val,
"*hop.paymentRelayInfo", uint64(n), 6)
}
relayInfo := *t
relayInfo.CltvExpiryDelta = binary.BigEndian.Uint16(
scratch[0:2],
)
relayInfo.FeeRate = binary.BigEndian.Uint32(scratch[2:6])
// To be able to re-use the DTUint32 function we create a
// buffer with just the bytes holding the variable length u32.
// If the base fee is zero, this will be an empty buffer, which
// is okay.
b := bytes.NewBuffer(scratch[6:])
var baseFee uint32
err = tlv.DTUint32(b, &baseFee, buf, l-6)
if err != nil {
return err
}
relayInfo.BaseFee = lnwire.MilliSatoshi(baseFee)
return nil
}
return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", l, 10)
}
// PaymentConstraints is a set of restrictions on a payment.
type PaymentConstraints struct {
// MaxCltvExpiry is the maximum expiry height for the payment.
MaxCltvExpiry uint32
// HtlcMinimumMsat is the minimum htlc size for the payment.
HtlcMinimumMsat lnwire.MilliSatoshi
}
func (p *PaymentConstraints) Record() tlv.Record {
return tlv.MakeDynamicRecord(
12, &p, func() uint64 {
// uint32 + tuint64.
return 4 + tlv.SizeTUint64(uint64(
p.HtlcMinimumMsat,
))
},
encodePaymentConstraints, decodePaymentConstraints,
)
}
func encodePaymentConstraints(w io.Writer, val interface{},
buf *[8]byte) error {
if c, ok := val.(**PaymentConstraints); ok {
constraints := *c
binary.BigEndian.PutUint32(buf[:4], constraints.MaxCltvExpiry)
if _, err := w.Write(buf[:4]); err != nil {
return err
}
// We can safely re-use buf here because we overwrite its
// contents.
htlcMsat := uint64(constraints.HtlcMinimumMsat)
return tlv.ETUint64(w, &htlcMsat, buf)
}
return tlv.NewTypeForEncodingErr(val, "**PaymentConstraints")
}
func decodePaymentConstraints(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if c, ok := val.(**PaymentConstraints); ok && l <= 12 {
scratch := make([]byte, l)
n, err := io.ReadFull(r, scratch)
if err != nil {
return err
}
// We expect at least 4 bytes for our uint32.
if n < 4 {
return tlv.NewTypeForDecodingErr(val,
"*paymentConstraints", uint64(n), 4)
}
payConstraints := *c
payConstraints.MaxCltvExpiry = binary.BigEndian.Uint32(
scratch[:4],
)
// This could be empty if our minimum is zero, that's okay.
var (
b = bytes.NewBuffer(scratch[4:])
minHtlc uint64
)
err = tlv.DTUint64(b, &minHtlc, buf, l-4)
if err != nil {
return err
}
payConstraints.HtlcMinimumMsat = lnwire.MilliSatoshi(minHtlc)
return nil
}
return tlv.NewTypeForDecodingErr(val, "**PaymentConstraints", l, l)
}
@@ -0,0 +1,31 @@
package record
import (
"fmt"
)
const (
// CustomTypeStart is the start of the custom tlv type range as defined
// in BOLT 01.
CustomTypeStart = 65536
)
// CustomSet stores a set of custom key/value pairs.
type CustomSet map[uint64][]byte
// Validate checks that all custom records are in the custom type range.
func (c CustomSet) Validate() error {
for key := range c {
if key < CustomTypeStart {
return fmt.Errorf("no custom records with types "+
"below %v allowed", CustomTypeStart)
}
}
return nil
}
// IsKeysend checks if the custom records contain the key send type.
func (c CustomSet) IsKeysend() bool {
return c[KeySendType] != nil
}
@@ -0,0 +1,6 @@
package record
const (
// KeySendType is the custom record identifier for keysend preimages.
KeySendType uint64 = 5482373484
)
+99
View File
@@ -0,0 +1,99 @@
package record
import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// AmtOnionType is the type used in the onion to reference the amount to
// send to the next hop.
AmtOnionType tlv.Type = 2
// LockTimeTLV is the type used in the onion to reference the CLTV
// value that should be used for the next hop's HTLC.
LockTimeOnionType tlv.Type = 4
// NextHopOnionType is the type used in the onion to reference the ID
// of the next hop.
NextHopOnionType tlv.Type = 6
// EncryptedDataOnionType is the type used to include encrypted data
// provided by the receiver in the onion for use in blinded paths.
EncryptedDataOnionType tlv.Type = 10
// BlindingPointOnionType is the type used to include receiver provided
// ephemeral keys in the onion that are used in blinded paths.
BlindingPointOnionType tlv.Type = 12
// MetadataOnionType is the type used in the onion for the payment
// metadata.
MetadataOnionType tlv.Type = 16
// TotalAmtMsatBlindedType is the type used in the onion for the total
// amount field that is included in the final hop for blinded payments.
TotalAmtMsatBlindedType tlv.Type = 18
)
// NewAmtToFwdRecord creates a tlv.Record that encodes the amount_to_forward
// (type 2) for an onion payload.
func NewAmtToFwdRecord(amt *uint64) tlv.Record {
return tlv.MakeDynamicRecord(
AmtOnionType, amt, func() uint64 {
return tlv.SizeTUint64(*amt)
},
tlv.ETUint64, tlv.DTUint64,
)
}
// NewLockTimeRecord creates a tlv.Record that encodes the outgoing_cltv_value
// (type 4) for an onion payload.
func NewLockTimeRecord(lockTime *uint32) tlv.Record {
return tlv.MakeDynamicRecord(
LockTimeOnionType, lockTime, func() uint64 {
return tlv.SizeTUint32(*lockTime)
},
tlv.ETUint32, tlv.DTUint32,
)
}
// NewNextHopIDRecord creates a tlv.Record that encodes the short_channel_id
// (type 6) for an onion payload.
func NewNextHopIDRecord(cid *uint64) tlv.Record {
return tlv.MakePrimitiveRecord(NextHopOnionType, cid)
}
// NewEncryptedDataRecord creates a tlv.Record that encodes the encrypted_data
// (type 10) record for an onion payload.
func NewEncryptedDataRecord(data *[]byte) tlv.Record {
return tlv.MakePrimitiveRecord(EncryptedDataOnionType, data)
}
// NewBlindingPointRecord creates a tlv.Record that encodes the blinding_point
// (type 12) record for an onion payload.
func NewBlindingPointRecord(point **btcec.PublicKey) tlv.Record {
return tlv.MakePrimitiveRecord(BlindingPointOnionType, point)
}
// NewMetadataRecord creates a tlv.Record that encodes the metadata (type 10)
// for an onion payload.
func NewMetadataRecord(metadata *[]byte) tlv.Record {
return tlv.MakeDynamicRecord(
MetadataOnionType, metadata,
func() uint64 {
return uint64(len(*metadata))
},
tlv.EVarBytes, tlv.DVarBytes,
)
}
// NewTotalAmtMsatBlinded creates a tlv.Record that encodes the
// total_amount_msat for the final an onion payload within a blinded route.
func NewTotalAmtMsatBlinded(amt *uint64) tlv.Record {
return tlv.MakeDynamicRecord(
TotalAmtMsatBlindedType, amt, func() uint64 {
return tlv.SizeTUint64(*amt)
},
tlv.ETUint64, tlv.DTUint64,
)
}
+112
View File
@@ -0,0 +1,112 @@
package record
import (
"fmt"
"io"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// MPPOnionType is the type used in the onion to reference the MPP fields:
// total_amt and payment_addr.
const MPPOnionType tlv.Type = 8
// MPP is a record that encodes the fields necessary for multi-path payments.
type MPP struct {
// paymentAddr is a random, receiver-generated value used to avoid
// collisions with concurrent payers.
paymentAddr [32]byte
// totalMsat is the total value of the payment, potentially spread
// across more than one HTLC.
totalMsat lnwire.MilliSatoshi
}
// NewMPP generates a new MPP record with the given total and payment address.
func NewMPP(total lnwire.MilliSatoshi, addr [32]byte) *MPP {
return &MPP{
paymentAddr: addr,
totalMsat: total,
}
}
// PaymentAddr returns the payment address contained in the MPP record.
func (r *MPP) PaymentAddr() [32]byte {
return r.paymentAddr
}
// TotalMsat returns the total value of an MPP payment in msats.
func (r *MPP) TotalMsat() lnwire.MilliSatoshi {
return r.totalMsat
}
// MPPEncoder writes the MPP record to the provided io.Writer.
func MPPEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*MPP); ok {
err := tlv.EBytes32(w, &v.paymentAddr, buf)
if err != nil {
return err
}
return tlv.ETUint64T(w, uint64(v.totalMsat), buf)
}
return tlv.NewTypeForEncodingErr(val, "MPP")
}
const (
// minMPPLength is the minimum length of a serialized MPP TLV record,
// which occurs when the truncated encoding of total_amt_msat takes 0
// bytes, leaving only the payment_addr.
minMPPLength = 32
// maxMPPLength is the maximum length of a serialized MPP TLV record,
// which occurs when the truncated encoding of total_amt_msat takes 8
// bytes.
maxMPPLength = 40
)
// MPPDecoder reads the MPP record to the provided io.Reader.
func MPPDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if v, ok := val.(*MPP); ok && minMPPLength <= l && l <= maxMPPLength {
if err := tlv.DBytes32(r, &v.paymentAddr, buf, 32); err != nil {
return err
}
var total uint64
if err := tlv.DTUint64(r, &total, buf, l-32); err != nil {
return err
}
v.totalMsat = lnwire.MilliSatoshi(total)
return nil
}
return tlv.NewTypeForDecodingErr(val, "MPP", l, maxMPPLength)
}
// Record returns a tlv.Record that can be used to encode or decode this record.
func (r *MPP) Record() tlv.Record {
// Fixed-size, 32 byte payment address followed by truncated 64-bit
// total msat.
size := func() uint64 {
return 32 + tlv.SizeTUint64(uint64(r.totalMsat))
}
return tlv.MakeDynamicRecord(
MPPOnionType, r, size, MPPEncoder, MPPDecoder,
)
}
// PayloadSize returns the size this record takes up in encoded form.
func (r *MPP) PayloadSize() uint64 {
return 32 + tlv.SizeTUint64(uint64(r.totalMsat))
}
// String returns a human-readable representation of the mpp payload field.
func (r *MPP) String() string {
if r == nil {
return "<nil>"
}
return fmt.Sprintf("total=%v, addr=%x", r.totalMsat, r.paymentAddr)
}
+138
View File
@@ -0,0 +1,138 @@
package migration1
import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/payments/db/migration1/record"
"github.com/lightningnetwork/lnd/tlv"
)
// Vertex is a frozen local type representing a 33-byte compressed public key,
// equivalent to route.Vertex at the time this migration was written.
type Vertex [33]byte
// NewVertex returns a new Vertex from a compressed public key.
func NewVertex(pub *btcec.PublicKey) Vertex {
var v Vertex
copy(v[:], pub.SerializeCompressed())
return v
}
// NewVertexFromBytes returns a new Vertex from a serialized compressed public
// key byte slice.
func NewVertexFromBytes(b []byte) (Vertex, error) {
if len(b) != 33 {
return Vertex{}, fmt.Errorf("invalid vertex length %d, "+
"expected 33", len(b))
}
var v Vertex
copy(v[:], b)
return v, nil
}
// Hop is a frozen local snapshot of route.Hop, containing exactly the fields
// that existed at the time this migration was written. This ensures the
// migration's serialization boundary is independent of future changes to
// route.Hop, in particular the planned removal of LegacyPayload once the KV
// backend is phased out.
type Hop struct {
// PubKeyBytes is the raw bytes of the public key of the target node.
PubKeyBytes Vertex
// ChannelID is the unique channel ID for the channel.
ChannelID uint64
// OutgoingTimeLock is the timelock value that should be used when
// crafting the _outgoing_ HTLC from this hop.
OutgoingTimeLock uint32
// AmtToForward is the amount that this hop will forward to the next
// hop.
AmtToForward lnwire.MilliSatoshi
// MPP encapsulates the data required for option_mpp. This field should
// only be set for the final hop.
MPP *record.MPP
// AMP encapsulates the data required for option_amp. This field should
// only be set for the final hop.
AMP *record.AMP
// CustomRecords if non-nil are a set of additional TLV records that
// should be included in the forwarding instructions for this node.
CustomRecords record.CustomSet
// LegacyPayload signals that this node doesn't understand the new TLV
// payload, so the legacy payload format must be used.
//
// NOTE: This field is preserved here even though it is marked for
// removal in the live route.Hop, because old KV data may have been
// serialized with LegacyPayload=true and the migration must be able to
// deserialize it correctly.
LegacyPayload bool
// Metadata is additional data sent along with the payment to the
// payee.
Metadata []byte
// EncryptedData is an encrypted data blob included for hops that are
// part of a blinded route.
EncryptedData []byte
// BlindingPoint is an ephemeral public key used by introduction nodes
// in blinded routes.
BlindingPoint *btcec.PublicKey
// TotalAmtMsat is the total amount for a blinded payment. This field
// should only be set for the final hop in a blinded path.
TotalAmtMsat lnwire.MilliSatoshi
}
// Route is a frozen local snapshot of route.Route, containing exactly the
// fields that existed at the time this migration was written.
type Route struct {
// TotalTimeLock is the cumulative (final) time lock across the entire
// route.
TotalTimeLock uint32
// TotalAmount is the total amount of funds required to complete a
// payment over this route, including fees.
TotalAmount lnwire.MilliSatoshi
// SourcePubKey is the pubkey of the node where this route originates.
SourcePubKey Vertex
// Hops contains details concerning the specific forwarding details at
// each hop.
Hops []*Hop
// FirstHopAmount is the amount that should actually be sent to the
// first hop. Only differs from TotalAmount for custom channels.
FirstHopAmount tlv.RecordT[
tlv.TlvType0, tlv.BigSizeT[lnwire.MilliSatoshi],
]
// FirstHopWireCustomRecords is a set of custom TLV records to include
// in the wire message sent to the first hop.
FirstHopWireCustomRecords lnwire.CustomRecords
}
// FinalHop returns the last hop in the route.
func (r *Route) FinalHop() *Hop {
return r.Hops[len(r.Hops)-1]
}
// ReceiverAmt returns the amount forwarded to the final hop.
func (r *Route) ReceiverAmt() lnwire.MilliSatoshi {
return r.FinalHop().AmtToForward
}
// TotalFees returns the total fees paid along the route.
func (r *Route) TotalFees() lnwire.MilliSatoshi {
return r.TotalAmount - r.ReceiverAmt()
}
+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/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/payments/db/migration1/record"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"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
}
+820
View File
@@ -0,0 +1,820 @@
package migration1
import (
"bytes"
"context"
"database/sql"
"fmt"
"strconv"
"time"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"golang.org/x/time/rate"
)
// MigrationStats tracks migration progress.
type MigrationStats struct {
TotalPayments int64
SuccessfulPayments int64
FailedPayments int64
InFlightPayments int64
InitiatedPayments int64
TotalAttempts int64
SettledAttempts int64
FailedAttempts int64
InFlightAttempts int64
TotalHops int64
DuplicatePayments int64
DuplicateEntries int64
SkippedPayments int64
MigrationDuration time.Duration
}
// MigratePaymentsKVToSQL migrates payments from KV to SQL and validates
// migrated data in batches. Callers are responsible for executing this within
// a single SQL transaction if atomicity is required.
func MigratePaymentsKVToSQL(ctx context.Context, kvBackend kvdb.Backend,
sqlDB SQLMigrationQueries, cfg *SQLStoreConfig) error {
if cfg == nil {
return fmt.Errorf("missing SQL store config for migration")
}
if cfg.QueryCfg == nil {
return fmt.Errorf("missing SQL store config for " +
"validation")
}
if cfg.QueryCfg.MaxBatchSize == 0 {
return fmt.Errorf("invalid max batch size for " +
"validation")
}
stats := &MigrationStats{}
startTime := time.Now()
log.Infof("Starting payment migration from KV to SQL...")
var (
validationBatch []migratedPaymentRef
validatedPayments int64
reportInterval = rate.Sometimes{Interval: 5 * time.Second}
validationInterval = rate.Sometimes{Interval: 5 * time.Second}
)
// Open the KV backend in read-only mode.
err := kvBackend.View(func(kvTx kvdb.RTx) error {
// In case we start with an empty database, there are no
// payments to migrate.
paymentsBucket := kvTx.ReadBucket(paymentsRootBucket)
if paymentsBucket == nil {
log.Infof("No payments bucket found - database is " +
"empty")
return nil
}
// The index bucket maps sequence number -> payment hash.
indexes := kvTx.ReadBucket(paymentsIndexBucket)
if indexes == nil {
return fmt.Errorf("index bucket does not exist")
}
// We iterate over all sequence numbers in the index bucket to
// make sure we have the correct order of payments. Otherwise,
// if we just loop over the payments bucket, we might get the
// payments not in the chronological order but rather the
// lexicographical order of the payment hashes.
return indexes.ForEach(func(seqKey, indexVal []byte) error {
// Progress reporting based on time + actual work done.
reportProgress := func() {
elapsed := time.Since(startTime)
if elapsed <= 0 {
return
}
if stats.TotalPayments == 0 {
return
}
paymentRate := float64(stats.TotalPayments) /
elapsed.Seconds()
attemptRate := float64(stats.TotalAttempts) /
elapsed.Seconds()
log.Infof("Progress: %d payments, %d "+
"attempts, %d hops | Rate: %.1f "+
"pmt/s, %.1f att/s | Elapsed: %v",
stats.TotalPayments,
stats.TotalAttempts, stats.TotalHops,
paymentRate, attemptRate,
elapsed.Round(time.Second),
)
}
reportInterval.Do(reportProgress)
r := bytes.NewReader(indexVal)
paymentHash, err := deserializePaymentIndex(r)
if err != nil {
return err
}
paymentBucket := paymentsBucket.NestedReadBucket(
paymentHash[:],
)
if paymentBucket == nil {
// We skip the entry in case this sequence
// number does not have a corresponding
// payment bucket. But aborting would not help
// either because it is just a db inconsistency.
log.Warnf("Missing bucket for payment %x",
paymentHash[:8])
stats.SkippedPayments++
return nil
}
// Every payment bucket should have a sequence number
// which is also important to check for duplicates.
seqBytes := paymentBucket.Get(paymentSequenceKey)
if seqBytes == nil {
return ErrNoSequenceNumber
}
// Skip duplicates. They are migrated into the
// payment_duplicates table when the primary payment is
// processed.
if !bytes.Equal(seqBytes, seqKey) {
return nil
}
// Fetch the payment from the kv store.
payment, err := fetchPayment(paymentBucket)
if err != nil {
return fmt.Errorf("fetch payment %x: %w",
paymentHash[:8], err)
}
// Migrate the payment to the SQL database.
paymentID, err := migratePayment(
ctx, payment, paymentHash, sqlDB, stats,
)
if err != nil {
return fmt.Errorf("migrate payment %x: %w",
paymentHash[:8], err)
}
// Check for duplicates.
dupBucket := paymentBucket.NestedReadBucket(
duplicatePaymentsBucket,
)
if dupBucket != nil {
err = migrateDuplicatePayments(
ctx, dupBucket, paymentHash,
paymentID, sqlDB, stats,
)
if err != nil {
return fmt.Errorf("migrate duplicates "+
"%x: %w", paymentHash[:8],
err)
}
}
// Add the payment to the validation batch.
validationBatch = append(
validationBatch, migratedPaymentRef{
Hash: paymentHash,
PaymentID: paymentID,
},
)
if uint32(len(validationBatch)) >=
cfg.QueryCfg.MaxBatchSize {
err := validateMigratedPaymentBatch(
ctx, kvBackend, sqlDB,
cfg,
validationBatch,
)
if err != nil {
return err
}
validatedPayments += int64(
len(validationBatch),
)
// Log validation progress periodically.
validationInterval.Do(func() {
log.Infof("Validated %d/%d "+
"payments",
validatedPayments,
stats.TotalPayments,
)
})
validationBatch = validationBatch[:0]
}
return nil
})
}, func() {})
if err != nil {
return fmt.Errorf("migrate payments: %w", err)
}
// Validate any remaining payments in the batch.
if len(validationBatch) > 0 {
if err := validateMigratedPaymentBatch(
ctx, kvBackend, sqlDB, cfg, validationBatch,
); err != nil {
return err
}
validatedPayments += int64(len(validationBatch))
log.Infof("Validated %d/%d payments", validatedPayments,
stats.TotalPayments)
}
// Validate the total number of payments as an additional sanity check.
if err := validatePaymentCounts(
ctx, sqlDB, stats.TotalPayments,
); err != nil {
return err
}
stats.MigrationDuration = time.Since(startTime)
printMigrationSummary(stats)
return nil
}
// normalizeTimeForSQL converts a timestamp into the representation we persist
// and compare against in SQL:
// - drops any monotonic clock reading (SQL can't store it),
// - forces UTC for deterministic comparisons across environments.
//
// A zero time is returned unchanged.
func normalizeTimeForSQL(t time.Time) time.Time {
if t.IsZero() {
return t
}
return time.Unix(0, t.UnixNano()).UTC()
}
// migratePayment migrates a single payment from KV to SQL.
func migratePayment(ctx context.Context, payment *MPPayment, hash lntypes.Hash,
sqlDB SQLMigrationQueries, stats *MigrationStats) (int64, error) {
// Update migration stats based on payment status.
switch payment.Status {
case StatusSucceeded:
stats.SuccessfulPayments++
case StatusFailed:
stats.FailedPayments++
case StatusInFlight:
stats.InFlightPayments++
case StatusInitiated:
stats.InitiatedPayments++
}
// Prepare fail reason for SQL insert.
var failReason sql.NullInt32
if payment.FailureReason != nil {
failReason = sql.NullInt32{
Int32: int32(*payment.FailureReason),
Valid: true,
}
}
// Insert payment using migration query.
paymentID, err := sqlDB.InsertPaymentMig(
ctx, sqlc.InsertPaymentMigParams{
AmountMsat: int64(payment.Info.Value),
CreatedAt: normalizeTimeForSQL(
payment.Info.CreationTime,
),
PaymentIdentifier: hash[:],
FailReason: failReason,
})
if err != nil {
return 0, fmt.Errorf("insert payment: %w", err)
}
// Insert payment intent.
//
// Only insert a row if we have an actual intent payload. For legacy
// hash-only/keysend-style payments, the intent may be absent.
if len(payment.Info.PaymentRequest) > 0 {
_, err = sqlDB.InsertPaymentIntent(
ctx, sqlc.InsertPaymentIntentParams{
PaymentID: paymentID,
IntentType: int16(PaymentIntentTypeBolt11),
IntentPayload: payment.Info.PaymentRequest,
},
)
if err != nil {
return 0, fmt.Errorf("insert intent: %w", err)
}
}
// Insert first hop custom records (payment level).
for key, value := range payment.Info.FirstHopCustomRecords {
err = sqlDB.InsertPaymentFirstHopCustomRecord(ctx,
sqlc.InsertPaymentFirstHopCustomRecordParams{
PaymentID: paymentID,
Key: int64(key),
Value: value,
},
)
if err != nil {
return 0, fmt.Errorf("insert custom record: %w", err)
}
}
// Migrate HTLC attempts.
for _, htlc := range payment.HTLCs {
err = migrateHTLCAttempt(
ctx, paymentID, hash, &htlc, sqlDB, stats,
)
if err != nil {
return 0, fmt.Errorf("migrate attempt %d: %w",
htlc.AttemptID, err)
}
}
stats.TotalPayments++
return paymentID, nil
}
// migrateHTLCAttempt migrates a single HTLC attempt.
func migrateHTLCAttempt(ctx context.Context, paymentID int64,
parentPaymentHash lntypes.Hash, htlc *HTLCAttempt, sqlDB SQLQueries,
stats *MigrationStats) error {
// Determine the payment hash for this HTLC attempt.
//
// For AMP payments, each HTLC has its own unique hash. For non-AMP
// payments (MPP, Legacy), all HTLCs use the same hash as the parent
// payment. Older payment attempts may not have the hash stored
// explicitly, in which case we fall back to the parent payment hash
// which is ok since non-AMP payments have a single hash for all HTLCs.
var paymentHash []byte
switch {
case htlc.Hash != nil:
paymentHash = (*htlc.Hash)[:]
default:
// For older payments where Hash is nil, use the parent payment
// hash. This is consistent with how the router handles these
// legacy payments.
paymentHash = parentPaymentHash[:]
}
firstHopAmountMsat := int64(htlc.Route.FirstHopAmount.Val.Int())
// Get the session key bytes.
sessionKeyBytes := htlc.SessionKey().Serialize()
// Insert HTLC attempt.
_, err := sqlDB.InsertHtlcAttempt(ctx, sqlc.InsertHtlcAttemptParams{
PaymentID: paymentID,
AttemptIndex: int64(htlc.AttemptID),
SessionKey: sessionKeyBytes,
AttemptTime: normalizeTimeForSQL(htlc.AttemptTime),
PaymentHash: paymentHash,
FirstHopAmountMsat: firstHopAmountMsat,
RouteTotalTimeLock: int32(htlc.Route.TotalTimeLock),
RouteTotalAmount: int64(htlc.Route.TotalAmount),
RouteSourceKey: htlc.Route.SourcePubKey[:],
})
if err != nil {
return fmt.Errorf("insert HTLC attempt: %w", err)
}
// Insert the route-level first hop custom records.
for key, value := range htlc.Route.FirstHopWireCustomRecords {
err = sqlDB.InsertPaymentAttemptFirstHopCustomRecord(
ctx,
sqlc.InsertPaymentAttemptFirstHopCustomRecordParams{
HtlcAttemptIndex: int64(htlc.AttemptID),
Key: int64(key),
Value: value,
},
)
if err != nil {
return fmt.Errorf("insert attempt first hop custom "+
"record: %w", err)
}
}
// Insert route hops.
for hopIndex := range htlc.Route.Hops {
hop := htlc.Route.Hops[hopIndex]
err = migrateRouteHop(
ctx, int64(htlc.AttemptID), hopIndex, hop,
sqlDB, stats,
)
if err != nil {
return fmt.Errorf("migrate hop %d: %w", hopIndex, err)
}
}
// Handle attempt resolution (settle or fail).
switch {
case htlc.Settle != nil:
// Settled
err = sqlDB.SettleAttempt(ctx, sqlc.SettleAttemptParams{
AttemptIndex: int64(htlc.AttemptID),
ResolutionTime: normalizeTimeForSQL(
htlc.Settle.SettleTime,
),
ResolutionType: int32(HTLCAttemptResolutionSettled),
SettlePreimage: htlc.Settle.Preimage[:],
})
if err != nil {
return fmt.Errorf("settle attempt: %w", err)
}
stats.SettledAttempts++
case htlc.Failure != nil:
var failureMsg bytes.Buffer
if htlc.Failure.Message != nil {
err := lnwire.EncodeFailureMessage(
&failureMsg, htlc.Failure.Message, 0,
)
if err != nil {
return fmt.Errorf("failed to encode "+
"failure message: %w", err)
}
}
err = sqlDB.FailAttempt(ctx, sqlc.FailAttemptParams{
AttemptIndex: int64(htlc.AttemptID),
ResolutionTime: normalizeTimeForSQL(
htlc.Failure.FailTime,
),
ResolutionType: int32(HTLCAttemptResolutionFailed),
FailureSourceIndex: sql.NullInt32{
Int32: int32(htlc.Failure.FailureSourceIndex),
Valid: true,
},
HtlcFailReason: sql.NullInt32{
Int32: int32(htlc.Failure.Reason),
Valid: true,
},
FailureMsg: failureMsg.Bytes(),
})
if err != nil {
return fmt.Errorf("fail attempt: %w", err)
}
stats.FailedAttempts++
default:
// If the attempt is not settled or failed, it is in flight.
stats.InFlightAttempts++
}
stats.TotalAttempts++
return nil
}
// migrateRouteHop migrates a single route hop.
func migrateRouteHop(ctx context.Context,
attemptID int64, hopIndex int, hop *Hop, sqlDB SQLQueries,
stats *MigrationStats) error {
// Convert channel ID to string representation of uint64.
// The SCID is stored as a decimal string to match the converter
// expectations (sql_converters.go:173).
scidStr := strconv.FormatUint(hop.ChannelID, 10)
// Insert route hop.
hopID, err := sqlDB.InsertRouteHop(ctx, sqlc.InsertRouteHopParams{
HtlcAttemptIndex: attemptID,
HopIndex: int32(hopIndex),
PubKey: hop.PubKeyBytes[:],
Scid: scidStr,
OutgoingTimeLock: int32(hop.OutgoingTimeLock),
AmtToForward: int64(hop.AmtToForward),
MetaData: hop.Metadata,
})
if err != nil {
return fmt.Errorf("insert hop: %w", err)
}
// Check for blinded route data (route blinding).
if len(hop.EncryptedData) > 0 || hop.BlindingPoint != nil ||
hop.TotalAmtMsat != 0 {
var blindingPoint []byte
if hop.BlindingPoint != nil {
blindingPoint = hop.BlindingPoint.SerializeCompressed()
}
var totalAmt sql.NullInt64
if hop.TotalAmtMsat != 0 {
totalAmt = sql.NullInt64{
Int64: int64(hop.TotalAmtMsat),
Valid: true,
}
}
err := sqlDB.InsertRouteHopBlinded(
ctx, sqlc.InsertRouteHopBlindedParams{
HopID: hopID,
EncryptedData: hop.EncryptedData,
BlindingPoint: blindingPoint,
BlindedPathTotalAmt: totalAmt,
},
)
if err != nil {
return fmt.Errorf("insert blinded hop: %w", err)
}
}
// Check for MPP record.
if hop.MPP != nil {
paymentAddr := hop.MPP.PaymentAddr()
err = sqlDB.InsertRouteHopMpp(ctx, sqlc.InsertRouteHopMppParams{
HopID: hopID,
PaymentAddr: paymentAddr[:],
TotalMsat: int64(hop.MPP.TotalMsat()),
})
if err != nil {
return fmt.Errorf("insert MPP: %w", err)
}
}
// Check for AMP record.
if hop.AMP != nil {
rootShare := hop.AMP.RootShare()
setID := hop.AMP.SetID()
err = sqlDB.InsertRouteHopAmp(ctx, sqlc.InsertRouteHopAmpParams{
HopID: hopID,
RootShare: rootShare[:],
SetID: setID[:],
ChildIndex: int32(hop.AMP.ChildIndex()),
})
if err != nil {
return fmt.Errorf("insert AMP: %w", err)
}
}
// Check for custom records.
if hop.CustomRecords != nil {
for tlvType, value := range hop.CustomRecords {
err = sqlDB.InsertPaymentHopCustomRecord(
ctx,
sqlc.InsertPaymentHopCustomRecordParams{
HopID: hopID,
Key: int64(tlvType),
Value: value,
},
)
if err != nil {
return fmt.Errorf("insert hop custom "+
"record: %w", err)
}
}
}
stats.TotalHops++
return nil
}
// migrateDuplicatePayments migrates duplicate payments into the dedicated
// payment_duplicates table.
func migrateDuplicatePayments(ctx context.Context, dupBucket kvdb.RBucket,
hash [32]byte, primaryPaymentID int64, sqlDB SQLMigrationQueries,
stats *MigrationStats) error {
duplicateCount := 0
err := dupBucket.ForEach(func(seqBytes, _ []byte) error {
// The duplicates bucket should only contain nested buckets
// keyed by 8-byte sequence numbers. Skip any unexpected keys
// (defensive check for corrupted or malformed data).
if len(seqBytes) != 8 {
log.Warnf("Skipping unexpected key in duplicates "+
"bucket for payment %x: key length %d, "+
"expected 8",
hash[:8], len(seqBytes),
)
return nil
}
seqNum := byteOrder.Uint64(seqBytes)
subBucket := dupBucket.NestedReadBucket(seqBytes)
if subBucket == nil {
return nil
}
duplicateCount++
log.Infof("Migrating duplicate payment seq=%d for "+
"payment %x", seqNum, hash[:8])
err := migrateSingleDuplicatePayment(
ctx, subBucket, hash, primaryPaymentID, seqNum,
sqlDB,
)
if err != nil {
return fmt.Errorf(
"migrate duplicate payment seq=%d: %w",
seqNum, err,
)
}
return nil
})
if duplicateCount > 0 {
stats.DuplicatePayments++
stats.DuplicateEntries += int64(duplicateCount)
log.Infof("Payment %x had %d duplicate(s) migrated", hash[:8],
duplicateCount)
}
return err
}
// migrateSingleDuplicatePayment inserts a duplicate payment record for the
// given payment hash into payment_duplicates.
func migrateSingleDuplicatePayment(ctx context.Context, dupBucket kvdb.RBucket,
hash [32]byte, primaryPaymentID int64, duplicateSeq uint64,
sqlDB SQLMigrationQueries) error {
creationData := dupBucket.Get(duplicatePaymentCreationInfoKey)
if creationData == nil {
return fmt.Errorf("duplicate payment seq=%d missing "+
"creation info (payment=%x)", duplicateSeq, hash[:8])
}
creationInfo, err := deserializeDuplicatePaymentCreationInfo(
bytes.NewReader(creationData),
)
if err != nil {
return fmt.Errorf("deserialize duplicate creation "+
"info: %w", err)
}
settleData := dupBucket.Get(duplicatePaymentSettleInfoKey)
failReasonData := dupBucket.Get(duplicatePaymentFailInfoKey)
attemptData := dupBucket.Get(duplicatePaymentAttemptInfoKey)
if settleData != nil && len(failReasonData) > 0 {
return fmt.Errorf("duplicate payment seq=%d has both "+
"settle and fail info (payment=%x)", duplicateSeq,
hash[:8])
}
var (
failReason sql.NullInt32
settlePreimage []byte
settleTime sql.NullTime
)
switch {
case settleData != nil:
settlePreimage, settleTime, err = parseDuplicateSettleData(
settleData,
)
if err != nil {
return err
}
case len(failReasonData) > 0:
failReason = sql.NullInt32{
Int32: int32(failReasonData[0]),
Valid: true,
}
default:
// If the duplicate payment has no settle or fail info,
// we mark it as failed during the migration. Duplicate
// payments were a bug in older versions of LND, so we can be
// sure if a duplicate payment has no failure reason or
// settlement data, the corresponding HTLC for this payment
// has been failed (resolved).
if attemptData == nil {
log.Warnf("Duplicate payment seq=%d has no "+
"attempt info and no resolution (payment=%x); "+
"marking failed", duplicateSeq, hash[:8])
} else {
log.Warnf("Duplicate payment seq=%d has attempt "+
"info but no resolution (payment=%x); "+
"marking failed", duplicateSeq, hash[:8])
}
failReason = sql.NullInt32{
Int32: int32(FailureReasonError),
Valid: true,
}
}
_, err = sqlDB.InsertPaymentDuplicateMig(
ctx, sqlc.InsertPaymentDuplicateMigParams{
PaymentID: primaryPaymentID,
AmountMsat: int64(creationInfo.Value),
CreatedAt: normalizeTimeForSQL(
creationInfo.CreationTime,
),
FailReason: failReason,
SettlePreimage: settlePreimage,
SettleTime: settleTime,
},
)
if err != nil {
return fmt.Errorf("insert duplicate payment: %w", err)
}
return nil
}
// parseDuplicateSettleData extracts settle data from either legacy or modern
// duplicate formats.
func parseDuplicateSettleData(settleData []byte) ([]byte, sql.NullTime, error) {
if len(settleData) == lntypes.PreimageSize {
return append([]byte(nil), settleData...), sql.NullTime{}, nil
}
settleInfo, err := deserializeHTLCSettleInfo(
bytes.NewReader(settleData),
)
if err != nil {
return nil, sql.NullTime{},
fmt.Errorf("deserialize duplicate settle: %w", err)
}
settleTime := normalizeTimeForSQL(settleInfo.SettleTime)
return settleInfo.Preimage[:], sql.NullTime{
Time: settleTime,
Valid: !settleTime.IsZero(),
}, nil
}
// printMigrationSummary prints a summary of the migration.
func printMigrationSummary(stats *MigrationStats) {
if stats.TotalPayments == 0 {
log.Infof("No payments migrated - database is empty")
return
}
log.Infof("========================================")
log.Infof(" Payment Migration Summary")
log.Infof("========================================")
log.Infof("Total Payments: %d", stats.TotalPayments)
log.Infof(" Successful: %d", stats.SuccessfulPayments)
log.Infof(" Failed: %d", stats.FailedPayments)
log.Infof(" In-Flight: %d", stats.InFlightPayments)
log.Infof(" Initiated: %d", stats.InitiatedPayments)
log.Infof("")
log.Infof("Total HTLC Attempts: %d", stats.TotalAttempts)
log.Infof(" Settled: %d", stats.SettledAttempts)
log.Infof(" Failed: %d", stats.FailedAttempts)
log.Infof(" In-Flight: %d", stats.InFlightAttempts)
log.Infof("")
log.Infof("Total Route Hops: %d", stats.TotalHops)
if stats.SkippedPayments > 0 {
log.Infof("")
log.Warnf("SKIPPED PAYMENTS:")
log.Warnf(" Indexed payments with missing buckets: %d",
stats.SkippedPayments)
log.Warnf(" These indicate minor DB inconsistencies.")
}
if stats.DuplicatePayments > 0 {
log.Infof("")
log.Warnf("DUPLICATE PAYMENTS DETECTED:")
log.Warnf(" Unique payment hashes with duplicates: %d",
stats.DuplicatePayments)
log.Warnf(" Total duplicate entries migrated: %d",
stats.DuplicateEntries)
log.Warnf(" These were caused by an old LND bug.")
}
log.Infof("")
log.Infof("Migration Duration: %v", stats.MigrationDuration)
log.Infof("========================================")
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package sqlc
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
+123
View File
@@ -0,0 +1,123 @@
package sqlc
import (
"fmt"
"strings"
)
// GetTx returns the underlying DBTX (either *sql.DB or *sql.Tx) used by the
// Queries struct.
func (q *Queries) GetTx() DBTX {
return q.db
}
// makeQueryParams generates a string of query parameters for a SQL query. It is
// meant to replace the `?` placeholders in a SQL query with numbered parameters
// like `$1`, `$2`, etc. This is required for the sqlc /*SLICE:<field_name>*/
// workaround. See scripts/gen_sqlc_docker.sh for more details.
func makeQueryParams(numTotalArgs, numListArgs int) string {
if numListArgs == 0 {
return ""
}
var b strings.Builder
// Pre-allocate a rough estimation of the buffer size to avoid
// re-allocations. A parameter like $1000, takes 6 bytes.
b.Grow(numListArgs * 6)
diff := numTotalArgs - numListArgs
for i := 0; i < numListArgs; i++ {
if i > 0 {
// We don't need to check the error here because the
// WriteString method of strings.Builder always returns
// nil.
_, _ = b.WriteString(",")
}
// We don't need to check the error here because the
// Write method (called by fmt.Fprintf) of strings.Builder
// always returns nil.
_, _ = fmt.Fprintf(&b, "$%d", i+diff+1)
}
return b.String()
}
// PaymentAndIntent is an interface that provides access to a payment and its
// associated payment intent.
type PaymentAndIntent interface {
// GetPayment returns the Payment associated with this interface.
GetPayment() Payment
// GetPaymentIntent returns the PaymentIntent associated with this
// payment.
GetPaymentIntent() PaymentIntent
}
// GetPayment returns the Payment associated with this interface.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FilterPaymentsRow) GetPayment() Payment {
return r.Payment
}
// GetPaymentIntent returns the PaymentIntent associated with this payment.
// If the payment has no intent (IntentType is NULL), this returns a zero-value
// PaymentIntent.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FilterPaymentsRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}
// GetPayment returns the Payment associated with this interface.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentRow) GetPayment() Payment {
return r.Payment
}
// GetPaymentIntent returns the PaymentIntent associated with this payment.
// If the payment has no intent (IntentType is NULL), this returns a zero-value
// PaymentIntent.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}
func (r FetchPaymentsByIDsRow) GetPayment() Payment {
return Payment{
ID: r.ID,
AmountMsat: r.AmountMsat,
CreatedAt: r.CreatedAt,
PaymentIdentifier: r.PaymentIdentifier,
FailReason: r.FailReason,
}
}
func (r FetchPaymentsByIDsRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}
+110
View File
@@ -0,0 +1,110 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package sqlc
import (
"database/sql"
"time"
)
type Payment struct {
ID int64
AmountMsat int64
CreatedAt time.Time
PaymentIdentifier []byte
FailReason sql.NullInt32
}
type PaymentAttemptFirstHopCustomRecord struct {
ID int64
HtlcAttemptIndex int64
Key int64
Value []byte
}
type PaymentDuplicate struct {
ID int64
PaymentID int64
AmountMsat int64
CreatedAt time.Time
FailReason sql.NullInt32
SettlePreimage []byte
SettleTime sql.NullTime
}
type PaymentFirstHopCustomRecord struct {
ID int64
PaymentID int64
Key int64
Value []byte
}
type PaymentHopCustomRecord struct {
ID int64
HopID int64
Key int64
Value []byte
}
type PaymentHtlcAttempt struct {
ID int64
AttemptIndex int64
PaymentID int64
SessionKey []byte
AttemptTime time.Time
PaymentHash []byte
FirstHopAmountMsat int64
RouteTotalTimeLock int32
RouteTotalAmount int64
RouteSourceKey []byte
}
type PaymentHtlcAttemptResolution struct {
AttemptIndex int64
ResolutionTime time.Time
ResolutionType int32
SettlePreimage []byte
FailureSourceIndex sql.NullInt32
HtlcFailReason sql.NullInt32
FailureMsg []byte
}
type PaymentIntent struct {
ID int64
PaymentID int64
IntentType int16
IntentPayload []byte
}
type PaymentRouteHop struct {
ID int64
HtlcAttemptIndex int64
HopIndex int32
PubKey []byte
Scid string
OutgoingTimeLock int32
AmtToForward int64
MetaData []byte
}
type PaymentRouteHopAmp struct {
HopID int64
RootShare []byte
SetID []byte
ChildIndex int32
}
type PaymentRouteHopBlinded struct {
HopID int64
EncryptedData []byte
BlindingPoint []byte
BlindedPathTotalAmt sql.NullInt64
}
type PaymentRouteHopMpp struct {
HopID int64
PaymentAddr []byte
TotalMsat int64
}
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
package migration1
import (
"testing"
"github.com/lightningnetwork/lnd/lntypes"
)
// TestHarness provides implementation-specific test utilities for the payments
// database. Different database backends (KV, SQL) have different internal
// structures and indexing mechanisms, so this interface allows tests to verify
// implementation-specific behavior without coupling the test logic to a
// particular backend.
type TestHarness interface {
// AssertPaymentIndex checks that a payment is correctly indexed.
// For KV: verifies the payment index bucket entry exists and points
// to the correct payment hash.
// For SQL: no-op (SQL doesn't use a separate index bucket).
AssertPaymentIndex(t *testing.T, expectedHash lntypes.Hash)
// AssertNoIndex checks that an index for a sequence number doesn't
// exist.
// For KV: verifies the index bucket entry is deleted.
// For SQL: no-op.
AssertNoIndex(t *testing.T, seqNr uint64)
}
+94
View File
@@ -0,0 +1,94 @@
//go:build test_db_postgres && !test_db_sqlite
package migration1
import (
"testing"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates a SQLStore backed by a SQL
// database for testing.
func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) {
db := NewTestDBWithFixture(t, nil, opts...)
return db, &noopTestHarness{}
}
// NewTestDBFixture creates a new sqldb.TestPgFixture for testing purposes.
func NewTestDBFixture(t *testing.T) *sqldb.TestPgFixture {
pgFixture := sqldb.NewTestPgFixture(
t, sqldb.DefaultPostgresFixtureLifetime,
)
t.Cleanup(func() {
pgFixture.TearDown(t)
})
return pgFixture
}
// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a
// SQL database for testing.
func NewTestDBWithFixture(t testing.TB,
pgFixture *sqldb.TestPgFixture, opts ...OptionModifier) DB {
var querier BatchedSQLQueries
if pgFixture == nil {
querier = newBatchQuerier(t)
} else {
querier = newBatchQuerierWithFixture(t, pgFixture)
}
store, err := NewSQLStore(
&SQLStoreConfig{
QueryCfg: sqldb.DefaultPostgresConfig(),
}, querier, opts...,
)
require.NoError(t, err)
return store
}
// newBatchQuerier creates a new BatchedSQLQueries instance for testing
// using a PostgreSQL database fixture.
func newBatchQuerier(t testing.TB) BatchedSQLQueries {
pgFixture := sqldb.NewTestPgFixture(
t, sqldb.DefaultPostgresFixtureLifetime,
)
t.Cleanup(func() {
pgFixture.TearDown(t)
})
return newBatchQuerierWithFixture(t, pgFixture)
}
// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for
// testing using a PostgreSQL database fixture.
func newBatchQuerierWithFixture(t testing.TB,
pgFixture *sqldb.TestPgFixture) BatchedSQLQueries {
rawDB := sqldb.NewTestPostgresDB(t, pgFixture).BaseDB.DB
return &testBatchedSQLQueries{
db: rawDB,
Queries: sqlc.New(rawDB),
}
}
// noopTestHarness is the SQL test harness implementation. Since SQL doesn't
// use a separate payment index bucket like KV, these assertions are no-ops.
type noopTestHarness struct{}
// AssertPaymentIndex is a no-op for SQL implementations.
func (h *noopTestHarness) AssertPaymentIndex(t *testing.T,
expectedHash lntypes.Hash) {
// No-op: SQL doesn't use a separate index bucket.
}
// AssertNoIndex is a no-op for SQL implementations.
func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) {
// No-op: SQL doesn't use a separate index bucket.
}
+58
View File
@@ -0,0 +1,58 @@
//go:build test_db_postgres || test_db_sqlite
package migration1
import (
"context"
"database/sql"
"testing"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// setupTestSQLDB creates a SQLStore-backed test database.
func setupTestSQLDB(t testing.TB, opts ...OptionModifier) *SQLStore {
t.Helper()
db, _ := NewTestDB(t, opts...)
sqlStore, ok := db.(*SQLStore)
require.True(t, ok)
return sqlStore
}
// testBatchedSQLQueries is a simple implementation of BatchedSQLQueries for
// testing.
type testBatchedSQLQueries struct {
db *sql.DB
*sqlc.Queries
}
// ExecTx implements the transaction execution logic.
func (t *testBatchedSQLQueries) ExecTx(ctx context.Context,
txOpts sqldb.TxOptions, txBody func(SQLQueries) error,
reset func()) error {
sqlOptions := sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: txOpts.ReadOnly(),
}
tx, err := t.db.BeginTx(ctx, &sqlOptions)
if err != nil {
return err
}
reset()
queries := sqlc.New(tx)
if err := txBody(queries); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
+73
View File
@@ -0,0 +1,73 @@
//go:build !test_db_postgres && test_db_sqlite
package migration1
import (
"testing"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates a SQLStore backed by a SQL
// database for testing.
func NewTestDB(t testing.TB, opts ...OptionModifier) (DB, TestHarness) {
db := NewTestDBWithFixture(t, nil, opts...)
return db, &noopTestHarness{}
}
// NewTestDBFixture is a no-op for the sqlite build.
func NewTestDBFixture(_ *testing.T) *sqldb.TestPgFixture {
return nil
}
// NewTestDBWithFixture is a helper function that creates a SQLStore backed by a
// SQL database for testing.
func NewTestDBWithFixture(t testing.TB, _ *sqldb.TestPgFixture,
opts ...OptionModifier) DB {
store, err := NewSQLStore(
&SQLStoreConfig{
QueryCfg: sqldb.DefaultSQLiteConfig(),
}, newBatchQuerier(t), opts...,
)
require.NoError(t, err)
return store
}
// newBatchQuerier creates a new BatchedSQLQueries instance for testing
// using a SQLite database.
func newBatchQuerier(t testing.TB) BatchedSQLQueries {
return newBatchQuerierWithFixture(t, nil)
}
// newBatchQuerierWithFixture creates a new BatchedSQLQueries instance for
// testing using a SQLite database.
func newBatchQuerierWithFixture(t testing.TB,
_ *sqldb.TestPgFixture) BatchedSQLQueries {
rawDB := sqldb.NewTestSqliteDB(t).BaseDB.DB
return &testBatchedSQLQueries{
db: rawDB,
Queries: sqlc.New(rawDB),
}
}
// noopTestHarness is the SQL test harness implementation. Since SQL doesn't
// use a separate payment index bucket like KV, these assertions are no-ops.
type noopTestHarness struct{}
// AssertPaymentIndex is a no-op for SQL implementations.
func (h *noopTestHarness) AssertPaymentIndex(t *testing.T,
expectedHash lntypes.Hash) {
// No-op: SQL doesn't use a separate index bucket.
}
// AssertNoIndex is a no-op for SQL implementations.
func (h *noopTestHarness) AssertNoIndex(t *testing.T, seqNr uint64) {
// No-op: SQL doesn't use a separate index bucket.
}
+52
View File
@@ -0,0 +1,52 @@
# Payment Migration External Testdata
This directory holds a real `channel.db` (bbolt) or `channel.sqlite` file for
testing the payments KV to SQL migration locally. You can also point the test
at an existing Postgres-backed kvdb instance.
## How to use
1. Copy your `channel.db` or `channel.sqlite` file into this folder.
2. Edit `migration_external_test.go`:
```go
// Comment out this line to enable the test
t.Skipf("skipping test meant for local debugging only")
// Set to your database filename
const fileName = "channel.db" // or "channel.sqlite"
```
3. Run the test:
```bash
# For Postgres backend
go test -v -tags="test_db_postgres" -run TestMigrationWithExternalDB
```
## SQLite kvdb source
To migrate from a `channel.sqlite` file, run with the `kvdb_sqlite` build
tag:
```bash
go test -v -tags="test_db_sqlite kvdb_sqlite" \
-run TestMigrationWithExternalDB
```
## Postgres kvdb source
To migrate from an existing Postgres-backed kvdb instance, edit
`postgresKVDSN` in `migration_external_test.go` (set it non-empty), then
run with the `kvdb_postgres` build tag:
```bash
go test -v -tags="kvdb_postgres test_db_postgres" \
-run TestMigrationWithExternalDB
```
## Notes
- The external database is opened read-only.
- The test creates a fresh SQL database for each run.
- Do not commit production data; keep the file local.
+27
View File
@@ -57,6 +57,8 @@ type SQLQueries interface {
FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error) FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error)
FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error) FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error)
FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]sqlc.PaymentDuplicate, error)
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error)
FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error)
FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error) FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error)
@@ -89,6 +91,31 @@ type SQLQueries interface {
DeleteFailedAttempts(ctx context.Context, paymentID int64) error DeleteFailedAttempts(ctx context.Context, paymentID int64) error
} }
// SQLMigrationQueries extends SQLQueries with the additional queries needed
// for the one-time migration from KV to SQL. Keeping them in a separate
// interface makes it clear which code paths are migration-only and prevents
// the regular store from accidentally depending on them.
//
//nolint:ll
type SQLMigrationQueries interface {
SQLQueries
// FetchPaymentsByIDsMig is a migration-only batch fetch that returns
// payment data along with HTLC attempt counts for structural
// validation.
FetchPaymentsByIDsMig(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsMigRow, error)
// InsertPaymentMig is a migration-only variant of InsertPayment that
// allows setting fail_reason when inserting historical payments, since
// for real payments they have not failed at creation time and so no
// failure reason would exist yet.
InsertPaymentMig(ctx context.Context, arg sqlc.InsertPaymentMigParams) (int64, error)
// InsertPaymentDuplicateMig inserts a duplicate payment record during
// migration.
InsertPaymentDuplicateMig(ctx context.Context, arg sqlc.InsertPaymentDuplicateMigParams) (int64, error)
}
// BatchedSQLQueries is a version of the SQLQueries that's capable // BatchedSQLQueries is a version of the SQLQueries that's capable
// of batched database operations. // of batched database operations.
type BatchedSQLQueries interface { type BatchedSQLQueries interface {
+14
View File
@@ -8,4 +8,18 @@ var migrationAdditions = []MigrationConfig{
Version: 12, Version: 12,
SchemaVersion: 10, SchemaVersion: 10,
}, },
{
Name: "000011_payment_duplicates",
Version: 13,
SchemaVersion: 11,
},
{
Name: "kv_payments_migration",
Version: 14,
SchemaVersion: 11,
// A migration function may be attached to this
// migration to migrate KV payments to the native SQL
// schema. This is optional and can be disabled by the
// user if necessary.
},
} }
+97
View File
@@ -616,3 +616,100 @@ func TestMigrationSucceedsAfterDirtyStateMigrationFailure19RC1(t *testing.T) {
require.False(t, dirty) require.False(t, dirty)
}) })
} }
// TestMigrationConfigConsistency verifies that the migration configuration in
// migrationConfig is consistent with the actual SQL schema files embedded in
// the binary. This catches version collisions (e.g. two migrations claiming
// the same schema version) and missing schema files.
func TestMigrationConfigConsistency(t *testing.T) {
t.Parallel()
migrations := GetMigrations()
require.NotEmpty(t, migrations)
// Build a set of schema versions that have actual .up.sql files in
// the embedded filesystem.
embeddedFiles, err := sqlSchemas.ReadDir("sqlc/migrations")
require.NoError(t, err)
fileSchemaVersions := make(map[int]string)
for _, f := range embeddedFiles {
if f.IsDir() {
continue
}
var version int
_, err := fmt.Sscanf(f.Name(), "%06d_", &version)
require.NoError(t, err, "schema migration file %q is "+
"missing a valid numeric prefix (expected "+
"format: 000XXX_name.up.sql)", f.Name())
// Enforce the 6-digit zero-padded naming convention
// for consistent directory listing order.
expectedPrefix := fmt.Sprintf("%06d_", version)
require.True(t,
len(f.Name()) > len(expectedPrefix) &&
f.Name()[:len(expectedPrefix)] == expectedPrefix,
"schema migration file %q should use 6-digit "+
"zero-padded prefix %q", f.Name(),
expectedPrefix)
// Verify no two files share the same numeric prefix.
if existing, ok := fileSchemaVersions[version]; ok {
t.Fatalf("duplicate schema file version %06d: "+
"%q and %q", version, existing, f.Name())
}
fileSchemaVersions[version] = f.Name()
}
// Track seen versions to detect duplicates.
seenVersions := make(map[int]string)
seenSchemaVersions := make(map[int]string)
for _, m := range migrations {
// 1. Verify no duplicate global versions.
if existing, ok := seenVersions[m.Version]; ok {
t.Fatalf("duplicate global version %d: %q and %q",
m.Version, existing, m.Name)
}
seenVersions[m.Version] = m.Name
// 2. For schema migrations (those that advance the schema
// version), verify a corresponding .up.sql file exists
// and no two config entries claim the same schema version
// with different file prefixes.
prevSchema := 0
if m.Version > 1 {
prevSchema = migrations[m.Version-2].SchemaVersion
}
// A migration advances the schema if its SchemaVersion is
// higher than the previous migration's SchemaVersion.
if m.SchemaVersion > prevSchema {
_, hasFile := fileSchemaVersions[m.SchemaVersion]
require.True(t, hasFile,
"migration %q (version %d) declares "+
"SchemaVersion=%d but no %06d_*.up.sql"+
" file exists in the embedded FS",
m.Name, m.Version, m.SchemaVersion,
m.SchemaVersion)
if existing, ok := seenSchemaVersions[m.SchemaVersion]; ok {
t.Fatalf("duplicate schema version %d: "+
"%q and %q", m.SchemaVersion,
existing, m.Name)
}
seenSchemaVersions[m.SchemaVersion] = m.Name
}
}
// 3. Verify versions are sequential starting from 1.
for i, m := range migrations {
require.Equal(t, i+1, m.Version,
"migration %q has version %d but expected %d "+
"(migrations must be sequential)",
m.Name, m.Version, i+1)
}
}
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_payment_duplicates_payment_id;
DROP TABLE IF EXISTS payment_duplicates;
@@ -0,0 +1,43 @@
-- ─────────────────────────────────────────────
-- Payment Duplicate Records Table
-- ─────────────────────────────────────────────
-- Stores duplicate payment records that were created in older versions
-- of lnd. This table is intentionally minimal and is expected to be dropped
-- in the future especially if no duplicates were migrated.
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS payment_duplicates (
-- Primary key for the duplicate record.
id INTEGER PRIMARY KEY,
-- Reference to the primary payment this duplicate belongs to.
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
-- Amount of the duplicate payment in millisatoshis.
amount_msat BIGINT NOT NULL,
-- Timestamp when the duplicate payment was created.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Failure reason for failed payments (if known).
fail_reason INTEGER,
-- Settlement payload for succeeded payments (if known).
settle_preimage BLOB,
-- Settlement time for succeeded payments (if known).
settle_time TIMESTAMP,
-- Ensure we record either a failure reason or settlement data.
-- During the migration if we encounter a duplicate payment that has no
-- failure reason or settlement data, we will mark it as failed. Duplicate
-- payments were a bug in older versions of LND, so we can be sure if a
-- duplicate payment has no failure reason or settlement data, the
-- corresponding HTLC has been failed.
CONSTRAINT chk_payment_duplicates_outcome
CHECK (fail_reason IS NOT NULL OR settle_preimage IS NOT NULL)
);
-- Index for efficient lookup by primary payment.
CREATE INDEX IF NOT EXISTS idx_payment_duplicates_payment_id
ON payment_duplicates(payment_id);
+10
View File
@@ -224,6 +224,16 @@ type PaymentAttemptFirstHopCustomRecord struct {
Value []byte Value []byte
} }
type PaymentDuplicate struct {
ID int64
PaymentID int64
AmountMsat int64
CreatedAt time.Time
FailReason sql.NullInt32
SettlePreimage []byte
SettleTime sql.NullTime
}
type PaymentFirstHopCustomRecord struct { type PaymentFirstHopCustomRecord struct {
ID int64 ID int64
PaymentID int64 PaymentID int64
+203
View File
@@ -479,6 +479,53 @@ func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (F
return i, err return i, err
} }
const fetchPaymentDuplicates = `-- name: FetchPaymentDuplicates :many
SELECT
id,
payment_id,
amount_msat,
created_at,
fail_reason,
settle_preimage,
settle_time
FROM payment_duplicates
WHERE payment_id = $1
ORDER BY id ASC
`
// Fetch all duplicate payment records from the payment_duplicates table for
// a given payment ID.
func (q *Queries) FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error) {
rows, err := q.db.QueryContext(ctx, fetchPaymentDuplicates, paymentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PaymentDuplicate
for rows.Next() {
var i PaymentDuplicate
if err := rows.Scan(
&i.ID,
&i.PaymentID,
&i.AmountMsat,
&i.CreatedAt,
&i.FailReason,
&i.SettlePreimage,
&i.SettleTime,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many
SELECT SELECT
l.id, l.id,
@@ -597,6 +644,72 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([
return items, nil return items, nil
} }
const fetchPaymentsByIDsMig = `-- name: FetchPaymentsByIDsMig :many
SELECT
p.id,
p.amount_msat,
p.created_at,
p.payment_identifier,
p.fail_reason,
COUNT(ha.id) AS htlc_attempt_count
FROM payments p
LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id
WHERE p.id IN (/*SLICE:payment_ids*/?)
GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason
ORDER BY p.id ASC
`
type FetchPaymentsByIDsMigRow struct {
ID int64
AmountMsat int64
CreatedAt time.Time
PaymentIdentifier []byte
FailReason sql.NullInt32
HtlcAttemptCount int64
}
// Migration-specific batch fetch that returns payment data along with HTLC
// attempt counts for structural validation during KV to SQL migration.
func (q *Queries) FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error) {
query := fetchPaymentsByIDsMig
var queryParams []interface{}
if len(paymentIds) > 0 {
for _, v := range paymentIds {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1)
} else {
query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []FetchPaymentsByIDsMigRow
for rows.Next() {
var i FetchPaymentsByIDsMigRow
if err := rows.Scan(
&i.ID,
&i.AmountMsat,
&i.CreatedAt,
&i.PaymentIdentifier,
&i.FailReason,
&i.HtlcAttemptCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many
SELECT SELECT
l.id, l.id,
@@ -843,6 +956,51 @@ func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context,
return err return err
} }
const insertPaymentDuplicateMig = `-- name: InsertPaymentDuplicateMig :one
INSERT INTO payment_duplicates (
payment_id,
amount_msat,
created_at,
fail_reason,
settle_preimage,
settle_time
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6
)
RETURNING id
`
type InsertPaymentDuplicateMigParams struct {
PaymentID int64
AmountMsat int64
CreatedAt time.Time
FailReason sql.NullInt32
SettlePreimage []byte
SettleTime sql.NullTime
}
// Insert a duplicate payment record into the payment_duplicates table and
// return its ID.
func (q *Queries) InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPaymentDuplicateMig,
arg.PaymentID,
arg.AmountMsat,
arg.CreatedAt,
arg.FailReason,
arg.SettlePreimage,
arg.SettleTime,
)
var id int64
err := row.Scan(&id)
return id, err
}
const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec
INSERT INTO payment_first_hop_custom_records ( INSERT INTO payment_first_hop_custom_records (
payment_id, payment_id,
@@ -918,6 +1076,51 @@ func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentInte
return id, err return id, err
} }
const insertPaymentMig = `-- name: InsertPaymentMig :one
/* ─────────────────────────────────────────────
Migration-specific queries
These queries are used ONLY for the one-time migration from KV to SQL.
─────────────────────────────────────────────
*/
INSERT INTO payments (
amount_msat,
created_at,
payment_identifier,
fail_reason)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING id
`
type InsertPaymentMigParams struct {
AmountMsat int64
CreatedAt time.Time
PaymentIdentifier []byte
FailReason sql.NullInt32
}
// Migration-specific payment insert that allows setting fail_reason.
// Normal InsertPayment forces fail_reason to NULL since new payments
// aren't failed yet. During migration, we're inserting historical data
// that may already be failed.
func (q *Queries) InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPaymentMig,
arg.AmountMsat,
arg.CreatedAt,
arg.PaymentIdentifier,
arg.FailReason,
)
var id int64
err := row.Scan(&id)
return id, err
}
const insertRouteHop = `-- name: InsertRouteHop :one const insertRouteHop = `-- name: InsertRouteHop :one
INSERT INTO payment_route_hops ( INSERT INTO payment_route_hops (
htlc_attempt_index, htlc_attempt_index,
+14
View File
@@ -49,11 +49,17 @@ type Querier interface {
FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error) FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error)
FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error) FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error)
FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error) FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error)
// Fetch all duplicate payment records from the payment_duplicates table for
// a given payment ID.
FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error)
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error)
// Batch fetch payment and intent data for a set of payment IDs. // Batch fetch payment and intent data for a set of payment IDs.
// Used to avoid fetching redundant payment data when processing multiple // Used to avoid fetching redundant payment data when processing multiple
// attempts for the same payment. // attempts for the same payment.
FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error)
// Migration-specific batch fetch that returns payment data along with HTLC
// attempt counts for structural validation during KV to SQL migration.
FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error)
// FetchPendingInvoices returns all invoices in a pending state (open or // FetchPendingInvoices returns all invoices in a pending state (open or
// accepted). The invoices_state_idx index on the state column makes this a // accepted). The invoices_state_idx index on the state column makes this a
// fast index scan rather than a full table scan. // fast index scan rather than a full table scan.
@@ -174,10 +180,18 @@ type Querier interface {
// payment process. // payment process.
InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error) InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error)
InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error
// Insert a duplicate payment record into the payment_duplicates table and
// return its ID.
InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error)
InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error
InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error
// Insert a payment intent for a given payment and return its ID. // Insert a payment intent for a given payment and return its ID.
InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error)
// Migration-specific payment insert that allows setting fail_reason.
// Normal InsertPayment forces fail_reason to NULL since new payments
// aren't failed yet. During migration, we're inserting historical data
// that may already be failed.
InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error)
InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error)
InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error
InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error
+77
View File
@@ -40,6 +40,21 @@ FROM payments p
LEFT JOIN payment_intents i ON i.payment_id = p.id LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE p.payment_identifier = $1; WHERE p.payment_identifier = $1;
-- name: FetchPaymentDuplicates :many
-- Fetch all duplicate payment records from the payment_duplicates table for
-- a given payment ID.
SELECT
id,
payment_id,
amount_msat,
created_at,
fail_reason,
settle_preimage,
settle_time
FROM payment_duplicates
WHERE payment_id = $1
ORDER BY id ASC;
-- name: CountPayments :one -- name: CountPayments :one
SELECT COUNT(*) FROM payments; SELECT COUNT(*) FROM payments;
@@ -368,3 +383,65 @@ VALUES (
-- name: FailPayment :execresult -- name: FailPayment :execresult
UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2; UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2;
/* ─────────────────────────────────────────────
Migration-specific queries
These queries are used ONLY for the one-time migration from KV to SQL.
─────────────────────────────────────────────
*/
-- name: InsertPaymentMig :one
-- Migration-specific payment insert that allows setting fail_reason.
-- Normal InsertPayment forces fail_reason to NULL since new payments
-- aren't failed yet. During migration, we're inserting historical data
-- that may already be failed.
INSERT INTO payments (
amount_msat,
created_at,
payment_identifier,
fail_reason)
VALUES (
@amount_msat,
@created_at,
@payment_identifier,
@fail_reason
)
RETURNING id;
-- name: FetchPaymentsByIDsMig :many
-- Migration-specific batch fetch that returns payment data along with HTLC
-- attempt counts for structural validation during KV to SQL migration.
SELECT
p.id,
p.amount_msat,
p.created_at,
p.payment_identifier,
p.fail_reason,
COUNT(ha.id) AS htlc_attempt_count
FROM payments p
LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id
WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/)
GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason
ORDER BY p.id ASC;
-- name: InsertPaymentDuplicateMig :one
-- Insert a duplicate payment record into the payment_duplicates table and
-- return its ID.
INSERT INTO payment_duplicates (
payment_id,
amount_msat,
created_at,
fail_reason,
settle_preimage,
settle_time
)
VALUES (
@payment_id,
@amount_msat,
@created_at,
@fail_reason,
@settle_preimage,
@settle_time
)
RETURNING id;