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