From 1877b7f08b429fd58aa88977d1a62660e73dde4e Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 4 Aug 2020 20:28:06 +0200 Subject: [PATCH] multi: store loop out htlc confirmations on disk To allow users to specify differing confirmation targets, we store the swap conf target per-swap. This makes us restart safe, so we do not forget confirmation values for swaps that are in flight when we restart. --- client_test.go | 11 ++++++++--- loopdb/loopout.go | 4 ++++ loopdb/store.go | 41 +++++++++++++++++++++++++++++++++++++++++ loopdb/store_test.go | 1 + loopout.go | 9 +++++---- loopout_test.go | 8 ++++---- test/context.go | 8 +++++++- 7 files changed, 70 insertions(+), 12 deletions(-) diff --git a/client_test.go b/client_test.go index a2d605d..3bb9f91 100644 --- a/client_test.go +++ b/client_test.go @@ -36,6 +36,8 @@ var ( swapInvoiceDesc = "swap" prepayInvoiceDesc = "prepay" + + defaultConfirmations = int32(loopdb.DefaultLoopOutHtlcConfirmations) ) // TestSuccess tests the loop out happy flow. @@ -57,7 +59,7 @@ func TestSuccess(t *testing.T) { signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) // Expect client to register for conf. - confIntent := ctx.AssertRegisterConf(false) + confIntent := ctx.AssertRegisterConf(false, defaultConfirmations) testSuccess(ctx, testRequest.Amount, info.SwapHash, signalPrepaymentResult, signalSwapPaymentResult, false, @@ -83,7 +85,7 @@ func TestFailOffchain(t *testing.T) { signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) - ctx.AssertRegisterConf(false) + ctx.AssertRegisterConf(false, defaultConfirmations) signalSwapPaymentResult( errors.New(lndclient.PaymentResultUnknownPaymentHash), @@ -196,6 +198,7 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) { DestAddr: dest, SwapInvoice: swapPayReq, SweepConfTarget: 2, + HtlcConfirmations: loopdb.DefaultLoopOutHtlcConfirmations, MaxSwapRoutingFee: 70000, PrepayInvoice: prePayReq, SwapContract: loopdb.SwapContract{ @@ -232,7 +235,9 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) { signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) // Expect client to register for conf. - confIntent := ctx.AssertRegisterConf(preimageRevealed) + confIntent := ctx.AssertRegisterConf( + preimageRevealed, defaultConfirmations, + ) signalSwapPaymentResult(nil) signalPrepaymentResult(nil) diff --git a/loopdb/loopout.go b/loopdb/loopout.go index d24a57e..54b31c0 100644 --- a/loopdb/loopout.go +++ b/loopdb/loopout.go @@ -36,6 +36,10 @@ type LoopOutContract struct { // client sweep tx. SweepConfTarget int32 + // HtlcConfirmations is the number of confirmations we require the on + // chain htlc to have before proceeding with the swap. + HtlcConfirmations uint32 + // OutgoingChanSet is the set of short ids of channels that may be used. // If empty, any channel may be used. OutgoingChanSet ChannelSet diff --git a/loopdb/store.go b/loopdb/store.go index b4a6fad..adb88aa 100644 --- a/loopdb/store.go +++ b/loopdb/store.go @@ -77,11 +77,23 @@ var ( // value: concatenation of uint64 channel ids outgoingChanSetKey = []byte("outgoing-chan-set") + // confirmationsKey is the key that stores the number of confirmations + // that were requested for a loop out swap. + // + // path: loopOutBucket -> swapBucket[hash] -> confirmationsKey + // + // value: uint32 confirmation value + confirmationsKey = []byte("confirmations") + byteOrder = binary.BigEndian keyLength = 33 ) +// DefaultLoopOutHtlcConfirmations is the default number of confirmations we +// set for a loop out htlc. +const DefaultLoopOutHtlcConfirmations uint32 = 1 + // fileExists returns true if the file exists, and false otherwise. func fileExists(path string) bool { if _, err := os.Stat(path); err != nil { @@ -242,6 +254,23 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) { } } + // Set our default number of confirmations for the swap. + contract.HtlcConfirmations = DefaultLoopOutHtlcConfirmations + + // If we have the number of confirmations stored for + // this swap, we overwrite our default with the stored + // value. + confBytes := swapBucket.Get(confirmationsKey) + if confBytes != nil { + r := bytes.NewReader(confBytes) + err := binary.Read( + r, byteOrder, &contract.HtlcConfirmations, + ) + if err != nil { + return err + } + } + updates, err := deserializeUpdates(swapBucket) if err != nil { return err @@ -471,6 +500,18 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash, return err } + // Write our confirmation target under its own key. + var buf bytes.Buffer + err = binary.Write(&buf, byteOrder, swap.HtlcConfirmations) + if err != nil { + return err + } + + err = swapBucket.Put(confirmationsKey, buf.Bytes()) + if err != nil { + return err + } + // Finally, we'll create an empty updates bucket for this swap // to track any future updates to the swap itself. _, err = swapBucket.CreateBucket(updatesBucketKey) diff --git a/loopdb/store_test.go b/loopdb/store_test.go index bde8c25..76edccc 100644 --- a/loopdb/store_test.go +++ b/loopdb/store_test.go @@ -70,6 +70,7 @@ func TestLoopOutStore(t *testing.T) { SwapInvoice: "swapinvoice", MaxSwapRoutingFee: 30, SweepConfTarget: 2, + HtlcConfirmations: 2, SwapPublicationDeadline: time.Unix(0, initiationTime.UnixNano()), } diff --git a/loopout.go b/loopout.go index 4f92ff5..dd609da 100644 --- a/loopout.go +++ b/loopout.go @@ -147,6 +147,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, DestAddr: request.DestAddr, MaxSwapRoutingFee: request.MaxSwapRoutingFee, SweepConfTarget: request.SweepConfTarget, + HtlcConfirmations: loopdb.DefaultLoopOutHtlcConfirmations, PrepayInvoice: swapResp.prepayInvoice, MaxPrepayRoutingFee: request.MaxPrepayRoutingFee, SwapPublicationDeadline: request.SwapPublicationDeadline, @@ -606,8 +607,8 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( // Wait for confirmation of the on-chain htlc by watching for a tx // producing the swap script output. s.log.Infof( - "Register conf ntfn for swap script on chain (hh=%v)", - s.InitiationHeight, + "Register %v conf ntfn for swap script on chain (hh=%v)", + s.HtlcConfirmations, s.InitiationHeight, ) // If we've revealed the preimage in a previous run, we expect to have @@ -624,8 +625,8 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) ( defer cancel() htlcConfChan, htlcErrChan, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn( - ctx, s.htlcTxHash, s.htlc.PkScript, 1, - s.InitiationHeight, + ctx, s.htlcTxHash, s.htlc.PkScript, + int32(s.HtlcConfirmations), s.InitiationHeight, ) if err != nil { return nil, err diff --git a/loopout_test.go b/loopout_test.go index 80d1625..0e0456c 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -116,7 +116,7 @@ func TestLoopOutPaymentParameters(t *testing.T) { // Swap is expected to register for confirmation of the htlc. Assert // this to prevent a blocked channel in the mock. - ctx.AssertRegisterConf(false) + ctx.AssertRegisterConf(false, defaultConfirmations) // Cancel the swap. There is nothing else we need to assert. The payment // parameters don't play a role in the remainder of the swap process. @@ -191,7 +191,7 @@ func TestLateHtlcPublish(t *testing.T) { signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) // Expect client to register for conf - ctx.AssertRegisterConf(false) + ctx.AssertRegisterConf(false, defaultConfirmations) // // Wait too long before publishing htlc. blockEpochChan <- int32(swap.CltvExpiry - 10) @@ -290,7 +290,7 @@ func TestCustomSweepConfTarget(t *testing.T) { signalPrepaymentResult(nil) // Notify the confirmation notification for the HTLC. - ctx.AssertRegisterConf(false) + ctx.AssertRegisterConf(false, defaultConfirmations) blockEpochChan <- ctx.Lnd.Height + 1 @@ -494,7 +494,7 @@ func TestPreimagePush(t *testing.T) { signalPrepaymentResult(nil) // Notify the confirmation notification for the HTLC. - ctx.AssertRegisterConf(false) + ctx.AssertRegisterConf(false, defaultConfirmations) blockEpochChan <- ctx.Lnd.Height + 1 diff --git a/test/context.go b/test/context.go index 146573c..ca83322 100644 --- a/test/context.go +++ b/test/context.go @@ -12,6 +12,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/zpay32" + "github.com/stretchr/testify/require" ) // Context contains shared test context functions. @@ -113,7 +114,7 @@ func (ctx *Context) AssertTrackPayment() TrackPaymentMessage { } // AssertRegisterConf asserts that a register for conf has been received. -func (ctx *Context) AssertRegisterConf(expectTxHash bool) *ConfRegistration { +func (ctx *Context) AssertRegisterConf(expectTxHash bool, confs int32) *ConfRegistration { ctx.T.Helper() // Expect client to register for conf @@ -127,6 +128,11 @@ func (ctx *Context) AssertRegisterConf(expectTxHash bool) *ConfRegistration { case !expectTxHash && confIntent.TxID != nil: ctx.T.Fatalf("expected script only registration") } + + // Require that we registered for the number of confirmations + // the test expects. + require.Equal(ctx.T, confs, confIntent.NumConfs) + case <-time.After(Timeout): ctx.T.Fatalf("htlc confirmed not subscribed to") }