Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routerrpc: add option PreventSubsequentPayment to TrackPaymentV2 #9457

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
79 changes: 78 additions & 1 deletion channeldb/payment_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ var (
// a failed payment.
ErrPaymentAlreadyFailed = errors.New("payment has already failed")

// ErrAlreadyTracked signals we have already tracked this payment hash
// before the payment was initialized.
ErrAlreadyTracked = errors.New("payment has been tracked")

// ErrUnknownPaymentStatus is returned when we do not recognize the
// existing state of a payment.
ErrUnknownPaymentStatus = errors.New("unknown payment status")
Expand Down Expand Up @@ -637,6 +641,80 @@ func (p *PaymentControl) FetchPayment(paymentHash lntypes.Hash) (
return payment, nil
}

// FetchOrIndalidatePayment fetches the payment corresponding to the given
// payment hash. If the payment doesn't exist, it invalidates this payment hash
// making it impossible to create the payment in the future.
func (p *PaymentControl) FetchOrIndalidatePayment(paymentHash lntypes.Hash) (
*MPPayment, error) {

var payment *MPPayment
var notInitiated bool
if err := kvdb.Update(p.db.Backend, func(tx kvdb.RwTx) error {
paymentsBucket, err := tx.CreateTopLevelBucket(
paymentsRootBucket,
)
if err != nil {
return err
}

bucket := paymentsBucket.NestedReadWriteBucket(paymentHash[:])
if bucket == nil {
// Payment doesn't exist. Create and invalidate it.
bucket, err := paymentsBucket.CreateBucketIfNotExists(
paymentHash[:],
)
if err != nil {
return err
}

v := []byte{byte(FailureReasonTracked)}
err = bucket.Put(paymentFailInfoKey, v)
if err != nil {
return err
}

// Put something to paymentCreationInfoKey, so
// fetchPaymentStatus returns the real status
// StatusTracked, not ErrPaymentNotInitiated.
info := &PaymentCreationInfo{}
var b bytes.Buffer
err = serializePaymentCreationInfo(&b, info)
if err != nil {
return err
}
err = bucket.Put(paymentCreationInfoKey, b.Bytes())
if err != nil {
return err
}

// Put some sequence number (8 bytes) for fetchPayment
// not to fail upon reading.
err = bucket.Put(paymentSequenceKey, make([]byte, 8))
if err != nil {
return err
}

notInitiated = true

return nil
}

payment, err = fetchPayment(bucket)

return err
}, func() {
payment = nil
}); err != nil {
return nil, err
}

if notInitiated {
return nil, ErrPaymentNotInitiated
}

return payment, nil
}

// prefetchPayment attempts to prefetch as much of the payment as possible to
// reduce DB roundtrips.
func prefetchPayment(tx kvdb.RTx, paymentHash lntypes.Hash) {
Expand Down Expand Up @@ -686,7 +764,6 @@ func fetchPaymentBucket(tx kvdb.RTx, paymentHash lntypes.Hash) (
}

return bucket, nil

}

// fetchPaymentBucketUpdate is identical to fetchPaymentBucket, but it returns a
Expand Down
24 changes: 24 additions & 0 deletions channeldb/payment_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,30 @@ func TestPaymentControlSwitchFail(t *testing.T) {
}
}

// TestPaymentControlFetchOrIndalidatePayment checks that payment status moves
// to FailureReasonTracked if FetchOrIndalidatePayment is called on a payment
// hash which wasn't initialized yet.
func TestPaymentControlFetchOrIndalidatePayment(t *testing.T) {
t.Parallel()

db, err := MakeTestDB(t)
require.NoError(t, err, "unable to init db")

pControl := NewPaymentControl(db)

info, _, _, err := genInfo()
require.NoError(t, err, "unable to generate htlc message")

// Invalidate the payment by attempting to load it with the method
// FetchOrIndalidatePayment.
_, err = pControl.FetchOrIndalidatePayment(info.PaymentIdentifier)
require.ErrorIs(t, err, ErrPaymentNotInitiated)

// Sends base htlc message which initiate StatusInFlight.
err = pControl.InitPayment(info.PaymentIdentifier, info)
require.ErrorIs(t, err, ErrAlreadyTracked)
}

