Skip to content

Commit d76959d

Browse files
authored
Merge pull request #240 from joostjager/loopin-record-htlc
loopin: record htlx tx hash
2 parents 98faa8c + f91422d commit d76959d

File tree

4 files changed

+129
-31
lines changed

4 files changed

+129
-31
lines changed

loopdb/swapstate.go

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ const (
6060
// StateInvoiceSettled means that the swap invoice has been paid by the
6161
// server.
6262
StateInvoiceSettled SwapState = 9
63+
64+
// StateFailIncorrectHtlcAmt indicates that the amount of an externally
65+
// published loop in htlc didn't match the swap amount.
66+
StateFailIncorrectHtlcAmt SwapState = 10
6367
)
6468

6569
// SwapStateType defines the types of swap states that exist. Every swap state
@@ -127,6 +131,9 @@ func (s SwapState) String() string {
127131
case StateInvoiceSettled:
128132
return "InvoiceSettled"
129133

134+
case StateFailIncorrectHtlcAmt:
135+
return "IncorrectHtlcAmt"
136+
130137
default:
131138
return "Unknown"
132139
}

loopin.go

+86-30
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@ import (
99

1010
"github.com/btcsuite/btcutil"
1111

12-
"github.com/lightningnetwork/lnd/chainntnfs"
13-
"github.com/lightningnetwork/lnd/channeldb"
14-
"github.com/lightningnetwork/lnd/lnwire"
15-
12+
"github.com/btcsuite/btcd/chaincfg/chainhash"
1613
"github.com/btcsuite/btcd/wire"
17-
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
18-
1914
"github.com/lightninglabs/lndclient"
2015
"github.com/lightninglabs/loop/loopdb"
2116
"github.com/lightninglabs/loop/swap"
17+
"github.com/lightningnetwork/lnd/chainntnfs"
18+
"github.com/lightningnetwork/lnd/channeldb"
19+
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
2220
"github.com/lightningnetwork/lnd/lntypes"
21+
"github.com/lightningnetwork/lnd/lnwire"
2322
)
2423

2524
var (
@@ -58,6 +57,9 @@ type loopInSwap struct {
5857

5958
htlcNP2WSH *swap.Htlc
6059

60+
// htlcTxHash is the confirmed htlc tx id.
61+
htlcTxHash *chainhash.Hash
62+
6163
timeoutAddr btcutil.Address
6264
}
6365

@@ -209,6 +211,7 @@ func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
209211
} else {
210212
swap.state = lastUpdate.State
211213
swap.lastUpdateTime = lastUpdate.Time
214+
swap.htlcTxHash = lastUpdate.HtlcTxHash
212215
}
213216

214217
return swap, nil
@@ -333,7 +336,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
333336
// HtlcPublished state directly and wait for
334337
// confirmation.
335338
s.setState(loopdb.StateHtlcPublished)
336-
err = s.persistState(globalCtx)
339+
err = s.persistAndAnnounceState(globalCtx)
337340
if err != nil {
338341
return err
339342
}
@@ -363,6 +366,13 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
363366
return err
364367
}
365368

369+
// Verify that the confirmed (external) htlc value matches the swap
370+
// amount. Otherwise fail the swap immediately.
371+
if htlcValue != s.LoopInContract.AmountRequested {
372+
s.setState(loopdb.StateFailIncorrectHtlcAmt)
373+
return s.persistAndAnnounceState(globalCtx)
374+
}
375+
366376
// TODO: Add miner fee of htlc tx to swap cost balance.
367377

368378
// The server is expected to see the htlc on-chain and knowing that it
@@ -376,7 +386,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
376386
}
377387

