Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 74 additions & 57 deletions channeldb/control_tower.go → channeldb/payment_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
)

var (
Expand All @@ -33,48 +34,20 @@ var (
// ErrUnknownPaymentStatus is returned when we do not recognize the
// existing state of a payment.
ErrUnknownPaymentStatus = errors.New("unknown payment status")
)

// ControlTower tracks all outgoing payments made, whose primary purpose is to
// prevent duplicate payments to the same payment hash. In production, a
// persistent implementation is preferred so that tracking can survive across
// restarts. Payments are transitioned through various payment states, and the
// ControlTower interface provides access to driving the state transitions.
type ControlTower interface {
// InitPayment atomically moves the payment into the InFlight state.
// This method checks that no suceeded payment exist for this payment
// hash.
InitPayment(lntypes.Hash, *PaymentCreationInfo) error

// RegisterAttempt atomically records the provided PaymentAttemptInfo.
RegisterAttempt(lntypes.Hash, *PaymentAttemptInfo) error

// Success transitions a payment into the Succeeded state. 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.
Success(lntypes.Hash, lntypes.Preimage) error

// Fail transitions a payment into the Failed state, and records the
// reason the payment failed. After invoking this method, InitPayment
// should return nil on its next call for this payment hash, allowing
// the switch to make a subsequent payment.
Fail(lntypes.Hash, FailureReason) error

// FetchInFlightPayments returns all payments with status InFlight.
FetchInFlightPayments() ([]*InFlightPayment, error)
}
// errNoAttemptInfo is returned when no attempt info is stored yet.
errNoAttemptInfo = errors.New("unable to find attempt info for " +
"inflight payment")
)

