Skip to content

Commit 8c544bf

Browse files
committed
loopdb: store outgoing channel set
Upgrade the database schema to allow for multiple outgoing channels. This is implemented as an on-the-fly migration leaving the old key in place.
1 parent 044c1c1 commit 8c544bf

File tree

9 files changed

+188
-38
lines changed

9 files changed

+188
-38
lines changed

client.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,8 @@ func (s *Client) resumeSwaps(ctx context.Context,
359359
func (s *Client) LoopOut(globalCtx context.Context,
360360
request *OutRequest) (*lntypes.Hash, btcutil.Address, error) {
361361

362-
log.Infof("LoopOut %v to %v (channel: %v)",
363-
request.Amount, request.DestAddr,
364-
request.LoopOutChannel,
362+
log.Infof("LoopOut %v to %v (channels: %v)",
363+
request.Amount, request.DestAddr, request.OutgoingChanSet,
365364
)
366365

367366
if err := s.waitForInitialized(globalCtx); err != nil {

interface.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ type OutRequest struct {
6464
// client sweep tx.
6565
SweepConfTarget int32
6666

67-
// LoopOutChannel optionally specifies the short channel id of the
68-
// channel to loop out.
69-
LoopOutChannel *uint64
67+
// OutgoingChanSet optionally specifies the short channel ids of the
68+
// channels that may be used to loop out.
69+
OutgoingChanSet loopdb.ChannelSet
7070

7171
// SwapPublicationDeadline can be set by the client to allow the server
7272
// delaying publication of the swap HTLC to save on chain fees.

loopd/swapclient_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
9090
),
9191
}
9292
if in.LoopOutChannel != 0 {
93-
req.LoopOutChannel = &in.LoopOutChannel
93+
req.OutgoingChanSet = loopdb.ChannelSet{in.LoopOutChannel}
9494
}
9595
hash, htlc, err := s.impl.LoopOut(ctx, req)
9696
if err != nil {

loopd/view.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package loopd
22

33
import (
44
"fmt"
5-
"strconv"
65

76
"github.com/btcsuite/btcd/chaincfg"
87
"github.com/lightninglabs/loop"
@@ -64,13 +63,8 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error {
6463
fmt.Printf(" Preimage: %v\n", s.Contract.Preimage)
6564
fmt.Printf(" Htlc address: %v\n", htlc.Address)
6665

67-
unchargeChannel := "any"
68-
if s.Contract.UnchargeChannel != nil {
69-
unchargeChannel = strconv.FormatUint(
70-
*s.Contract.UnchargeChannel, 10,
71-
)
72-
}
73-
fmt.Printf(" Uncharge channel: %v\n", unchargeChannel)
66+
fmt.Printf(" Uncharge channels: %v\n",
67+
s.Contract.OutgoingChanSet)
7468
fmt.Printf(" Dest: %v\n", s.Contract.DestAddr)
7569
fmt.Printf(" Amt: %v, Expiry: %v\n",
7670
s.Contract.AmountRequested, s.Contract.CltvExpiry,

loopdb/loopout.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"bytes"
55
"encoding/binary"
66
"fmt"
7+
"strconv"
8+
"strings"
79
"time"
810

911
"github.com/btcsuite/btcd/chaincfg"
@@ -34,9 +36,9 @@ type LoopOutContract struct {
3436
// client sweep tx.
3537
SweepConfTarget int32
3638

37-
// TargetChannel is the channel to loop out. If zero, any channel may
38-
// be used.
39-
UnchargeChannel *uint64
39+
// OutgoingChanSet is the set of short ids of channels that may be used.
40+
// If empty, any channel may be used.
41+
OutgoingChanSet ChannelSet
4042

4143
// PrepayInvoice is the invoice that the client should pay to the
4244
// server that will be returned if the swap is complete.
@@ -53,6 +55,34 @@ type LoopOutContract struct {
5355
SwapPublicationDeadline time.Time
5456
}
5557

58+
// ChannelSet stores a set of channels.
59+
type ChannelSet []uint64
60+
61+
// String returns the human-readable representation of a channel set.
62+
func (c ChannelSet) String() string {
63+
channelStrings := make([]string, len(c))
64+
for i, chanID := range c {
65+
channelStrings[i] = strconv.FormatUint(chanID, 10)
66+
}
67+
return strings.Join(channelStrings, ",")
68+
}
69+
70+
// NewChannelSet instantiates a new channel set and verifies that there are no
71+
// duplicates present.
72+
func NewChannelSet(set []uint64) (ChannelSet, error) {
73+
// Check channel set for duplicates.
74+
chanSet := make(map[uint64]struct{})
75+
for _, chanID := range set {
76+
if _, exists := chanSet[chanID]; exists {
77+
return nil, fmt.Errorf("duplicate chan in set: id=%v",
78+
chanID)
79+
}
80+
chanSet[chanID] = struct{}{}
81+
}
82+
83+
return ChannelSet(set), nil
84+
}
85+
5686
// LoopOut is a combination of the contract and the updates.
5787
type LoopOut struct {
5888
Loop
@@ -161,7 +191,7 @@ func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
161191
return nil, err
162192
}
163193
if unchargeChannel != 0 {
164-
contract.UnchargeChannel = &unchargeChannel
194+
contract.OutgoingChanSet = ChannelSet{unchargeChannel}
165195
}
166196

167197
var deadlineNano int64
@@ -248,10 +278,9 @@ func serializeLoopOutContract(swap *LoopOutContract) (
248278
return nil, err
249279
}
250280

251-
var unchargeChannel uint64
252-
if swap.UnchargeChannel != nil {
253-
unchargeChannel = *swap.UnchargeChannel
254-
}
281+
// Always write no outgoing channel. This field is replaced by an
282+
// outgoing channel set.
283+
unchargeChannel := uint64(0)
255284
if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil {
256285
return nil, err
257286
}

loopdb/store.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package loopdb
22

33
import (
4+
"bytes"
45
"encoding/binary"
56
"errors"
67
"fmt"
8+
"io"
79
"os"
810
"path/filepath"
911
"time"
@@ -51,6 +53,14 @@ var (
5153
// value: time || rawSwapState
5254
contractKey = []byte("contract")
5355

56+
// outgoingChanSetKey is the key that stores a list of channel ids that
57+
// restrict the loop out swap payment.
58+
//
59+
// path: loopOutBucket -> swapBucket[hash] -> outgoingChanSetKey
60+
//
61+
// value: concatenation of uint64 channel ids
62+
outgoingChanSetKey = []byte("outgoing-chan-set")
63+
5464
byteOrder = binary.BigEndian
5565

5666
keyLength = 33
@@ -190,6 +200,29 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
190200
return err
191201
}
192202

203+
// Read the list of concatenated outgoing channel ids
204+
// that form the outgoing set.
205+
setBytes := swapBucket.Get(outgoingChanSetKey)
206+
if outgoingChanSetKey != nil {
207+
r := bytes.NewReader(setBytes)
208+
readLoop:
209+
for {
210+
var chanID uint64
211+
err := binary.Read(r, byteOrder, &chanID)
212+
switch {
213+
case err == io.EOF:
214+
break readLoop
215+
case err != nil:
216+
return err
217+
}
218+
219+
contract.OutgoingChanSet = append(
220+
contract.OutgoingChanSet,
221+
chanID,
222+
)
223+
}
224+
}
225+
193226
updates, err := deserializeUpdates(swapBucket)
194227
if err != nil {
195228
return err
@@ -374,6 +407,19 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
374407
return err
375408
}
376409

410+
// Write the outgoing channel set.
411+
var b bytes.Buffer
412+
for _, chanID := range swap.OutgoingChanSet {
413+
err := binary.Write(&b, byteOrder, chanID)
414+
if err != nil {
415+
return err
416+
}
417+
}
418+
err = swapBucket.Put(outgoingChanSetKey, b.Bytes())
419+
if err != nil {
420+
return err
421+
}
422+
377423
// Finally, we'll create an empty updates bucket for this swap
378424
// to track any future updates to the swap itself.
379425
_, err = swapBucket.CreateBucket(updatesBucketKey)

loopdb/store_test.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestLoopOutStore(t *testing.T) {
4545

4646
// Next, we'll make a new pending swap that we'll insert into the
4747
// database shortly.
48-
pendingSwap := LoopOutContract{
48+
unrestrictedSwap := LoopOutContract{
4949
SwapContract: SwapContract{
5050
AmountRequested: 100,
5151
Preimage: testPreimage,
@@ -71,7 +71,16 @@ func TestLoopOutStore(t *testing.T) {
7171
SwapPublicationDeadline: time.Unix(0, initiationTime.UnixNano()),
7272
}
7373

74-
testLoopOutStore(t, &pendingSwap)
74+
t.Run("no outgoing set", func(t *testing.T) {
75+
testLoopOutStore(t, &unrestrictedSwap)
76+
})
77+
78+
restrictedSwap := unrestrictedSwap
79+
restrictedSwap.OutgoingChanSet = ChannelSet{1, 2}
80+
81+
t.Run("two channel outgoing set", func(t *testing.T) {
82+
testLoopOutStore(t, &restrictedSwap)
83+
})
7584
}
7685

7786
// testLoopOutStore tests the basic functionality of the current bbolt
@@ -373,3 +382,65 @@ func createVersionZeroDb(t *testing.T, dbPath string) {
373382
t.Fatal(err)
374383
}
375384
}
385+
386+
// TestLegacyOutgoingChannel asserts that a legacy channel restriction is
387+
// properly mapped onto the newer channel set.
388+
func TestLegacyOutgoingChannel(t *testing.T) {
389+
var (
390+
legacyDbVersion = Hex("00000003")
391+
legacyOutgoingChannel = Hex("0000000000000005")
392+
)
393+
394+
legacyDb := map[string]interface{}{
395+
"loop-in": map[string]interface{}{},
396+
"metadata": map[string]interface{}{
397+
"dbp": legacyDbVersion,
398+
},
399+
"uncharge-swaps": map[string]interface{}{
400+
Hex("2a595d79a55168970532805ae20c9b5fac98f04db79ba4c6ae9b9ac0f206359e"): map[string]interface{}{
401+
"contract": Hex("1562d6fbec140000010101010202020203030303040404040101010102020202030303030404040400000000000000640d707265706179696e766f69636501010101010101010101010101010101010101010101010101010101010101010201010101010101010101010101010101010101010101010101010101010101010300000090000000000000000a0000000000000014000000000000002800000063223347454e556d6e4552745766516374344e65676f6d557171745a757a5947507742530b73776170696e766f69636500000002000000000000001e") + legacyOutgoingChannel + Hex("1562d6fbec140000"),
402+
"updates": map[string]interface{}{
403+
Hex("0000000000000001"): Hex("1508290a92d4c00001000000000000000000000000000000000000000000000000"),
404+
Hex("0000000000000002"): Hex("1508290a92d4c00006000000000000000000000000000000000000000000000000"),
405+
},
406+
},
407+
},
408+
}
409+
410+
// Restore a legacy database.
411+
tempDirName, err := ioutil.TempDir("", "clientstore")
412+
if err != nil {
413+
t.Fatal(err)
414+
}
415+
defer os.RemoveAll(tempDirName)
416+
417+
tempPath := filepath.Join(tempDirName, dbFileName)
418+
db, err := bbolt.Open(tempPath, 0600, nil)
419+
if err != nil {
420+
t.Fatal(err)
421+
}
422+
err = db.Update(func(tx *bbolt.Tx) error {
423+
return RestoreDB(tx, legacyDb)
424+
})
425+
if err != nil {
426+
t.Fatal(err)
427+
}
428+
db.Close()
429+
430+
// Fetch the legacy swap.
431+
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
432+
if err != nil {
433+
t.Fatal(err)
434+
}
435+
436+
swaps, err := store.FetchLoopOutSwaps()
437+
if err != nil {
438+
t.Fatal(err)
439+
}
440+
441+
// Assert that the outgoing channel is read properly.
442+
expectedChannelSet := ChannelSet{5}
443+
if !reflect.DeepEqual(swaps[0].Contract.OutgoingChanSet, expectedChannelSet) {
444+
t.Fatal("invalid outgoing channel")
445+
}
446+
}

loopout.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
112112
return nil, err
113113
}
114114

115+
// Check channel set for duplicates.
116+
chanSet, err := loopdb.NewChannelSet(request.OutgoingChanSet)
117+
if err != nil {
118+
return nil, err
119+
}
120+
115121
// Instantiate a struct that contains all required data to start the
116122
// swap.
117123
initiationTime := time.Now()
@@ -121,7 +127,6 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
121127
DestAddr: request.DestAddr,
122128
MaxSwapRoutingFee: request.MaxSwapRoutingFee,
123129
SweepConfTarget: request.SweepConfTarget,
124-
UnchargeChannel: request.LoopOutChannel,
125130
PrepayInvoice: swapResp.prepayInvoice,
126131
MaxPrepayRoutingFee: request.MaxPrepayRoutingFee,
127132
SwapPublicationDeadline: request.SwapPublicationDeadline,
@@ -136,6 +141,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
136141
MaxMinerFee: request.MaxMinerFee,
137142
MaxSwapFee: request.MaxSwapFee,
138143
},
144+
OutgoingChanSet: chanSet,
139145
}
140146

141147
swapKit := newSwapKit(
@@ -430,15 +436,9 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) {
430436
// Pay the swap invoice.
431437
s.log.Infof("Sending swap payment %v", s.SwapInvoice)
432438

433-
var outgoingChanIds []uint64
434-
if s.LoopOutContract.UnchargeChannel != nil {
435-
outgoingChanIds = append(
436-
outgoingChanIds, *s.LoopOutContract.UnchargeChannel,
437-
)
438-
}
439-
440439
s.swapPaymentChan = s.payInvoice(
441-
ctx, s.SwapInvoice, s.MaxSwapRoutingFee, outgoingChanIds,
440+
ctx, s.SwapInvoice, s.MaxSwapRoutingFee,
441+
s.LoopOutContract.OutgoingChanSet,
442442
)
443443

444444
// Pay the prepay invoice.
@@ -452,7 +452,7 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) {
452452
// payInvoice pays a single invoice.
453453
func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
454454
maxFee btcutil.Amount,
455-
outgoingChanIds []uint64) chan lndclient.PaymentResult {
455+
outgoingChanIds loopdb.ChannelSet) chan lndclient.PaymentResult {
456456

457457
resultChan := make(chan lndclient.PaymentResult)
458458

@@ -481,8 +481,8 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
481481

482482
// payInvoiceAsync is the asynchronously executed part of paying an invoice.
483483
func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
484-
invoice string, maxFee btcutil.Amount, outgoingChanIds []uint64) (
485-
*lndclient.PaymentStatus, error) {
484+
invoice string, maxFee btcutil.Amount,
485+
outgoingChanIds loopdb.ChannelSet) (*lndclient.PaymentStatus, error) {
486486

487487
// Extract hash from payment request. Unfortunately the request
488488
// components aren't available directly.

0 commit comments

Comments
 (0)