Merge pull request #10485 from ziggie1984/migration-kvdb-sql-payments-part1
payments: kv-to-sql migration (tests + wiring)
This commit is contained in:
66 files changed
+19914
-11
No files matched your search
+3
-1
@@ -302,7 +302,9 @@ linters:
|
|||||||
- "internal\\/musig2v040"
|
- "internal\\/musig2v040"
|
||||||
- channeldb/migration_01_to_11
|
- channeldb/migration_01_to_11
|
||||||
- channeldb/migration/lnwire21
|
- channeldb/migration/lnwire21
|
||||||
|
- payments/db/migration1/lnwire
|
||||||
|
- payments/db/migration1/record
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
# Only show newly introduced problems.
|
# Only show newly introduced problems.
|
||||||
new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
|
new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/lightningnetwork/lnd/msgmux"
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
|
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
|
||||||
|
paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
|
||||||
|
paymentsmig1sqlc "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
|
||||||
"github.com/lightningnetwork/lnd/rpcperms"
|
"github.com/lightningnetwork/lnd/rpcperms"
|
||||||
"github.com/lightningnetwork/lnd/signal"
|
"github.com/lightningnetwork/lnd/signal"
|
||||||
"github.com/lightningnetwork/lnd/sqldb"
|
"github.com/lightningnetwork/lnd/sqldb"
|
||||||
@@ -76,6 +78,10 @@ const (
|
|||||||
// graphMigration is the version number for the graph migration
|
// graphMigration is the version number for the graph migration
|
||||||
// that migrates the KV graph to the native SQL schema.
|
// that migrates the KV graph to the native SQL schema.
|
||||||
graphMigration = 10
|
graphMigration = 10
|
||||||
|
|
||||||
|
// paymentMigration is the version number for the payments migration
|
||||||
|
// that migrates KV payments to the native SQL schema.
|
||||||
|
paymentMigration = 14
|
||||||
)
|
)
|
||||||
|
|
||||||
// GrpcRegistrar is an interface that must be satisfied by an external subserver
|
// GrpcRegistrar is an interface that must be satisfied by an external subserver
|
||||||
@@ -1153,6 +1159,31 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paymentMig := func(tx *sqlc.Queries) error {
|
||||||
|
err := paymentsmig1.MigratePaymentsKVToSQL(
|
||||||
|
ctx,
|
||||||
|
dbs.ChanStateDB.Backend,
|
||||||
|
paymentsmig1sqlc.New(tx.GetTx()),
|
||||||
|
&paymentsmig1.SQLStoreConfig{
|
||||||
|
QueryCfg: queryCfg,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate "+
|
||||||
|
"payments to SQL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the payments bucket tombstone to
|
||||||
|
// indicate that the migration has been
|
||||||
|
// completed.
|
||||||
|
d.logger.Debugf("Setting payments bucket " +
|
||||||
|
"tombstone")
|
||||||
|
|
||||||
|
return paymentsdb.SetPaymentsBucketTombstone(
|
||||||
|
dbs.ChanStateDB.Backend,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we attach the custom migration function to
|
// Make sure we attach the custom migration function to
|
||||||
// the correct migration version.
|
// the correct migration version.
|
||||||
for i := 0; i < len(migrations); i++ {
|
for i := 0; i < len(migrations); i++ {
|
||||||
@@ -1162,11 +1193,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
|
|||||||
migrations[i].MigrationFn = invoiceMig
|
migrations[i].MigrationFn = invoiceMig
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case graphMigration:
|
case graphMigration:
|
||||||
migrations[i].MigrationFn = graphMig
|
migrations[i].MigrationFn = graphMig
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
case paymentMigration:
|
||||||
|
migrations[i].MigrationFn = paymentMig
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,6 +1302,27 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the payments bucket tombstone is set. If it is, we
|
||||||
|
// need to return and ask the user switch back to using the
|
||||||
|
// native SQL store.
|
||||||
|
ripPayments, err := paymentsdb.GetPaymentsBucketTombstone(
|
||||||
|
dbs.ChanStateDB.Backend,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to check payments bucket "+
|
||||||
|
"tombstone: %w", err)
|
||||||
|
d.logger.Error(err)
|
||||||
|
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if ripPayments {
|
||||||
|
err = fmt.Errorf("payments bucket tombstoned, please " +
|
||||||
|
"switch back to native SQL")
|
||||||
|
d.logger.Error(err)
|
||||||
|
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
dbs.InvoiceDB = dbs.ChanStateDB
|
dbs.InvoiceDB = dbs.ChanStateDB
|
||||||
|
|
||||||
graphStore, err = graphdb.NewKVStore(
|
graphStore, err = graphdb.NewKVStore(
|
||||||
|
|||||||
@@ -217,6 +217,10 @@
|
|||||||
db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308)
|
db functions Part 2](https://github.com/lightningnetwork/lnd/pull/10308)
|
||||||
* [Finalize SQL implementation for
|
* [Finalize SQL implementation for
|
||||||
payments db](https://github.com/lightningnetwork/lnd/pull/10373)
|
payments db](https://github.com/lightningnetwork/lnd/pull/10373)
|
||||||
|
* [Add the KV-to-SQL payment
|
||||||
|
migration](https://github.com/lightningnetwork/lnd/pull/10485) with
|
||||||
|
comprehensive tests. The migration is currently dev-only, compiled behind
|
||||||
|
the `test_db_postgres`, `test_db_sqlite`, or `test_native_sql` build tags.
|
||||||
|
|
||||||
|
|
||||||
## Code Health
|
## Code Health
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ require (
|
|||||||
github.com/opencontainers/runc v1.1.14 // indirect
|
github.com/opencontainers/runc v1.1.14 // indirect
|
||||||
github.com/ory/dockertest/v3 v3.10.0 // indirect
|
github.com/ory/dockertest/v3 v3.10.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.26.0 // indirect
|
github.com/prometheus/common v0.26.0 // indirect
|
||||||
github.com/prometheus/procfs v0.6.0 // indirect
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
|
|||||||
@@ -31,16 +31,15 @@ func (t *testBatchedSQLQueries) ExecTx(ctx context.Context,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
} else {
|
|
||||||
err = tx.Commit()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
queries := sqlc.New(tx)
|
queries := sqlc.New(tx)
|
||||||
|
|
||||||
return txBody(queries)
|
if err := txBody(queries); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,11 @@ var (
|
|||||||
ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " +
|
ErrNoDuplicateNestedBucket = errors.New("nested duplicate bucket not " +
|
||||||
"found")
|
"found")
|
||||||
|
|
||||||
|
// ErrNoDuplicateSequenceNumber is returned when a duplicate payment
|
||||||
|
// sub-bucket does not contain the sequence number key.
|
||||||
|
ErrNoDuplicateSequenceNumber = errors.New("duplicate payment " +
|
||||||
|
"sequence number not found")
|
||||||
|
|
||||||
// ErrNoSequenceNrIndex is returned when an attempt to lookup a payment
|
// ErrNoSequenceNrIndex is returned when an attempt to lookup a payment
|
||||||
// index is made for a sequence number that is not indexed.
|
// index is made for a sequence number that is not indexed.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1236,7 +1236,7 @@ func fetchPaymentWithSequenceNumber(tx kvdb.RTx, paymentHash lntypes.Hash,
|
|||||||
|
|
||||||
seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
|
seqBytes := subBucket.Get(duplicatePaymentSequenceKey)
|
||||||
if seqBytes == nil {
|
if seqBytes == nil {
|
||||||
return err
|
return ErrNoDuplicateSequenceNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this duplicate payment is not the sequence number we are
|
// If this duplicate payment is not the sequence number we are
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package paymentsdb
|
|||||||
import (
|
import (
|
||||||
"github.com/btcsuite/btclog/v2"
|
"github.com/btcsuite/btclog/v2"
|
||||||
"github.com/lightningnetwork/lnd/build"
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// log is a logger that is initialized with no output filters. This
|
// log is a logger that is initialized with no output filters. This
|
||||||
@@ -29,4 +30,5 @@ func DisableLog() {
|
|||||||
// using btclog.
|
// using btclog.
|
||||||
func UseLogger(logger btclog.Logger) {
|
func UseLogger(logger btclog.Logger) {
|
||||||
log = logger
|
log = logger
|
||||||
|
paymentsmig1.UseLogger(logger)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
)
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 &{
|
||||||
|
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
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
@@ -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.
|
||||||
@@ -57,6 +57,8 @@ type SQLQueries interface {
|
|||||||
FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error)
|
FetchAllInflightAttempts(ctx context.Context, arg sqlc.FetchAllInflightAttemptsParams) ([]sqlc.PaymentHtlcAttempt, error)
|
||||||
FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error)
|
FetchHopsForAttempts(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.FetchHopsForAttemptsRow, error)
|
||||||
|
|
||||||
|
FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]sqlc.PaymentDuplicate, error)
|
||||||
|
|
||||||
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error)
|
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIDs []int64) ([]sqlc.PaymentFirstHopCustomRecord, error)
|
||||||
FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error)
|
FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]sqlc.PaymentAttemptFirstHopCustomRecord, error)
|
||||||
FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error)
|
FetchHopLevelCustomRecords(ctx context.Context, hopIDs []int64) ([]sqlc.PaymentHopCustomRecord, error)
|
||||||
@@ -89,6 +91,31 @@ type SQLQueries interface {
|
|||||||
DeleteFailedAttempts(ctx context.Context, paymentID int64) error
|
DeleteFailedAttempts(ctx context.Context, paymentID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SQLMigrationQueries extends SQLQueries with the additional queries needed
|
||||||
|
// for the one-time migration from KV to SQL. Keeping them in a separate
|
||||||
|
// interface makes it clear which code paths are migration-only and prevents
|
||||||
|
// the regular store from accidentally depending on them.
|
||||||
|
//
|
||||||
|
//nolint:ll
|
||||||
|
type SQLMigrationQueries interface {
|
||||||
|
SQLQueries
|
||||||
|
|
||||||
|
// FetchPaymentsByIDsMig is a migration-only batch fetch that returns
|
||||||
|
// payment data along with HTLC attempt counts for structural
|
||||||
|
// validation.
|
||||||
|
FetchPaymentsByIDsMig(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsMigRow, error)
|
||||||
|
|
||||||
|
// InsertPaymentMig is a migration-only variant of InsertPayment that
|
||||||
|
// allows setting fail_reason when inserting historical payments, since
|
||||||
|
// for real payments they have not failed at creation time and so no
|
||||||
|
// failure reason would exist yet.
|
||||||
|
InsertPaymentMig(ctx context.Context, arg sqlc.InsertPaymentMigParams) (int64, error)
|
||||||
|
|
||||||
|
// InsertPaymentDuplicateMig inserts a duplicate payment record during
|
||||||
|
// migration.
|
||||||
|
InsertPaymentDuplicateMig(ctx context.Context, arg sqlc.InsertPaymentDuplicateMigParams) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
// BatchedSQLQueries is a version of the SQLQueries that's capable
|
// BatchedSQLQueries is a version of the SQLQueries that's capable
|
||||||
// of batched database operations.
|
// of batched database operations.
|
||||||
type BatchedSQLQueries interface {
|
type BatchedSQLQueries interface {
|
||||||
|
|||||||
@@ -8,4 +8,18 @@ var migrationAdditions = []MigrationConfig{
|
|||||||
Version: 12,
|
Version: 12,
|
||||||
SchemaVersion: 10,
|
SchemaVersion: 10,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "000011_payment_duplicates",
|
||||||
|
Version: 13,
|
||||||
|
SchemaVersion: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "kv_payments_migration",
|
||||||
|
Version: 14,
|
||||||
|
SchemaVersion: 11,
|
||||||
|
// A migration function may be attached to this
|
||||||
|
// migration to migrate KV payments to the native SQL
|
||||||
|
// schema. This is optional and can be disabled by the
|
||||||
|
// user if necessary.
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -616,3 +616,100 @@ func TestMigrationSucceedsAfterDirtyStateMigrationFailure19RC1(t *testing.T) {
|
|||||||
require.False(t, dirty)
|
require.False(t, dirty)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMigrationConfigConsistency verifies that the migration configuration in
|
||||||
|
// migrationConfig is consistent with the actual SQL schema files embedded in
|
||||||
|
// the binary. This catches version collisions (e.g. two migrations claiming
|
||||||
|
// the same schema version) and missing schema files.
|
||||||
|
func TestMigrationConfigConsistency(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
migrations := GetMigrations()
|
||||||
|
require.NotEmpty(t, migrations)
|
||||||
|
|
||||||
|
// Build a set of schema versions that have actual .up.sql files in
|
||||||
|
// the embedded filesystem.
|
||||||
|
embeddedFiles, err := sqlSchemas.ReadDir("sqlc/migrations")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fileSchemaVersions := make(map[int]string)
|
||||||
|
for _, f := range embeddedFiles {
|
||||||
|
if f.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var version int
|
||||||
|
_, err := fmt.Sscanf(f.Name(), "%06d_", &version)
|
||||||
|
require.NoError(t, err, "schema migration file %q is "+
|
||||||
|
"missing a valid numeric prefix (expected "+
|
||||||
|
"format: 000XXX_name.up.sql)", f.Name())
|
||||||
|
|
||||||
|
// Enforce the 6-digit zero-padded naming convention
|
||||||
|
// for consistent directory listing order.
|
||||||
|
expectedPrefix := fmt.Sprintf("%06d_", version)
|
||||||
|
require.True(t,
|
||||||
|
len(f.Name()) > len(expectedPrefix) &&
|
||||||
|
f.Name()[:len(expectedPrefix)] == expectedPrefix,
|
||||||
|
"schema migration file %q should use 6-digit "+
|
||||||
|
"zero-padded prefix %q", f.Name(),
|
||||||
|
expectedPrefix)
|
||||||
|
|
||||||
|
// Verify no two files share the same numeric prefix.
|
||||||
|
if existing, ok := fileSchemaVersions[version]; ok {
|
||||||
|
t.Fatalf("duplicate schema file version %06d: "+
|
||||||
|
"%q and %q", version, existing, f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSchemaVersions[version] = f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track seen versions to detect duplicates.
|
||||||
|
seenVersions := make(map[int]string)
|
||||||
|
seenSchemaVersions := make(map[int]string)
|
||||||
|
|
||||||
|
for _, m := range migrations {
|
||||||
|
// 1. Verify no duplicate global versions.
|
||||||
|
if existing, ok := seenVersions[m.Version]; ok {
|
||||||
|
t.Fatalf("duplicate global version %d: %q and %q",
|
||||||
|
m.Version, existing, m.Name)
|
||||||
|
}
|
||||||
|
seenVersions[m.Version] = m.Name
|
||||||
|
|
||||||
|
// 2. For schema migrations (those that advance the schema
|
||||||
|
// version), verify a corresponding .up.sql file exists
|
||||||
|
// and no two config entries claim the same schema version
|
||||||
|
// with different file prefixes.
|
||||||
|
prevSchema := 0
|
||||||
|
if m.Version > 1 {
|
||||||
|
prevSchema = migrations[m.Version-2].SchemaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// A migration advances the schema if its SchemaVersion is
|
||||||
|
// higher than the previous migration's SchemaVersion.
|
||||||
|
if m.SchemaVersion > prevSchema {
|
||||||
|
_, hasFile := fileSchemaVersions[m.SchemaVersion]
|
||||||
|
require.True(t, hasFile,
|
||||||
|
"migration %q (version %d) declares "+
|
||||||
|
"SchemaVersion=%d but no %06d_*.up.sql"+
|
||||||
|
" file exists in the embedded FS",
|
||||||
|
m.Name, m.Version, m.SchemaVersion,
|
||||||
|
m.SchemaVersion)
|
||||||
|
|
||||||
|
if existing, ok := seenSchemaVersions[m.SchemaVersion]; ok {
|
||||||
|
t.Fatalf("duplicate schema version %d: "+
|
||||||
|
"%q and %q", m.SchemaVersion,
|
||||||
|
existing, m.Name)
|
||||||
|
}
|
||||||
|
seenSchemaVersions[m.SchemaVersion] = m.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify versions are sequential starting from 1.
|
||||||
|
for i, m := range migrations {
|
||||||
|
require.Equal(t, i+1, m.Version,
|
||||||
|
"migration %q has version %d but expected %d "+
|
||||||
|
"(migrations must be sequential)",
|
||||||
|
m.Name, m.Version, i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_payment_duplicates_payment_id;
|
||||||
|
DROP TABLE IF EXISTS payment_duplicates;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- ─────────────────────────────────────────────
|
||||||
|
-- Payment Duplicate Records Table
|
||||||
|
-- ─────────────────────────────────────────────
|
||||||
|
-- Stores duplicate payment records that were created in older versions
|
||||||
|
-- of lnd. This table is intentionally minimal and is expected to be dropped
|
||||||
|
-- in the future especially if no duplicates were migrated.
|
||||||
|
-- ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS payment_duplicates (
|
||||||
|
-- Primary key for the duplicate record.
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Reference to the primary payment this duplicate belongs to.
|
||||||
|
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Amount of the duplicate payment in millisatoshis.
|
||||||
|
amount_msat BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- Timestamp when the duplicate payment was created.
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Failure reason for failed payments (if known).
|
||||||
|
fail_reason INTEGER,
|
||||||
|
|
||||||
|
-- Settlement payload for succeeded payments (if known).
|
||||||
|
settle_preimage BLOB,
|
||||||
|
|
||||||
|
-- Settlement time for succeeded payments (if known).
|
||||||
|
settle_time TIMESTAMP,
|
||||||
|
|
||||||
|
-- Ensure we record either a failure reason or settlement data.
|
||||||
|
-- During the migration if we encounter a duplicate payment that has no
|
||||||
|
-- failure reason or settlement data, we will mark it as failed. Duplicate
|
||||||
|
-- payments were a bug in older versions of LND, so we can be sure if a
|
||||||
|
-- duplicate payment has no failure reason or settlement data, the
|
||||||
|
-- corresponding HTLC has been failed.
|
||||||
|
CONSTRAINT chk_payment_duplicates_outcome
|
||||||
|
CHECK (fail_reason IS NOT NULL OR settle_preimage IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for efficient lookup by primary payment.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payment_duplicates_payment_id
|
||||||
|
ON payment_duplicates(payment_id);
|
||||||
@@ -224,6 +224,16 @@ type PaymentAttemptFirstHopCustomRecord struct {
|
|||||||
Value []byte
|
Value []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaymentDuplicate struct {
|
||||||
|
ID int64
|
||||||
|
PaymentID int64
|
||||||
|
AmountMsat int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
FailReason sql.NullInt32
|
||||||
|
SettlePreimage []byte
|
||||||
|
SettleTime sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
type PaymentFirstHopCustomRecord struct {
|
type PaymentFirstHopCustomRecord struct {
|
||||||
ID int64
|
ID int64
|
||||||
PaymentID int64
|
PaymentID int64
|
||||||
|
|||||||
@@ -479,6 +479,53 @@ func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (F
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPaymentDuplicates = `-- name: FetchPaymentDuplicates :many
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
payment_id,
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
fail_reason,
|
||||||
|
settle_preimage,
|
||||||
|
settle_time
|
||||||
|
FROM payment_duplicates
|
||||||
|
WHERE payment_id = $1
|
||||||
|
ORDER BY id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
// Fetch all duplicate payment records from the payment_duplicates table for
|
||||||
|
// a given payment ID.
|
||||||
|
func (q *Queries) FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, fetchPaymentDuplicates, paymentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []PaymentDuplicate
|
||||||
|
for rows.Next() {
|
||||||
|
var i PaymentDuplicate
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.PaymentID,
|
||||||
|
&i.AmountMsat,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.FailReason,
|
||||||
|
&i.SettlePreimage,
|
||||||
|
&i.SettleTime,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many
|
const fetchPaymentLevelFirstHopCustomRecords = `-- name: FetchPaymentLevelFirstHopCustomRecords :many
|
||||||
SELECT
|
SELECT
|
||||||
l.id,
|
l.id,
|
||||||
@@ -597,6 +644,72 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPaymentsByIDsMig = `-- name: FetchPaymentsByIDsMig :many
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.amount_msat,
|
||||||
|
p.created_at,
|
||||||
|
p.payment_identifier,
|
||||||
|
p.fail_reason,
|
||||||
|
COUNT(ha.id) AS htlc_attempt_count
|
||||||
|
FROM payments p
|
||||||
|
LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id
|
||||||
|
WHERE p.id IN (/*SLICE:payment_ids*/?)
|
||||||
|
GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason
|
||||||
|
ORDER BY p.id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type FetchPaymentsByIDsMigRow struct {
|
||||||
|
ID int64
|
||||||
|
AmountMsat int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
PaymentIdentifier []byte
|
||||||
|
FailReason sql.NullInt32
|
||||||
|
HtlcAttemptCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration-specific batch fetch that returns payment data along with HTLC
|
||||||
|
// attempt counts for structural validation during KV to SQL migration.
|
||||||
|
func (q *Queries) FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error) {
|
||||||
|
query := fetchPaymentsByIDsMig
|
||||||
|
var queryParams []interface{}
|
||||||
|
if len(paymentIds) > 0 {
|
||||||
|
for _, v := range paymentIds {
|
||||||
|
queryParams = append(queryParams, v)
|
||||||
|
}
|
||||||
|
query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1)
|
||||||
|
} else {
|
||||||
|
query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1)
|
||||||
|
}
|
||||||
|
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []FetchPaymentsByIDsMigRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i FetchPaymentsByIDsMigRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AmountMsat,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.PaymentIdentifier,
|
||||||
|
&i.FailReason,
|
||||||
|
&i.HtlcAttemptCount,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many
|
const fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many
|
||||||
SELECT
|
SELECT
|
||||||
l.id,
|
l.id,
|
||||||
@@ -843,6 +956,51 @@ func (q *Queries) InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const insertPaymentDuplicateMig = `-- name: InsertPaymentDuplicateMig :one
|
||||||
|
INSERT INTO payment_duplicates (
|
||||||
|
payment_id,
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
fail_reason,
|
||||||
|
settle_preimage,
|
||||||
|
settle_time
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertPaymentDuplicateMigParams struct {
|
||||||
|
PaymentID int64
|
||||||
|
AmountMsat int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
FailReason sql.NullInt32
|
||||||
|
SettlePreimage []byte
|
||||||
|
SettleTime sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a duplicate payment record into the payment_duplicates table and
|
||||||
|
// return its ID.
|
||||||
|
func (q *Queries) InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertPaymentDuplicateMig,
|
||||||
|
arg.PaymentID,
|
||||||
|
arg.AmountMsat,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.FailReason,
|
||||||
|
arg.SettlePreimage,
|
||||||
|
arg.SettleTime,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec
|
const insertPaymentFirstHopCustomRecord = `-- name: InsertPaymentFirstHopCustomRecord :exec
|
||||||
INSERT INTO payment_first_hop_custom_records (
|
INSERT INTO payment_first_hop_custom_records (
|
||||||
payment_id,
|
payment_id,
|
||||||
@@ -918,6 +1076,51 @@ func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentInte
|
|||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const insertPaymentMig = `-- name: InsertPaymentMig :one
|
||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
Migration-specific queries
|
||||||
|
|
||||||
|
These queries are used ONLY for the one-time migration from KV to SQL.
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
INSERT INTO payments (
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
payment_identifier,
|
||||||
|
fail_reason)
|
||||||
|
VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertPaymentMigParams struct {
|
||||||
|
AmountMsat int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
PaymentIdentifier []byte
|
||||||
|
FailReason sql.NullInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration-specific payment insert that allows setting fail_reason.
|
||||||
|
// Normal InsertPayment forces fail_reason to NULL since new payments
|
||||||
|
// aren't failed yet. During migration, we're inserting historical data
|
||||||
|
// that may already be failed.
|
||||||
|
func (q *Queries) InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertPaymentMig,
|
||||||
|
arg.AmountMsat,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.PaymentIdentifier,
|
||||||
|
arg.FailReason,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
const insertRouteHop = `-- name: InsertRouteHop :one
|
const insertRouteHop = `-- name: InsertRouteHop :one
|
||||||
INSERT INTO payment_route_hops (
|
INSERT INTO payment_route_hops (
|
||||||
htlc_attempt_index,
|
htlc_attempt_index,
|
||||||
|
|||||||
@@ -49,11 +49,17 @@ type Querier interface {
|
|||||||
FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error)
|
FetchHtlcAttemptResolutionsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptResolutionsForPaymentsRow, error)
|
||||||
FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error)
|
FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds []int64) ([]FetchHtlcAttemptsForPaymentsRow, error)
|
||||||
FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error)
|
FetchPayment(ctx context.Context, paymentIdentifier []byte) (FetchPaymentRow, error)
|
||||||
|
// Fetch all duplicate payment records from the payment_duplicates table for
|
||||||
|
// a given payment ID.
|
||||||
|
FetchPaymentDuplicates(ctx context.Context, paymentID int64) ([]PaymentDuplicate, error)
|
||||||
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error)
|
FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, paymentIds []int64) ([]PaymentFirstHopCustomRecord, error)
|
||||||
// Batch fetch payment and intent data for a set of payment IDs.
|
// Batch fetch payment and intent data for a set of payment IDs.
|
||||||
// Used to avoid fetching redundant payment data when processing multiple
|
// Used to avoid fetching redundant payment data when processing multiple
|
||||||
// attempts for the same payment.
|
// attempts for the same payment.
|
||||||
FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error)
|
FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error)
|
||||||
|
// Migration-specific batch fetch that returns payment data along with HTLC
|
||||||
|
// attempt counts for structural validation during KV to SQL migration.
|
||||||
|
FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error)
|
||||||
// FetchPendingInvoices returns all invoices in a pending state (open or
|
// FetchPendingInvoices returns all invoices in a pending state (open or
|
||||||
// accepted). The invoices_state_idx index on the state column makes this a
|
// accepted). The invoices_state_idx index on the state column makes this a
|
||||||
// fast index scan rather than a full table scan.
|
// fast index scan rather than a full table scan.
|
||||||
@@ -174,10 +180,18 @@ type Querier interface {
|
|||||||
// payment process.
|
// payment process.
|
||||||
InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error)
|
InsertPayment(ctx context.Context, arg InsertPaymentParams) (int64, error)
|
||||||
InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error
|
InsertPaymentAttemptFirstHopCustomRecord(ctx context.Context, arg InsertPaymentAttemptFirstHopCustomRecordParams) error
|
||||||
|
// Insert a duplicate payment record into the payment_duplicates table and
|
||||||
|
// return its ID.
|
||||||
|
InsertPaymentDuplicateMig(ctx context.Context, arg InsertPaymentDuplicateMigParams) (int64, error)
|
||||||
InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error
|
InsertPaymentFirstHopCustomRecord(ctx context.Context, arg InsertPaymentFirstHopCustomRecordParams) error
|
||||||
InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error
|
InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error
|
||||||
// Insert a payment intent for a given payment and return its ID.
|
// Insert a payment intent for a given payment and return its ID.
|
||||||
InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error)
|
InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error)
|
||||||
|
// Migration-specific payment insert that allows setting fail_reason.
|
||||||
|
// Normal InsertPayment forces fail_reason to NULL since new payments
|
||||||
|
// aren't failed yet. During migration, we're inserting historical data
|
||||||
|
// that may already be failed.
|
||||||
|
InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error)
|
||||||
InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error)
|
InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error)
|
||||||
InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error
|
InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error
|
||||||
InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error
|
InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error
|
||||||
|
|||||||
@@ -40,6 +40,21 @@ FROM payments p
|
|||||||
LEFT JOIN payment_intents i ON i.payment_id = p.id
|
LEFT JOIN payment_intents i ON i.payment_id = p.id
|
||||||
WHERE p.payment_identifier = $1;
|
WHERE p.payment_identifier = $1;
|
||||||
|
|
||||||
|
-- name: FetchPaymentDuplicates :many
|
||||||
|
-- Fetch all duplicate payment records from the payment_duplicates table for
|
||||||
|
-- a given payment ID.
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
payment_id,
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
fail_reason,
|
||||||
|
settle_preimage,
|
||||||
|
settle_time
|
||||||
|
FROM payment_duplicates
|
||||||
|
WHERE payment_id = $1
|
||||||
|
ORDER BY id ASC;
|
||||||
|
|
||||||
-- name: CountPayments :one
|
-- name: CountPayments :one
|
||||||
SELECT COUNT(*) FROM payments;
|
SELECT COUNT(*) FROM payments;
|
||||||
|
|
||||||
@@ -368,3 +383,65 @@ VALUES (
|
|||||||
|
|
||||||
-- name: FailPayment :execresult
|
-- name: FailPayment :execresult
|
||||||
UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2;
|
UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2;
|
||||||
|
|
||||||
|
/* ─────────────────────────────────────────────
|
||||||
|
Migration-specific queries
|
||||||
|
|
||||||
|
These queries are used ONLY for the one-time migration from KV to SQL.
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- name: InsertPaymentMig :one
|
||||||
|
-- Migration-specific payment insert that allows setting fail_reason.
|
||||||
|
-- Normal InsertPayment forces fail_reason to NULL since new payments
|
||||||
|
-- aren't failed yet. During migration, we're inserting historical data
|
||||||
|
-- that may already be failed.
|
||||||
|
INSERT INTO payments (
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
payment_identifier,
|
||||||
|
fail_reason)
|
||||||
|
VALUES (
|
||||||
|
@amount_msat,
|
||||||
|
@created_at,
|
||||||
|
@payment_identifier,
|
||||||
|
@fail_reason
|
||||||
|
)
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: FetchPaymentsByIDsMig :many
|
||||||
|
-- Migration-specific batch fetch that returns payment data along with HTLC
|
||||||
|
-- attempt counts for structural validation during KV to SQL migration.
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.amount_msat,
|
||||||
|
p.created_at,
|
||||||
|
p.payment_identifier,
|
||||||
|
p.fail_reason,
|
||||||
|
COUNT(ha.id) AS htlc_attempt_count
|
||||||
|
FROM payments p
|
||||||
|
LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id
|
||||||
|
WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/)
|
||||||
|
GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason
|
||||||
|
ORDER BY p.id ASC;
|
||||||
|
|
||||||
|
-- name: InsertPaymentDuplicateMig :one
|
||||||
|
-- Insert a duplicate payment record into the payment_duplicates table and
|
||||||
|
-- return its ID.
|
||||||
|
INSERT INTO payment_duplicates (
|
||||||
|
payment_id,
|
||||||
|
amount_msat,
|
||||||
|
created_at,
|
||||||
|
fail_reason,
|
||||||
|
settle_preimage,
|
||||||
|
settle_time
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@payment_id,
|
||||||
|
@amount_msat,
|
||||||
|
@created_at,
|
||||||
|
@fail_reason,
|
||||||
|
@settle_preimage,
|
||||||
|
@settle_time
|
||||||
|
)
|
||||||
|
RETURNING id;
|
||||||
Reference in new issue
Block a user