Merge pull request #2761 from halseth/reliable-payments-router-state-machine

[reliable payments] router payment state machine
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2019-05-27 16:46:19 -07:00
commit 19fafd7a9a
24 files changed
+3569 -1269

No files matched your search

-245
View File
@@ -1,245 +0,0 @@
package htlcswitch
import (
"errors"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)
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")
// ErrPaymentNotInitiated is returned if payment wasn't initiated in
// switch.
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
// ErrPaymentAlreadyCompleted is returned in the event we attempt to
// recomplete a completed payment.
ErrPaymentAlreadyCompleted = errors.New("payment is already completed")
// 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 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.
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
// 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
// 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
}
// paymentControl is persistent implementation of ControlTower to restrict
// double payment sending.
type paymentControl struct {
strict bool
db *channeldb.DB
}
// NewPaymentControl creates a new instance of the paymentControl. The strict
// flag indicates whether the controller should require "strict" state
// transitions, which would be otherwise intolerant to older databases that may
// already have duplicate payments to the same payment hash. It should be
// enabled only after sufficient checks have been made to ensure the db does not
// contain such payments. In the meantime, non-strict mode enforces a superset
// of the state transitions that prevent additional payments to a given payment
// hash from being added.
func NewPaymentControl(strict bool, db *channeldb.DB) ControlTower {
return &paymentControl{
strict: strict,
db: db,
}
}
// 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 {
var takeoffErr error
err := p.db.Batch(func(tx *bbolt.Tx) error {
// Retrieve current status of payment from local database.
paymentStatus, err := channeldb.FetchPaymentStatusTx(
tx, htlc.PaymentHash,
)
if err != nil {
return err
}
// Reset the takeoff error, to avoid carrying over an error
// from a previous execution of the batched db transaction.
takeoffErr = nil
switch paymentStatus {
case channeldb.StatusGrounded:
// It is safe to reattempt a payment if we know that we
// haven't left one in flight. Since this one is
// grounded, Transition the payment status to InFlight
// to prevent others.
return channeldb.UpdatePaymentStatusTx(
tx, htlc.PaymentHash, channeldb.StatusInFlight,
)
case channeldb.StatusInFlight:
// We already have an InFlight payment on the network. We will
// disallow any more payment until a response is received.
takeoffErr = ErrPaymentInFlight
case channeldb.StatusCompleted:
// We've already completed a payment to this payment hash,
// forbid the switch from sending another.
takeoffErr = ErrAlreadyPaid
default:
takeoffErr = ErrUnknownPaymentStatus
}
return nil
})
if err != nil {
return err
}
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 {
var updateErr error
err := p.db.Batch(func(tx *bbolt.Tx) error {
paymentStatus, err := channeldb.FetchPaymentStatusTx(
tx, paymentHash,
)
if err != nil {
return err
}
// Reset the update error, to avoid carrying over an error
// from a previous execution of the batched db transaction.
updateErr = nil
switch {
case paymentStatus == channeldb.StatusGrounded && p.strict:
// Our records show the payment as still being grounded,
// meaning it never should have left the switch.
updateErr = ErrPaymentNotInitiated
case paymentStatus == channeldb.StatusGrounded && !p.strict:
// Though our records show the payment as still being
// grounded, meaning it never should have left the
// switch, we permit this transition in non-strict mode
// to handle inconsistent db states.
fallthrough
case paymentStatus == channeldb.StatusInFlight:
// A successful response was received for an InFlight
// payment, mark it as completed to prevent sending to
// this payment hash again.
return channeldb.UpdatePaymentStatusTx(
tx, paymentHash, channeldb.StatusCompleted,
)
case paymentStatus == channeldb.StatusCompleted:
// The payment was completed previously, alert the
// caller that this may be a duplicate call.
updateErr = ErrPaymentAlreadyCompleted
default:
updateErr = ErrUnknownPaymentStatus
}
return nil
})
if err != nil {
return err
}
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 {
var updateErr error
err := p.db.Batch(func(tx *bbolt.Tx) error {
paymentStatus, err := channeldb.FetchPaymentStatusTx(
tx, paymentHash,
)
if err != nil {
return err
}
// Reset the update error, to avoid carrying over an error
// from a previous execution of the batched db transaction.
updateErr = nil
switch {
case paymentStatus == channeldb.StatusGrounded && p.strict:
// Our records show the payment as still being grounded,
// meaning it never should have left the switch.
updateErr = ErrPaymentNotInitiated
case paymentStatus == channeldb.StatusGrounded && !p.strict:
// Though our records show the payment as still being
// grounded, meaning it never should have left the
// switch, we permit this transition in non-strict mode
// to handle inconsistent db states.
fallthrough
case paymentStatus == channeldb.StatusInFlight:
// A failed response was received for an InFlight
// payment, mark it as Grounded again to allow
// subsequent attempts.
return channeldb.UpdatePaymentStatusTx(
tx, paymentHash, channeldb.StatusGrounded,
)
case paymentStatus == channeldb.StatusCompleted:
// The payment was completed previously, and we are now
// reporting that it has failed. Leave the status as
// completed, but alert the user that something is
// wrong.
updateErr = ErrPaymentAlreadyCompleted
default:
updateErr = ErrUnknownPaymentStatus
}
return nil
})
if err != nil {
return err
}
return updateErr
}
-351
View File
@@ -1,351 +0,0 @@
package htlcswitch
import (
"fmt"
"testing"
"github.com/btcsuite/fastsha256"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)
func genHtlc() (*lnwire.UpdateAddHTLC, error) {
preimage, err := genPreimage()
if err != nil {
return nil, fmt.Errorf("unable to generate preimage: %v", err)
}
rhash := fastsha256.Sum256(preimage[:])
htlc := &lnwire.UpdateAddHTLC{
PaymentHash: rhash,
Amount: 1,
}
return htlc, nil
}
type paymentControlTestCase func(*testing.T, bool)
var paymentControlTests = []struct {
name string
strict bool
testcase paymentControlTestCase
}{
{
name: "fail-strict",
strict: true,
testcase: testPaymentControlSwitchFail,
},
{
name: "double-send-strict",
strict: true,
testcase: testPaymentControlSwitchDoubleSend,
},
{
name: "double-pay-strict",
strict: true,
testcase: testPaymentControlSwitchDoublePay,
},
{
name: "fail-not-strict",
strict: false,
testcase: testPaymentControlSwitchFail,
},
{
name: "double-send-not-strict",
strict: false,
testcase: testPaymentControlSwitchDoubleSend,
},
{
name: "double-pay-not-strict",
strict: false,
testcase: testPaymentControlSwitchDoublePay,
},
}
// TestPaymentControls runs a set of common tests against both the strict and
// non-strict payment control instances. This ensures that the two both behave
// identically when making the expected state-transitions of the stricter
// implementation. Behavioral differences in the strict and non-strict
// implementations are tested separately.
func TestPaymentControls(t *testing.T) {
for _, test := range paymentControlTests {
t.Run(test.name, func(t *testing.T) {
test.testcase(t, test.strict)
})
}
}
// testPaymentControlSwitchFail checks that payment status returns to Grounded
// status after failing, and that ClearForTakeoff allows another HTLC for the
// same payment hash.
func testPaymentControlSwitchFail(t *testing.T, strict bool) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(strict, db)
htlc, err := genHtlc()
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 {
t.Fatalf("unable to send htlc message: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusInFlight)
// Fail the payment, which should moved it to Grounded.
if err := pControl.Fail(htlc.PaymentHash); err != nil {
t.Fatalf("unable to fail payment hash: %v", err)
}
// Verify the status is indeed Grounded.
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusGrounded)
// Sends the htlc again, which should succeed since the prior payment
// failed.
if err := pControl.ClearForTakeoff(htlc); err != nil {
t.Fatalf("unable to send htlc message: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusInFlight)
// Verifies that status was changed to StatusCompleted.
if err := pControl.Success(htlc.PaymentHash); err != nil {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusCompleted)
// Attempt a final payment, which should now fail since the prior
// payment succeed.
if err := pControl.ClearForTakeoff(htlc); err != ErrAlreadyPaid {
t.Fatalf("unable to send htlc message: %v", err)
}
}
// testPaymentControlSwitchDoubleSend checks the ability of payment control to
// prevent double sending of htlc message, when message is in StatusInFlight.
func testPaymentControlSwitchDoubleSend(t *testing.T, strict bool) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(strict, db)
htlc, err := genHtlc()
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 {
t.Fatalf("unable to send htlc message: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.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 {
t.Fatalf("payment control wrong behaviour: " +
"double sending must trigger ErrPaymentInFlight error")
}
}
// TestPaymentControlSwitchDoublePay checks the ability of payment control to
// prevent double payment.
func testPaymentControlSwitchDoublePay(t *testing.T, strict bool) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(strict, db)
htlc, err := genHtlc()
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 {
t.Fatalf("unable to send htlc message: %v", err)
}
// Verify that payment is InFlight.
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusInFlight)
// Move payment to completed status, second payment should return error.
if err := pControl.Success(htlc.PaymentHash); err != nil {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
// Verify that payment is Completed.
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusCompleted)
if err := pControl.ClearForTakeoff(htlc); err != ErrAlreadyPaid {
t.Fatalf("payment control wrong behaviour:" +
" double payment must trigger ErrAlreadyPaid")
}
}
// TestPaymentControlNonStrictSuccessesWithoutInFlight checks that a non-strict
// payment control will allow calls to Success when no payment is in flight. This
// is necessary to gracefully handle the case in which the switch already sent
// out a payment for a particular payment hash in a prior db version that didn't
// have payment statuses.
func TestPaymentControlNonStrictSuccessesWithoutInFlight(t *testing.T) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(false, db)
htlc, err := genHtlc()
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
if err := pControl.Success(htlc.PaymentHash); err != nil {
t.Fatalf("unable to mark payment hash success: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusCompleted)
err = pControl.Success(htlc.PaymentHash)
if err != ErrPaymentAlreadyCompleted {
t.Fatalf("unable to remark payment hash failed: %v", err)
}
}
// TestPaymentControlNonStrictFailsWithoutInFlight checks that a non-strict
// payment control will allow calls to Fail when no payment is in flight. This
// is necessary to gracefully handle the case in which the switch already sent
// out a payment for a particular payment hash in a prior db version that didn't
// have payment statuses.
func TestPaymentControlNonStrictFailsWithoutInFlight(t *testing.T) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(false, db)
htlc, err := genHtlc()
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
if err := pControl.Fail(htlc.PaymentHash); err != nil {
t.Fatalf("unable to mark payment hash failed: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusGrounded)
err = pControl.Fail(htlc.PaymentHash)
if err != nil {
t.Fatalf("unable to remark payment hash failed: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusGrounded)
err = pControl.Success(htlc.PaymentHash)
if err != nil {
t.Fatalf("unable to remark payment hash success: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusCompleted)
err = pControl.Fail(htlc.PaymentHash)
if err != ErrPaymentAlreadyCompleted {
t.Fatalf("unable to remark payment hash failed: %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusCompleted)
}
// TestPaymentControlStrictSuccessesWithoutInFlight checks that a strict payment
// control will disallow calls to Success when no payment is in flight.
func TestPaymentControlStrictSuccessesWithoutInFlight(t *testing.T) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(true, db)
htlc, err := genHtlc()
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
err = pControl.Success(htlc.PaymentHash)
if err != ErrPaymentNotInitiated {
t.Fatalf("expected ErrPaymentNotInitiated, got %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusGrounded)
}
// TestPaymentControlStrictFailsWithoutInFlight checks that a strict payment
// control will disallow calls to Fail when no payment is in flight.
func TestPaymentControlStrictFailsWithoutInFlight(t *testing.T) {
t.Parallel()
db, err := initDB()
if err != nil {
t.Fatalf("unable to init db: %v", err)
}
pControl := NewPaymentControl(true, db)
htlc, err := genHtlc()
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
err = pControl.Fail(htlc.PaymentHash)
if err != ErrPaymentNotInitiated {
t.Fatalf("expected ErrPaymentNotInitiated, got %v", err)
}
assertPaymentStatus(t, db, htlc.PaymentHash, channeldb.StatusGrounded)
}
func assertPaymentStatus(t *testing.T, db *channeldb.DB,
hash [32]byte, expStatus channeldb.PaymentStatus) {
t.Helper()
pStatus, err := db.FetchPaymentStatus(hash)
if err != nil {
t.Fatalf("unable to fetch payment status: %v", err)
}
if pStatus != expStatus {
t.Fatalf("payment status mismatch: expected %v, got %v",
expStatus, pStatus)
}
}
+11 -11
View File
@@ -3889,8 +3889,7 @@ func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
}
// With the invoice now added to Carol's registry, we'll send the
// payment. It should succeed w/o any issues as it has been crafted
// properly.
// payment.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
@@ -3905,6 +3904,16 @@ func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
t.Fatalf("unable to get payment result: %v", err)
}
// Now, if we attempt to send the payment *again* it should be rejected
// as it's a duplicate request.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
if err != ErrPaymentIDAlreadyExists {
t.Fatalf("ErrPaymentIDAlreadyExists should have been "+
"received got: %v", err)
}
select {
case result, ok := <-resultChan:
if !ok {
@@ -3917,15 +3926,6 @@ func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
case <-time.After(5 * time.Second):
t.Fatalf("payment result did not arrive")
}
// Now, if we attempt to send the payment *again* it should be rejected
// as it's a duplicate request.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
if err != ErrAlreadyPaid {
t.Fatalf("ErrAlreadyPaid should have been received got: %v", err)
}
}
// TestChannelLinkAcceptOverpay tests that if we create an invoice for sender,
-32
View File
@@ -208,9 +208,6 @@ type Switch struct {
pendingPayments map[uint64]*pendingPayment
pendingMutex sync.RWMutex
// control provides verification of sending htlc mesages
control ControlTower
// circuits is storage for payment circuits which are used to
// forward the settle/fail htlc updates back to the add htlc initiator.
circuits CircuitMap
@@ -290,7 +287,6 @@ func New(cfg Config, currentHeight uint32) (*Switch, error) {
bestHeight: currentHeight,
cfg: &cfg,
circuits: circuitMap,
control: NewPaymentControl(false, cfg.DB),
linkIndex: make(map[lnwire.ChannelID]ChannelLink),
mailOrchestrator: newMailOrchestrator(),
forwardingIndex: make(map[lnwire.ShortChannelID]ChannelLink),
@@ -402,13 +398,6 @@ func (s *Switch) GetPaymentResult(paymentID uint64,
func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, paymentID uint64,
htlc *lnwire.UpdateAddHTLC) error {
// Before sending, double check that we don't already have 1) an
// in-flight payment to this payment hash, or 2) a complete payment for
// the same hash.
if err := s.control.ClearForTakeoff(htlc); err != nil {
return err
}
// Create payment and add to the map of payment in order later to be
// able to retrieve it and return response to the user.
payment := &pendingPayment{
@@ -439,10 +428,6 @@ func (s *Switch) SendHTLC(firstHop lnwire.ShortChannelID, paymentID uint64,
if err := s.forward(packet); err != nil {
s.removePendingPayment(paymentID)
if err := s.control.Fail(htlc.PaymentHash); err != nil {
return err
}
return err
}
@@ -939,15 +924,6 @@ func (s *Switch) extractResult(deobfuscator ErrorDecrypter, n *networkResult,
// We've received a settle update which means we can finalize the user
// payment and return successful response.
case *lnwire.UpdateFulfillHTLC:
// Persistently mark that a payment to this payment hash
// succeeded. This will prevent us from ever making another
// payment to this hash.
err := s.control.Success(paymentHash)
if err != nil && err != ErrPaymentAlreadyCompleted {
return nil, fmt.Errorf("Unable to mark completed "+
"payment %x: %v", paymentHash, err)
}
return &PaymentResult{
Preimage: htlc.PaymentPreimage,
}, nil
@@ -955,14 +931,6 @@ func (s *Switch) extractResult(deobfuscator ErrorDecrypter, n *networkResult,
// We've received a fail update which means we can finalize the
// user payment and return fail response.
case *lnwire.UpdateFailHTLC:
// Persistently mark that a payment to this payment hash
// failed. This will permit us to make another attempt at a
// successful payment.
err := s.control.Fail(paymentHash)
if err != nil && err != ErrPaymentAlreadyCompleted {
return nil, fmt.Errorf("Unable to ground payment "+
"%x: %v", paymentHash, err)
}
paymentErr := s.parseFailedPayment(
deobfuscator, paymentID, paymentHash, n.unencrypted,
n.isResolution, htlc,