// paymentControl is persistent implementation of ControlTower to restrict
// double payment sending.
type paymentControl struct {
// PaymentControl implements persistence for payments and payment attempts.
type PaymentControl struct {
db *DB
}

// NewPaymentControl creates a new instance of the paymentControl.
func NewPaymentControl(db *DB) ControlTower {
return &paymentControl{
// NewPaymentControl creates a new instance of the PaymentControl.
func NewPaymentControl(db *DB) *PaymentControl {
return &PaymentControl{
db: db,
}
}
Expand All @@ -83,7 +56,7 @@ func NewPaymentControl(db *DB) ControlTower {
// making sure it does not already exist as an in-flight payment. Then this
// method returns successfully, the payment is guranteeed to be in the InFlight
// state.
func (p *paymentControl) InitPayment(paymentHash lntypes.Hash,
func (p *PaymentControl) InitPayment(paymentHash lntypes.Hash,
info *PaymentCreationInfo) error {

var b bytes.Buffer
Expand Down Expand Up @@ -173,7 +146,7 @@ func (p *paymentControl) InitPayment(paymentHash lntypes.Hash,

// RegisterAttempt atomically records the provided PaymentAttemptInfo to the
// DB.
func (p *paymentControl) RegisterAttempt(paymentHash lntypes.Hash,
func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
attempt *PaymentAttemptInfo) error {

// Serialize the information before opening the db transaction.
Expand Down Expand Up @@ -218,10 +191,13 @@ func (p *paymentControl) RegisterAttempt(paymentHash lntypes.Hash,
// 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.
func (p *paymentControl) Success(paymentHash lntypes.Hash,
preimage lntypes.Preimage) error {
func (p *PaymentControl) Success(paymentHash lntypes.Hash,
preimage lntypes.Preimage) (*route.Route, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having to do this change, one could instead provinde the route as a param to the Success method in the routing ControlTower.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if that is better. It puts a new requirement on the caller of ControlTower.


var updateErr error
var (
updateErr error
route *route.Route
)
err := p.db.Batch(func(tx *bbolt.Tx) error {
// Reset the update error, to avoid carrying over an error
// from a previous execution of the batched db transaction.
Expand All @@ -243,21 +219,33 @@ func (p *paymentControl) Success(paymentHash lntypes.Hash,

// Record the successful payment info atomically to the
// payments record.
return bucket.Put(paymentSettleInfoKey, preimage[:])
err = bucket.Put(paymentSettleInfoKey, preimage[:])
if err != nil {
return err
}

// Retrieve attempt info for the notification.
attempt, err := fetchPaymentAttempt(bucket)
if err != nil {
return err
}

route = &attempt.Route

return nil
})
if err != nil {
return err
return nil, err
}

return updateErr

return route, updateErr
}

// Fail transitions a payment into the Failed state, and records the reason the
// payment failed. After invoking this method, InitPayment should return nil on
// its next call for this payment hash, allowing the switch to make a
// subsequent payment.
func (p *paymentControl) Fail(paymentHash lntypes.Hash,
func (p *PaymentControl) Fail(paymentHash lntypes.Hash,
reason FailureReason) error {

var updateErr error
Expand Down Expand Up @@ -291,6 +279,28 @@ func (p *paymentControl) Fail(paymentHash lntypes.Hash,
return updateErr
}

// FetchPayment returns information about a payment from the database.
func (p *PaymentControl) FetchPayment(paymentHash lntypes.Hash) (
*Payment, error) {

var payment *Payment
err := p.db.View(func(tx *bbolt.Tx) error {
bucket, err := fetchPaymentBucket(tx, paymentHash)
if err != nil {
return err
}

payment, err = fetchPayment(bucket)

return err
})
if err != nil {
return nil, err
}

return payment, nil
}

// createPaymentBucket creates or fetches the sub-bucket assigned to this
// payment hash.
func createPaymentBucket(tx *bbolt.Tx, paymentHash lntypes.Hash) (
Expand Down Expand Up @@ -389,6 +399,17 @@ func ensureInFlight(bucket *bbolt.Bucket) error {
}
}

// fetchPaymentAttempt fetches the payment attempt from the bucket.
func fetchPaymentAttempt(bucket *bbolt.Bucket) (*PaymentAttemptInfo, error) {
attemptData := bucket.Get(paymentAttemptInfoKey)
if attemptData == nil {
return nil, errNoAttemptInfo
}

r := bytes.NewReader(attemptData)
return deserializePaymentAttemptInfo(r)
}

// InFlightPayment is a wrapper around a payment that has status InFlight.
type InFlightPayment struct {
// Info is the PaymentCreationInfo of the in-flight payment.
Expand All @@ -402,7 +423,7 @@ type InFlightPayment struct {
}

// FetchInFlightPayments returns all payments with status InFlight.
func (p *paymentControl) FetchInFlightPayments() ([]*InFlightPayment, error) {
func (p *PaymentControl) FetchInFlightPayments() ([]*InFlightPayment, error) {
var inFlights []*InFlightPayment
err := p.db.View(func(tx *bbolt.Tx) error {
payments := tx.Bucket(paymentsRootBucket)
Expand Down Expand Up @@ -440,15 +461,11 @@ func (p *paymentControl) FetchInFlightPayments() ([]*InFlightPayment, error) {
return err
}

// Now get the attempt info, which may or may not be
// available.
attempt := bucket.Get(paymentAttemptInfoKey)
if attempt != nil {
r = bytes.NewReader(attempt)
inFlight.Attempt, err = deserializePaymentAttemptInfo(r)
if err != nil {
return err
}
// Now get the attempt info. It could be that there is
// no attempt info yet.
inFlight.Attempt, err = fetchPaymentAttempt(bucket)
Comment thread
joostjager marked this conversation as resolved.
Outdated
if err != nil && err != errNoAttemptInfo {
return err
}

inFlights = append(inFlights, inFlight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/coreos/bbolt"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
)

func initDB() (*DB, error) {
Expand Down Expand Up @@ -131,9 +132,14 @@ func TestPaymentControlSwitchFail(t *testing.T) {
)

// Verifies that status was changed to StatusSucceeded.
if err := pControl.Success(info.PaymentHash, preimg); err != nil {
var route *route.Route
route, err = pControl.Success(info.PaymentHash, preimg)
if err != nil {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
if !reflect.DeepEqual(*route, attempt.Route) {
t.Fatalf("unexpected route returned")
}

assertPaymentStatus(t, db, info.PaymentHash, StatusSucceeded)
assertPaymentInfo(t, db, info.PaymentHash, info, attempt, preimg, nil)
Expand Down Expand Up @@ -204,7 +210,7 @@ func TestPaymentControlSwitchDoubleSend(t *testing.T) {
}

// After settling, the error should be ErrAlreadyPaid.
if err := pControl.Success(info.PaymentHash, preimg); err != nil {
if _, err := pControl.Success(info.PaymentHash, preimg); err != nil {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
assertPaymentStatus(t, db, info.PaymentHash, StatusSucceeded)
Expand Down Expand Up @@ -234,7 +240,7 @@ func TestPaymentControlSuccessesWithoutInFlight(t *testing.T) {
}

// Attempt to complete the payment should fail.
err = pControl.Success(info.PaymentHash, preimg)
_, err = pControl.Success(info.PaymentHash, preimg)
if err != ErrPaymentNotInitiated {
t.Fatalf("expected ErrPaymentNotInitiated, got %v", err)
}
Expand Down Expand Up @@ -337,7 +343,7 @@ func TestPaymentControlDeleteNonInFligt(t *testing.T) {
)
} else if p.success {
// Verifies that status was changed to StatusSucceeded.
err := pControl.Success(info.PaymentHash, preimg)
_, err := pControl.Success(info.PaymentHash, preimg)
if err != nil {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
Expand Down
16 changes: 16 additions & 0 deletions channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,24 @@ const (
FailureReasonNoRoute FailureReason = 1

// TODO(halseth): cancel state.

// TODO(joostjager): Add failure reasons for:
// UnknownPaymentHash, FinalInvalidAmt, FinalInvalidCltv
// LocalLiquidityInsufficient, RemoteCapacityInsufficient.
)

// String returns a human readable FailureReason
func (r FailureReason) String() string {
switch r {
case FailureReasonTimeout:
return "timeout"
case FailureReasonNoRoute:
return "no_route"
}

return "unknown"
}

// PaymentStatus represent current status of payment
type PaymentStatus byte

Expand Down
Loading