378388
// Persist swap outcome.
379-
if err := s.persistState(globalCtx); err != nil {
389+
if err := s.persistAndAnnounceState(globalCtx); err != nil {
380390
return err
381391
}
382392

@@ -387,39 +397,53 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
387397
func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
388398
*chainntnfs.TxConfirmation, error) {
389399

400+
// Register for confirmation of the htlc. It is essential to specify not
401+
// just the pk script, because an attacker may publish the same htlc
402+
// with a lower value and we don't want to follow through with that tx.
403+
// In the unlikely event that our call to SendOutputs crashes and we
404+
// restart, htlcTxHash will be nil at this point. Then only register
405+
// with PkScript and accept the risk that the call triggers on a
406+
// different htlc outpoint.
407+
s.log.Infof("Register for htlc conf (hh=%v, txid=%v)",
408+
s.InitiationHeight, s.htlcTxHash)
409+
410+
if s.htlcTxHash == nil {
411+
s.log.Warnf("No htlc tx hash available, registering with " +
412+
"just the pkscript")
413+
}
414+
390415
ctx, cancel := context.WithCancel(globalCtx)
391416
defer cancel()
392417

393418
notifier := s.lnd.ChainNotifier
394419

395420
confChanP2WSH, confErrP2WSH, err := notifier.RegisterConfirmationsNtfn(
396-
ctx, nil, s.htlcP2WSH.PkScript, 1, s.InitiationHeight,
421+
ctx, s.htlcTxHash, s.htlcP2WSH.PkScript, 1, s.InitiationHeight,
397422
)
398423
if err != nil {
399424
return nil, err
400425
}
401426

402427
confChanNP2WSH, confErrNP2WSH, err := notifier.RegisterConfirmationsNtfn(
403-
ctx, nil, s.htlcNP2WSH.PkScript, 1, s.InitiationHeight,
428+
ctx, s.htlcTxHash, s.htlcNP2WSH.PkScript, 1, s.InitiationHeight,
404429
)
405430
if err != nil {
406431
return nil, err
407432
}
408433

409-
for {
434+
var conf *chainntnfs.TxConfirmation
435+
for conf == nil {
410436
select {
411437

412438
// P2WSH htlc confirmed.
413-
case conf := <-confChanP2WSH:
439+
case conf = <-confChanP2WSH:
414440
s.htlc = s.htlcP2WSH
415441
s.log.Infof("P2WSH htlc confirmed")
416-
return conf, nil
417442

418443
// NP2WSH htlc confirmed.
419-
case conf := <-confChanNP2WSH:
444+
case conf = <-confChanNP2WSH:
420445
s.htlc = s.htlcNP2WSH
421446
s.log.Infof("NP2WSH htlc confirmed")
422-
return conf, nil
423447

424448
// Conf ntfn error.
425449
case err := <-confErrP2WSH:
@@ -438,6 +462,19 @@ func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
438462
return nil, globalCtx.Err()
439463
}
440464
}
465+
466+
// Store htlc tx hash for accounting purposes. Usually this call is a
467+
// no-op because the htlc tx hash was already known. Exceptions are:
468+
//
469+
// - Old pending swaps that were initiated before we persisted the htlc
470+
// tx hash directly after publish.
471+
//
472+
// - Swaps that experienced a crash during their call to SendOutputs. In
473+
// that case, we weren't able to record the tx hash.
474+
txHash := conf.Tx.TxHash()
475+
s.htlcTxHash = &txHash
476+
477+
return conf, nil
441478
}
442479

443480
// publishOnChainHtlc checks whether there are still enough blocks left and if
@@ -451,7 +488,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
451488
// Verify whether it still makes sense to publish the htlc.
452489
if blocksRemaining < MinLoopInPublishDelta {
453490
s.setState(loopdb.StateFailTimeout)
454-
return false, s.persistState(ctx)
491+
return false, s.persistAndAnnounceState(ctx)
455492
}
456493

457494
// Get fee estimate from lnd.
@@ -465,7 +502,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
465502
// Transition to state HtlcPublished before calling SendOutputs to
466503
// prevent us from ever paying multiple times after a crash.
467504
s.setState(loopdb.StateHtlcPublished)
468-
err = s.persistState(ctx)
505+
err = s.persistAndAnnounceState(ctx)
469506
if err != nil {
470507
return false, err
471508
}
@@ -483,7 +520,20 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
483520
if err != nil {
484521
return false, fmt.Errorf("send outputs: %v", err)
485522
}
486-
s.log.Infof("Published on chain HTLC tx %v", tx.TxHash())
523+
txHash := tx.TxHash()
524+
s.log.Infof("Published on chain HTLC tx %v", txHash)
525+
526+
// Persist the htlc hash so that after a restart we are still waiting
527+
// for our own htlc. We don't need to announce to clients, because the
528+
// state remains unchanged.
529+
//
530+
// TODO(joostjager): Store tx hash before calling SendOutputs. This is
531+
// not yet possible with the current lnd api.
532+
s.htlcTxHash = &txHash
533+
s.lastUpdateTime = time.Now()
534+
if err := s.persistState(); err != nil {
535+
return false, fmt.Errorf("persist htlc tx: %v", err)
536+
}
487537

488538
return true, nil
489539

@@ -499,7 +549,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
499549
rpcCtx, cancel := context.WithCancel(ctx)
500550
defer cancel()
501551
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
502-
rpcCtx, nil, s.htlc.PkScript, s.InitiationHeight,
552+
rpcCtx, htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
503553
)
504554
if err != nil {
505555
return fmt.Errorf("register spend ntfn: %v", err)
@@ -589,7 +639,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
589639
// accounting data.
590640
if s.state == loopdb.StateHtlcPublished {
591641
s.setState(loopdb.StateInvoiceSettled)
592-
err := s.persistState(ctx)
642+
err := s.persistAndAnnounceState(ctx)
593643
if err != nil {
594644
return err
595645
}
@@ -689,24 +739,30 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
689739
return nil
690740
}
691741

692-
// persistState updates the swap state and sends out an update notification.
693-
func (s *loopInSwap) persistState(ctx context.Context) error {
742+
// persistAndAnnounceState updates the swap state on disk and sends out an
743+
// update notification.
744+
func (s *loopInSwap) persistAndAnnounceState(ctx context.Context) error {
694745
// Update state in store.
695-
err := s.store.UpdateLoopIn(
696-
s.hash, s.lastUpdateTime,
697-
loopdb.SwapStateData{
698-
State: s.state,
699-
Cost: s.cost,
700-
},
701-
)
702-
if err != nil {
746+
if err := s.persistState(); err != nil {
703747
return err
704748
}
705749

706750
// Send out swap update
707751
return s.sendUpdate(ctx)
708752
}
709753

754+
// persistState updates the swap state on disk.
755+
func (s *loopInSwap) persistState() error {
756+
return s.store.UpdateLoopIn(
757+
s.hash, s.lastUpdateTime,
758+
loopdb.SwapStateData{
759+
State: s.state,
760+
Cost: s.cost,
761+
HtlcTxHash: s.htlcTxHash,
762+
},
763+
)
764+
}
765+
710766
// setState updates the swap state and last update timestamp.
711767
func (s *loopInSwap) setState(state loopdb.SwapState) {
712768
s.lastUpdateTime = time.Now()

loopin_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/lightninglabs/loop/test"
1111
"github.com/lightningnetwork/lnd/chainntnfs"
1212
"github.com/lightningnetwork/lnd/channeldb"
13+
"github.com/stretchr/testify/require"
1314

1415
"github.com/btcsuite/btcd/wire"
1516
"github.com/btcsuite/btcutil"
@@ -61,6 +62,10 @@ func TestLoopInSuccess(t *testing.T) {
6162
// Expect htlc to be published.
6263
htlcTx := <-ctx.lnd.SendOutputsChannel
6364

65+
// Expect the same state to be written again with the htlc tx hash.
66+
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
67+
require.NotNil(t, state.HtlcTxHash)
68+
6469
// Expect register for htlc conf.
6570
<-ctx.lnd.RegisterConfChannel
6671
<-ctx.lnd.RegisterConfChannel
@@ -182,6 +187,10 @@ func testLoopInTimeout(t *testing.T,
182187
if externalValue == 0 {
183188
// Expect htlc to be published.
184189
htlcTx = <-ctx.lnd.SendOutputsChannel
190+
191+
// Expect the same state to be written again with the htlc tx hash.
192+
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
193+
require.NotNil(t, state.HtlcTxHash)
185194
} else {
186195
// Create an external htlc publish tx.
187196
var pkScript []byte
@@ -209,6 +218,20 @@ func testLoopInTimeout(t *testing.T,
209218
Tx: &htlcTx,
210219
}
211220

221+
// Assert that the swap is failed in case of an invalid amount.
222+
invalidAmt := externalValue != 0 && externalValue != int64(req.Amount)
223+
if invalidAmt {
224+
ctx.assertState(loopdb.StateFailIncorrectHtlcAmt)
225+
ctx.store.assertLoopInState(loopdb.StateFailIncorrectHtlcAmt)
226+
227+
err = <-errChan
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
232+
return
233+
}
234+
212235
// Client starts listening for spend of htlc.
213236
<-ctx.lnd.RegisterSpendChannel
214237

@@ -375,11 +398,17 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) {
375398

376399
// Expect htlc to be published.
377400
htlcTx = <-ctx.lnd.SendOutputsChannel
401+
402+
// Expect the same state to be written again with the htlc tx
403+
// hash.
404+
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
405+
require.NotNil(t, state.HtlcTxHash)
378406
} else {
379407
ctx.assertState(loopdb.StateHtlcPublished)
380408

381409
htlcTx.AddTxOut(&wire.TxOut{
382410
PkScript: htlc.PkScript,
411+
Value: int64(contract.AmountRequested),
383412
})
384413
}
385414

store_mock_test.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,19 @@ func (s *storeMock) assertLoopInStored() {
215215
<-s.loopInStoreChan
216216
}
217217

218-
func (s *storeMock) assertLoopInState(expectedState loopdb.SwapState) {
218+
// assertLoopInState asserts that a specified state transition is persisted to
219+
// disk.
220+
func (s *storeMock) assertLoopInState(
221+
expectedState loopdb.SwapState) loopdb.SwapStateData {
222+
219223
s.t.Helper()
220224

221225
state := <-s.loopInUpdateChan
222226
if state.State != expectedState {
223227
s.t.Fatalf("expected state %v, got %v", expectedState, state)
224228
}
229+
230+
return state
225231
}
226232

227233
func (s *storeMock) assertStorePreimageReveal() {

0 commit comments

Comments
 (0)