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"
- channeldb/migration_01_to_11
- channeldb/migration/lnwire21
- payments/db/migration1/lnwire
- payments/db/migration1/record
issues:
# Only show newly introduced problems.
new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
+58
View File
@@ -51,6 +51,8 @@ import (
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/msgmux"
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/signal"
"github.com/lightningnetwork/lnd/sqldb"
@@ -76,6 +78,10 @@ const (
// graphMigration is the version number for the graph migration
// that migrates the KV graph to the native SQL schema.
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
@@ -1153,6 +1159,31 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
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
// the correct migration version.
for i := 0; i < len(migrations); i++ {
@@ -1162,11 +1193,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
migrations[i].MigrationFn = invoiceMig
continue
case graphMigration:
migrations[i].MigrationFn = graphMig
continue
case paymentMigration:
migrations[i].MigrationFn = paymentMig
continue
default:
}
@@ -1265,6 +1302,27 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
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
graphStore, err = graphdb.NewKVStore(
@@ -217,6 +217,10 @@
db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308)
* [Finalize SQL implementation for
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
+1 -1
View File
@@ -141,7 +141,7 @@ require (
github.com/opencontainers/runc v1.1.14 // indirect
github.com/ory/dockertest/v3 v3.10.0 // 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/common v0.26.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 {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
} else {
err = tx.Commit()
}
}()
reset()
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 " +
"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.
//
+1 -1
View File
@@ -1236,7 +1236,7 @@ func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash,
seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
if seqBytes == nil {
return err
return ErrNoDuplicateSequenceNumber
}
// 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 (
"github.com/btcsuite/btclog/v2"
"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
@@ -29,4 +30,5 @@ func DisableLog() {
// using btclog.
func UseLogger(logger btclog.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)
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)
FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error)
FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error)
@@ -89,6 +91,31 @@ type SQLQueries interface {
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
// of batched database operations.
type BatchedSQLQueries interface {
+14
View File
@@ -8,4 +8,18 @@ var migrationAdditions = []MigrationConfig{
Version: 12,
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)
})
}
// 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
}
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
+203
View File
@@ -479,6 +479,53 @@ func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (F
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
SELECT
l.id,
@@ -597,6 +644,72 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([
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
SELECT
l.id,
@@ -843,6 +956,51 @@ func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context,
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
INSERT INTO payment_first_hop_custom_records (
payment_id,
@@ -918,6 +1076,51 @@ func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentInte
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
INSERT INTO payment_route_hops (
htlc_attempt_index,
+14
View File
@@ -49,11 +49,17 @@ type Querier interface {
FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error)
FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, 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)
// Batch fetch payment and intent data for a set of payment IDs.
// Used to avoid fetching redundant payment data when processing multiple
// attempts for the same payment.
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
// accepted). The invoices_state_idx index on the state column makes this a
// fast index scan rather than a full table scan.
@@ -174,10 +180,18 @@ type Querier interface {
// payment process.
InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, 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
InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error
// Insert a payment intent for a given payment and return its ID.
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)
InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) 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
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
SELECT COUNT(*) FROM payments;
@@ -368,3 +383,65 @@ VALUES (
-- name: FailPayment :execresult
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;