// TestPaymentControlSwitchDoubleSend checks the ability of payment control to
// prevent double sending of htlc message, when message is in StatusInFlight.
func TestPaymentControlSwitchDoubleSend(t *testing.T) {
Expand Down
25 changes: 25 additions & 0 deletions channeldb/payment_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const (
// StatusFailed is the status where a payment has been initiated and a
// failure result has come back.
StatusFailed PaymentStatus = 4

// StatusTracked is the status where a payment has been tracked before
// being initialized.
StatusTracked PaymentStatus = 5
)

// errPaymentStatusUnknown is returned when a payment has an unknown status.
Expand All @@ -44,6 +48,9 @@ func (ps PaymentStatus) String() string {
case StatusFailed:
return "Failed"

case StatusTracked:
return "Tracked"

default:
return "Unknown"
}
Expand Down Expand Up @@ -74,6 +81,11 @@ func (ps PaymentStatus) initializable() error {
case StatusFailed:
return nil

// The payment has been tracked before it was initialized. We don't
// allow retrying such payments.
case StatusTracked:
return ErrAlreadyTracked

default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps)
}
Expand Down Expand Up @@ -102,6 +114,10 @@ func (ps PaymentStatus) removable() error {
case StatusFailed:
return nil

// Tracked payments are allowed to be removed.
case StatusTracked:
return nil

default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps)
}
Expand All @@ -126,6 +142,9 @@ func (ps PaymentStatus) updatable() error {
case StatusFailed:
return ErrPaymentAlreadyFailed

case StatusTracked:
return ErrAlreadyTracked

default:
return fmt.Errorf("%w: %v", ErrUnknownPaymentStatus, ps)
}
Expand Down Expand Up @@ -165,6 +184,8 @@ func (ps PaymentStatus) updatable() error {
// 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.
//
// If failure reason is FailureReasonTracked, the status is StatusTracked.
func decidePaymentStatus(htlcs []HTLCAttempt,
reason *FailureReason) (PaymentStatus, error) {

Expand All @@ -178,6 +199,10 @@ func decidePaymentStatus(htlcs []HTLCAttempt,
// If we have a failure reason, the payment is failed.
if reason != nil {
paymentFailed = true

if *reason == FailureReasonTracked {
return StatusTracked, nil
}
}

// Go through all HTLCs for this payment, check whether we have any
Expand Down
18 changes: 18 additions & 0 deletions channeldb/payment_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func TestDecidePaymentStatus(t *testing.T) {
reason := FailureReasonNoRoute
failure := &reason

// Create a test failure reason for 'tracked' case and get the pointer.
reasonTracked := FailureReasonTracked
failureTracked := &reasonTracked

testCases := []struct {
name string
htlcs []HTLCAttempt
Expand Down Expand Up @@ -165,6 +169,14 @@ func TestDecidePaymentStatus(t *testing.T) {
reason: nil,
expectedStatus: StatusInitiated,
},
{
// Test when inflight=false, settled=false,
// failed=false, reason=no.
name: "tracked",
htlcs: []HTLCAttempt{},
reason: failureTracked,
expectedStatus: StatusTracked,
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -219,6 +231,12 @@ func TestPaymentStatusActions(t *testing.T) {
updateErr: ErrPaymentAlreadyFailed,
removeErr: nil,
},
{
status: StatusTracked,
initErr: ErrAlreadyTracked,
updateErr: ErrAlreadyTracked,
removeErr: nil,
},
{
status: 0,
initErr: ErrUnknownPaymentStatus,
Expand Down
8 changes: 7 additions & 1 deletion channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ const (
// user.
FailureReasonCanceled FailureReason = 5

// FailureReasonTracked indicates that the payment was tracked before
// it was initiated.
FailureReasonTracked FailureReason = 6

// TODO(joostjager): Add failure reasons for:
// LocalLiquidityInsufficient, RemoteCapacityInsufficient.
)
Expand All @@ -175,6 +179,8 @@ func (r FailureReason) String() string {
return "insufficient_balance"
case FailureReasonCanceled:
return "canceled"
case FailureReasonTracked:
return "tracked"
}

return "unknown"
Expand Down Expand Up @@ -279,7 +285,7 @@ func fetchCreationInfo(bucket kvdb.RBucket) (*PaymentCreationInfo, error) {
func fetchPayment(bucket kvdb.RBucket) (*MPPayment, error) {
seqBytes := bucket.Get(paymentSequenceKey)
if seqBytes == nil {
return nil, fmt.Errorf("sequence number not found")
return nil, ErrNoSequenceNumber
}

sequenceNum := binary.BigEndian.Uint64(seqBytes)
Expand Down
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ var allTestCases = []*lntest.TestCase{
Name: "payment failure reason canceled",
TestFunc: testPaymentFailureReasonCanceled,
},
{
Name: "tracked payment invalidates",
TestFunc: testTrackedPaymentInvalidates,
},
{
Name: "invoice update subscription",
TestFunc: testInvoiceSubscriptions,
Expand Down
99 changes: 98 additions & 1 deletion itest/lnd_payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,15 @@ func testPaymentSucceededHTLCRemoteSwept(ht *lntest.HarnessTest) {
ht.MineBlocksAndAssertNumTxes(1, 1)

// Since Alice is restarted, we need to track the payments again.
payStream := alice.RPC.TrackPaymentV2(payHash[:])
// Use PreventSubsequentPayment=true in one case to verify that it has
// no effect if TrackPaymentV2 is called on an existing payment.
payStream, err := alice.RPC.Router.TrackPaymentV2(
context.Background(), &routerrpc.TrackPaymentRequest{
PaymentHash: payHash[:],
PreventSubsequentPayment: true,
},
)
require.NoError(ht, err)
dustPayStream := alice.RPC.TrackPaymentV2(dustPayHash[:])

// Check that the dust payment is failed in both the stream and DB.
Expand Down Expand Up @@ -1067,6 +1075,95 @@ func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) {
)
}

// testTrackedPaymentInvalidates tests that if TrackPaymentV2 is called with
// PreventSubsequentPayment=true before the payment is sent, then a subsequent
// attempt to send will be blocked. Also make sure that DeletePayment undoes the
// effect of PreventSubsequentPayment.
func testTrackedPaymentInvalidates(ht *lntest.HarnessTest) {
const chanAmt = btcutil.Amount(300000)
p := lntest.OpenChannelParams{Amt: chanAmt}

// Initialize the test context with 2 connected nodes.
cfgs := [][]string{nil, nil}

// Open and wait for channels.
_, nodes := ht.CreateSimpleNetwork(cfgs, p)
alice, bob := nodes[0], nodes[1]

// Create an invoice from Bob.
invoiceResp := bob.RPC.AddInvoice(&lnrpc.Invoice{
Value: 1000,
})
payReq := invoiceResp.PaymentRequest
payHash := invoiceResp.RHash

// Call TrackPaymentV2 with PreventSubsequentPayment=true.
trackClient, err := alice.RPC.Router.TrackPaymentV2(
context.Background(), &routerrpc.TrackPaymentRequest{
PaymentHash: payHash,
PreventSubsequentPayment: true,
},
)
require.NoError(ht, err)
_, err = trackClient.Recv()
require.ErrorContains(ht, err, "payment isn't initiated")

// Now try to call SendPaymentV2. It should fail.
sendClient, err := alice.RPC.Router.SendPaymentV2(
context.Background(), &routerrpc.SendPaymentRequest{
PaymentRequest: payReq,
TimeoutSeconds: 3600,
},
)
require.NoError(ht, err)
_, err = sendClient.Recv()
require.ErrorContains(ht, err, "payment has been tracked")

// Delete payment from DB. This should undo the effect done by
// TrackPaymentV2 with PreventSubsequentPayment enabled.
_, err = alice.RPC.LN.DeletePayment(
context.Background(), &lnrpc.DeletePaymentRequest{
PaymentHash: payHash,
},
)
require.NoError(ht, err)

// Now call TrackPaymentV2 with PreventSubsequentPayment=false. It
// should get "payment isn't initiated" and have no effect on the
// subsequent SendPaymentV2.
trackClient, err = alice.RPC.Router.TrackPaymentV2(
context.Background(), &routerrpc.TrackPaymentRequest{
PaymentHash: payHash,
PreventSubsequentPayment: false,
},
)
require.NoError(ht, err)
_, err = trackClient.Recv()
require.ErrorContains(ht, err, "payment isn't initiated")

// Now try to call SendPaymentV2 again. It should succeed.
sendClient, err = alice.RPC.Router.SendPaymentV2(
context.Background(), &routerrpc.SendPaymentRequest{
PaymentRequest: payReq,
TimeoutSeconds: 3600,
},
)
require.NoError(ht, err)
_, err = sendClient.Recv()
require.NoError(ht, err)

// Now call TrackPaymentV2 with PreventSubsequentPayment=true again.
trackClient, err = alice.RPC.Router.TrackPaymentV2(
context.Background(), &routerrpc.TrackPaymentRequest{
PaymentHash: payHash,
PreventSubsequentPayment: true,
},
)
require.NoError(ht, err)
_, err = trackClient.Recv()
require.NoError(ht, err)
}

func sendPaymentInterceptAndCancel(ht *lntest.HarnessTest,
alice, bob, carol *node.HarnessNode, cpAB *lnrpc.ChannelPoint,
interceptorAction routerrpc.ResolveHoldForwardAction,
Expand Down
Loading
Loading