diff --git a/channeldb/control_tower.go b/channeldb/control_tower.go index 98de12797..b81b0f1c0 100644 --- a/channeldb/control_tower.go +++ b/channeldb/control_tower.go @@ -1,11 +1,12 @@ package channeldb import ( + "bytes" + "encoding/binary" "errors" "github.com/coreos/bbolt" "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwire" ) var ( @@ -33,29 +34,31 @@ var ( ErrUnknownPaymentStatus = errors.New("unknown payment status") ) -// ControlTower tracks all outgoing payments made by the switch, 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 transition through various payment -// states, and the ControlTower interface provides access to driving the state -// transitions. +// 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 { - // ClearForTakeoff atomically checks that no inflight or completed - // payments exist for this payment hash. If none are found, this method - // atomically transitions the status for this payment hash as InFlight. - ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error + // InitPayment atomically moves the payment into the InFlight state. + // This method checks that no completed payment exist for this payment + // hash. + InitPayment(lntypes.Hash, *PaymentCreationInfo) error - // Success transitions an InFlight payment into a Completed payment. - // After invoking this method, ClearForTakeoff should always return an - // error to prevent us from making duplicate payments to the same - // payment hash. - Success(paymentHash [32]byte) error + // RegisterAttempt atomically records the provided PaymentAttemptInfo. + RegisterAttempt(lntypes.Hash, *PaymentAttemptInfo) error - // Fail transitions an InFlight payment into a Grounded Payment. After - // invoking this method, ClearForTakeoff should return nil on its next - // call for this payment hash, allowing the switch to make a subsequent - // payment. - Fail(paymentHash [32]byte) error + // Success transitions a payment into the Completed 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. 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) error } // paymentControl is persistent implementation of ControlTower to restrict @@ -71,12 +74,22 @@ func NewPaymentControl(db *DB) ControlTower { } } -// ClearForTakeoff checks that we don't already have an InFlight or Completed -// payment identified by the same payment hash. -func (p *paymentControl) ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error { +// InitPayment checks or records the given PaymentCreationInfo with the DB, +// 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, + info *PaymentCreationInfo) error { + + var b bytes.Buffer + if err := serializePaymentCreationInfo(&b, info); err != nil { + return err + } + infoBytes := b.Bytes() + var takeoffErr error err := p.db.Batch(func(tx *bbolt.Tx) error { - bucket, err := fetchPaymentBucket(tx, htlc.PaymentHash) + bucket, err := fetchPaymentBucket(tx, paymentHash) if err != nil { return err } @@ -92,41 +105,118 @@ func (p *paymentControl) ClearForTakeoff(htlc *lnwire.UpdateAddHTLC) error { // We allow retrying failed payments. case StatusFailed: - fallthrough - // It is safe to reattempt a payment if we know that we haven't - // left one in flight. Since this one is grounded or failed, - // transition the payment status to InFlight to prevent others. + // This is a new payment that is being initialized for the + // first time. case StatusGrounded: - return bucket.Put(paymentStatusKey, StatusInFlight.Bytes()) // We already have an InFlight payment on the network. We will - // disallow any more payment until a response is received. + // disallow any new payments. case StatusInFlight: takeoffErr = ErrPaymentInFlight + return nil // We've already completed a payment to this payment hash, // forbid the switch from sending another. case StatusCompleted: takeoffErr = ErrAlreadyPaid + return nil default: takeoffErr = ErrUnknownPaymentStatus + return nil + } + + // Obtain a new sequence number for this payment. This is used + // to sort the payments in order of creation, and also acts as + // a unique identifier for each payment. + sequenceNum, err := nextPaymentSequence(tx) + if err != nil { + return err + } + + err = bucket.Put(paymentSequenceKey, sequenceNum) + if err != nil { + return err + } + + // We'll move it into the InFlight state. + err = bucket.Put(paymentStatusKey, StatusInFlight.Bytes()) + if err != nil { + return err + } + + // Add the payment info to the bucket, which contains the + // static information for this payment + err = bucket.Put(paymentCreationInfoKey, infoBytes) + if err != nil { + return err + } + + // We'll delete any lingering attempt info to start with, in + // case we are initializing a payment that was attempted + // earlier, but left in a state where we could retry. + err = bucket.Delete(paymentAttemptInfoKey) + if err != nil { + return err } return nil }) if err != nil { - return err + return nil } return takeoffErr } -// Success transitions an InFlight payment to Completed, otherwise it returns an -// error. After calling Success, ClearForTakeoff should prevent any further -// attempts for the same payment hash. -func (p *paymentControl) Success(paymentHash [32]byte) error { +// RegisterAttempt atomically records the provided PaymentAttemptInfo to the +// DB. +func (p *paymentControl) RegisterAttempt(paymentHash lntypes.Hash, + attempt *PaymentAttemptInfo) error { + + // Serialize the information before opening the db transaction. + var a bytes.Buffer + if err := serializePaymentAttemptInfo(&a, attempt); err != nil { + return err + } + attemptBytes := a.Bytes() + + var updateErr error + 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. + updateErr = nil + + bucket, err := fetchPaymentBucket(tx, paymentHash) + if err != nil { + return err + } + + // We can only register attempts for payments that are + // in-flight. + if err := ensureInFlight(bucket); err != nil { + updateErr = err + return nil + } + + // Add the payment attempt to the payments bucket. + return bucket.Put(paymentAttemptInfoKey, attemptBytes) + }) + if err != nil { + return err + } + + return updateErr +} + +// Success transitions a payment into the Completed 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. +func (p *paymentControl) Success(paymentHash lntypes.Hash, + preimage lntypes.Preimage) error { + var updateErr error err := p.db.Batch(func(tx *bbolt.Tx) error { // Reset the update error, to avoid carrying over an error @@ -144,6 +234,13 @@ func (p *paymentControl) Success(paymentHash [32]byte) error { return nil } + // Record the successful payment info atomically to the + // payments record. + err = bucket.Put(paymentSettleInfoKey, preimage[:]) + if err != nil { + return err + } + return bucket.Put(paymentStatusKey, StatusCompleted.Bytes()) }) if err != nil { @@ -151,12 +248,13 @@ func (p *paymentControl) Success(paymentHash [32]byte) error { } return updateErr + } -// Fail transitions an InFlight payment to Grounded, otherwise it returns an -// error. After calling Fail, ClearForTakeoff should fail any further attempts -// for the same payment hash. -func (p *paymentControl) Fail(paymentHash [32]byte) error { +// Fail transitions a payment into the Failed state. 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) error { var updateErr error err := p.db.Batch(func(tx *bbolt.Tx) error { // Reset the update error, to avoid carrying over an error @@ -174,7 +272,9 @@ func (p *paymentControl) Fail(paymentHash [32]byte) error { return nil } - return bucket.Put(paymentStatusKey, StatusGrounded.Bytes()) + // A failed response was received for an InFlight payment, mark + // it as Failed to allow subsequent attempts. + return bucket.Put(paymentStatusKey, StatusFailed.Bytes()) }) if err != nil { return err @@ -196,6 +296,24 @@ func fetchPaymentBucket(tx *bbolt.Tx, paymentHash lntypes.Hash) ( return payments.CreateBucketIfNotExists(paymentHash[:]) } +// nextPaymentSequence returns the next sequence number to store for a new +// payment. +func nextPaymentSequence(tx *bbolt.Tx) ([]byte, error) { + payments, err := tx.CreateBucketIfNotExists(paymentsRootBucket) + if err != nil { + return nil, err + } + + seq, err := payments.NextSequence() + if err != nil { + return nil, err + } + + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, seq) + return b, nil +} + // fetchPaymentStatus fetches the payment status from the bucket. If the // status isn't found, it will default to "StatusGrounded". func fetchPaymentStatus(bucket *bbolt.Bucket) PaymentStatus { diff --git a/channeldb/control_tower_test.go b/channeldb/control_tower_test.go index a70e3d11e..2d6e71d63 100644 --- a/channeldb/control_tower_test.go +++ b/channeldb/control_tower_test.go @@ -6,10 +6,11 @@ import ( "io" "io/ioutil" "testing" + "time" "github.com/btcsuite/fastsha256" "github.com/coreos/bbolt" - "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/lntypes" ) func initDB() (*DB, error) { @@ -34,19 +35,27 @@ func genPreimage() ([32]byte, error) { return preimage, nil } -func genHtlc() (*lnwire.UpdateAddHTLC, error) { +func genInfo() (*PaymentCreationInfo, *PaymentAttemptInfo, + lntypes.Preimage, error) { + preimage, err := genPreimage() if err != nil { - return nil, fmt.Errorf("unable to generate preimage: %v", err) + return nil, nil, preimage, fmt.Errorf("unable to "+ + "generate preimage: %v", err) } rhash := fastsha256.Sum256(preimage[:]) - htlc := &lnwire.UpdateAddHTLC{ - PaymentHash: rhash, - Amount: 1, - } - - return htlc, nil + return &PaymentCreationInfo{ + PaymentHash: rhash, + Value: 1, + CreationDate: time.Unix(time.Now().Unix(), 0), + PaymentRequest: []byte("hola"), + }, + &PaymentAttemptInfo{ + PaymentID: 1, + SessionKey: priv, + Route: testRoute, + }, preimage, nil } type paymentControlTestCase func(*testing.T) @@ -95,44 +104,47 @@ func testPaymentControlSwitchFail(t *testing.T) { pControl := NewPaymentControl(db) - htlc, err := genHtlc() + info, _, preimg, err := genInfo() if err != nil { t.Fatalf("unable to generate htlc message: %v", err) } // Sends base htlc message which initiate StatusInFlight. - if err := pControl.ClearForTakeoff(htlc); err != nil { + err = pControl.InitPayment(info.PaymentHash, info) + if err != nil { t.Fatalf("unable to send htlc message: %v", err) } - assertPaymentStatus(t, db, htlc.PaymentHash, StatusInFlight) + assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight) // Fail the payment, which should moved it to Grounded. - if err := pControl.Fail(htlc.PaymentHash); err != nil { + if err := pControl.Fail(info.PaymentHash); err != nil { t.Fatalf("unable to fail payment hash: %v", err) } - // Verify the status is indeed Grounded. - assertPaymentStatus(t, db, htlc.PaymentHash, StatusGrounded) + // Verify the status is indeed Failed. + assertPaymentStatus(t, db, info.PaymentHash, StatusFailed) // Sends the htlc again, which should succeed since the prior payment // failed. - if err := pControl.ClearForTakeoff(htlc); err != nil { + err = pControl.InitPayment(info.PaymentHash, info) + if err != nil { t.Fatalf("unable to send htlc message: %v", err) } - assertPaymentStatus(t, db, htlc.PaymentHash, StatusInFlight) + assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight) // Verifies that status was changed to StatusCompleted. - if err := pControl.Success(htlc.PaymentHash); 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, htlc.PaymentHash, StatusCompleted) + assertPaymentStatus(t, db, info.PaymentHash, StatusCompleted) // Attempt a final payment, which should now fail since the prior // payment succeed. - if err := pControl.ClearForTakeoff(htlc); err != ErrAlreadyPaid { + err = pControl.InitPayment(info.PaymentHash, info) + if err != ErrAlreadyPaid { t.Fatalf("unable to send htlc message: %v", err) } } @@ -149,23 +161,25 @@ func testPaymentControlSwitchDoubleSend(t *testing.T) { pControl := NewPaymentControl(db) - htlc, err := genHtlc() + info, _, _, err := genInfo() if err != nil { t.Fatalf("unable to generate htlc message: %v", err) } // Sends base htlc message which initiate base status and move it to // StatusInFlight and verifies that it was changed. - if err := pControl.ClearForTakeoff(htlc); err != nil { + err = pControl.InitPayment(info.PaymentHash, info) + if err != nil { t.Fatalf("unable to send htlc message: %v", err) } - assertPaymentStatus(t, db, htlc.PaymentHash, StatusInFlight) + assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight) // Try to initiate double sending of htlc message with the same // payment hash, should result in error indicating that payment has // already been sent. - if err := pControl.ClearForTakeoff(htlc); err != ErrPaymentInFlight { + err = pControl.InitPayment(info.PaymentHash, info) + if err != ErrPaymentInFlight { t.Fatalf("payment control wrong behaviour: " + "double sending must trigger ErrPaymentInFlight error") } @@ -183,28 +197,31 @@ func testPaymentControlSwitchDoublePay(t *testing.T) { pControl := NewPaymentControl(db) - htlc, err := genHtlc() + info, _, preimg, err := genInfo() if err != nil { t.Fatalf("unable to generate htlc message: %v", err) } // Sends base htlc message which initiate StatusInFlight. - if err := pControl.ClearForTakeoff(htlc); err != nil { + err = pControl.InitPayment(info.PaymentHash, info) + if err != nil { t.Fatalf("unable to send htlc message: %v", err) } // Verify that payment is InFlight. - assertPaymentStatus(t, db, htlc.PaymentHash, StatusInFlight) + assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight) // Move payment to completed status, second payment should return error. - if err := pControl.Success(htlc.PaymentHash); err != nil { + err = pControl.Success(info.PaymentHash, preimg) + if err != nil { t.Fatalf("error shouldn't have been received, got: %v", err) } // Verify that payment is Completed. - assertPaymentStatus(t, db, htlc.PaymentHash, StatusCompleted) + assertPaymentStatus(t, db, info.PaymentHash, StatusCompleted) - if err := pControl.ClearForTakeoff(htlc); err != ErrAlreadyPaid { + err = pControl.InitPayment(info.PaymentHash, info) + if err != ErrAlreadyPaid { t.Fatalf("payment control wrong behaviour:" + " double payment must trigger ErrAlreadyPaid") } @@ -222,17 +239,17 @@ func TestPaymentControlStrictSuccessesWithoutInFlight(t *testing.T) { pControl := NewPaymentControl(db) - htlc, err := genHtlc() + info, _, preimg, err := genInfo() if err != nil { t.Fatalf("unable to generate htlc message: %v", err) } - err = pControl.Success(htlc.PaymentHash) + err = pControl.Success(info.PaymentHash, preimg) if err != ErrPaymentNotInitiated { t.Fatalf("expected ErrPaymentNotInitiated, got %v", err) } - assertPaymentStatus(t, db, htlc.PaymentHash, StatusGrounded) + assertPaymentStatus(t, db, info.PaymentHash, StatusGrounded) } // TestPaymentControlStrictFailsWithoutInFlight checks that a strict payment @@ -247,17 +264,17 @@ func TestPaymentControlStrictFailsWithoutInFlight(t *testing.T) { pControl := NewPaymentControl(db) - htlc, err := genHtlc() + info, _, _, err := genInfo() if err != nil { t.Fatalf("unable to generate htlc message: %v", err) } - err = pControl.Fail(htlc.PaymentHash) + err = pControl.Fail(info.PaymentHash) if err != ErrPaymentNotInitiated { t.Fatalf("expected ErrPaymentNotInitiated, got %v", err) } - assertPaymentStatus(t, db, htlc.PaymentHash, StatusGrounded) + assertPaymentStatus(t, db, info.PaymentHash, StatusGrounded) } func assertPaymentStatus(t *testing.T, db *DB,