Skip to content

Commit 9678c78

Browse files
committed
multi: add swap label to SwapContract and store under separate key
This commits adds an optional label to our swaps, and writes it to disk under a separate key in our swap bucket. This approach is chosen rather than an on-the-fly addition to our existing swap contract field so that we do not need to deal with EOF checking in the future. To allow creation of unique internal labels, we add a reserved prefix which can be used by the daemon to set labels that are distinct from client set ones.
1 parent e2c54bd commit 9678c78

File tree

9 files changed

+223
-21
lines changed

9 files changed

+223
-21
lines changed

interface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ type OutRequest struct {
7474

7575
// Expiry is the absolute expiry height of the on-chain htlc.
7676
Expiry int32
77+
78+
// Label contains an optional label for the swap.
79+
Label string
7780
}
7881

7982
// Out contains the full details of a loop out request. This includes things
@@ -186,6 +189,9 @@ type LoopInRequest struct {
186189
// ExternalHtlc specifies whether the htlc is published by an external
187190
// source.
188191
ExternalHtlc bool
192+
193+
// Label contains an optional label for the swap.
194+
Label string
189195
}
190196

191197
// LoopInTerms are the server terms on which it executes loop in swaps.

labels/labels.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package labels
2+
3+
import (
4+
"errors"
5+
)
6+
7+
const (
8+
// MaxLength is the maximum length we allow for labels.
9+
MaxLength = 500
10+
11+
// Reserved is used as a prefix to separate labels that are created by
12+
// loopd from those created by users.
13+
Reserved = "[reserved]"
14+
)
15+
16+
var (
17+
// ErrLabelTooLong is returned when a label exceeds our length limit.
18+
ErrLabelTooLong = errors.New("label exceeds maximum length")
19+
20+
// ErrReservedPrefix is returned when a label contains the prefix
21+
// which is reserved for internally produced labels.
22+
ErrReservedPrefix = errors.New("label contains reserved prefix")
23+
)
24+
25+
// Validate checks that a label is of appropriate length and is not in our list
26+
// of reserved labels.
27+
func Validate(label string) error {
28+
if len(label) > MaxLength {
29+
return ErrLabelTooLong
30+
}
31+
32+
// If the label is shorter than our reserved prefix, it cannot contain
33+
// it.
34+
if len(label) < len(Reserved) {
35+
return nil
36+
}
37+
38+
// Check if our label begins with our reserved prefix. We don't mind if
39+
// it has our reserved prefix in another case, we just need to be able
40+
// to reserve a subset of labels with this prefix.
41+
if label[0:len(Reserved)] == Reserved {
42+
return ErrReservedPrefix
43+
}
44+
45+
return nil
46+
}

labels/labels_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package labels
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestValidate tests validation of labels.
12+
func TestValidate(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
label string
16+
err error
17+
}{
18+
{
19+
name: "label ok",
20+
label: "label",
21+
err: nil,
22+
},
23+
{
24+
name: "exceeds limit",
25+
label: strings.Repeat(" ", MaxLength+1),
26+
err: ErrLabelTooLong,
27+
},
28+
{
29+
name: "exactly reserved prefix",
30+
label: Reserved,
31+
err: ErrReservedPrefix,
32+
},
33+
{
34+
name: "starts with reserved prefix",
35+
label: fmt.Sprintf("%v test", Reserved),
36+
err: ErrReservedPrefix,
37+
},
38+
{
39+
name: "ends with reserved prefix",
40+
label: fmt.Sprintf("test %v", Reserved),
41+
err: nil,
42+
},
43+
}
44+
45+
for _, test := range tests {
46+
test := test
47+
48+
t.Run(test.name, func(t *testing.T) {
49+
t.Parallel()
50+
require.Equal(t, test.err, Validate(test.label))
51+
})
52+
}
53+
}

loopdb/loop.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type SwapContract struct {
4343

4444
// InitiationTime is the time at which the swap was initiated.
4545
InitiationTime time.Time
46+
47+
// Label contains an optional label for the swap.
48+
Label string
4649
}
4750

4851
// Loop contains fields shared between LoopIn and LoopOut

loopdb/loopin.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/coreos/bbolt"
10+
"github.com/lightninglabs/loop/labels"
911
"github.com/lightningnetwork/lnd/routing/route"
1012
)
1113

@@ -24,6 +26,10 @@ type LoopInContract struct {
2426
// ExternalHtlc specifies whether the htlc is published by an external
2527
// source.
2628
ExternalHtlc bool
29+
30+
// Label contains an optional label for the swap. Note that this field
31+
// is stored separately to the rest of the contract on disk.
32+
Label string
2733
}
2834

2935
// LoopIn is a combination of the contract and the updates.
@@ -112,6 +118,31 @@ func serializeLoopInContract(swap *LoopInContract) (
112118
return b.Bytes(), nil
113119
}
114120

121+
// putLabel performs validation of a label and writes it to the bucket provided
122+
// under the label key if it is non-zero.
123+
func putLabel(bucket *bbolt.Bucket, label string) error {
124+
if len(label) == 0 {
125+
return nil
126+
}
127+
128+
if err := labels.Validate(label); err != nil {
129+
return err
130+
}
131+
132+
return bucket.Put(labelKey, []byte(label))
133+
}
134+
135+
// getLabel attempts to get an optional label stored under the label key in a
136+
// bucket. If it is not present, an empty label is returned.
137+
func getLabel(bucket *bbolt.Bucket) string {
138+
label := bucket.Get(labelKey)
139+
if label == nil {
140+
return ""
141+
}
142+
143+
return string(label)
144+
}
145+
115146
// deserializeLoopInContract deserializes the loop in contract from a byte slice.
116147
func deserializeLoopInContract(value []byte) (*LoopInContract, error) {
117148
r := bytes.NewReader(value)

loopdb/store.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ var (
6060
// value: time || rawSwapState
6161
contractKey = []byte("contract")
6262

63+
// labelKey is the key that stores an optional label for the swap. If
64+
// a swap was created before we started adding labels, or was created
65+
// without a label, this key will not be present.
66+
//
67+
// path: loopInBucket/loopOutBucket -> swapBucket[hash] -> labelKey
68+
//
69+
// value: string label
70+
labelKey = []byte("label")
71+
6372
// outgoingChanSetKey is the key that stores a list of channel ids that
6473
// restrict the loop out swap payment.
6574
//
@@ -207,6 +216,9 @@ func (s *boltSwapStore) FetchLoopOutSwaps() ([]*LoopOut, error) {
207216
return err
208217
}
209218

219+
// Get our label for this swap, if it is present.
220+
contract.Label = getLabel(swapBucket)
221+
210222
// Read the list of concatenated outgoing channel ids
211223
// that form the outgoing set.
212224
setBytes := swapBucket.Get(outgoingChanSetKey)
@@ -352,6 +364,9 @@ func (s *boltSwapStore) FetchLoopInSwaps() ([]*LoopIn, error) {
352364
return err
353365
}
354366

367+
// Get our label for this swap, if it is present.
368+
contract.Label = getLabel(swapBucket)
369+
355370
updates, err := deserializeUpdates(swapBucket)
356371
if err != nil {
357372
return err
@@ -434,6 +449,10 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
434449
return err
435450
}
436451

452+
if err := putLabel(swapBucket, swap.Label); err != nil {
453+
return err
454+
}
455+
437456
// Write the outgoing channel set.
438457
var b bytes.Buffer
439458
for _, chanID := range swap.OutgoingChanSet {
@@ -447,6 +466,11 @@ func (s *boltSwapStore) CreateLoopOut(hash lntypes.Hash,
447466
return err
448467
}
449468

469+
// Write label to disk if we have one.
470+
if err := putLabel(swapBucket, swap.Label); err != nil {
471+
return err
472+
}
473+
450474
// Finally, we'll create an empty updates bucket for this swap
451475
// to track any future updates to the swap itself.
452476
_, err = swapBucket.CreateBucket(updatesBucketKey)
@@ -485,6 +509,11 @@ func (s *boltSwapStore) CreateLoopIn(hash lntypes.Hash,
485509
return err
486510
}
487511

512+
// Write label to disk if we have one.
513+
if err := putLabel(swapBucket, swap.Label); err != nil {
514+
return err
515+
}
516+
488517
// Finally, we'll create an empty updates bucket for this swap
489518
// to track any future updates to the swap itself.
490519
_, err = swapBucket.CreateBucket(updatesBucketKey)

loopdb/store_test.go

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ func TestLoopOutStore(t *testing.T) {
8383
t.Run("two channel outgoing set", func(t *testing.T) {
8484
testLoopOutStore(t, &restrictedSwap)
8585
})
86+
87+
labelledSwap := unrestrictedSwap
88+
labelledSwap.Label = "test label"
89+
t.Run("labelled swap", func(t *testing.T) {
90+
testLoopOutStore(t, &labelledSwap)
91+
})
92+
8693
}
8794

8895
// testLoopOutStore tests the basic functionality of the current bbolt
@@ -196,27 +203,6 @@ func testLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
196203
// TestLoopInStore tests all the basic functionality of the current bbolt
197204
// swap store.
198205
func TestLoopInStore(t *testing.T) {
199-
tempDirName, err := ioutil.TempDir("", "clientstore")
200-
if err != nil {
201-
t.Fatal(err)
202-
}
203-
defer os.RemoveAll(tempDirName)
204-
205-
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
206-
if err != nil {
207-
t.Fatal(err)
208-
}
209-
210-
// First, verify that an empty database has no active swaps.
211-
swaps, err := store.FetchLoopInSwaps()
212-
if err != nil {
213-
t.Fatal(err)
214-
}
215-
if len(swaps) != 0 {
216-
t.Fatal("expected empty store")
217-
}
218-
219-
hash := sha256.Sum256(testPreimage[:])
220206
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
221207

222208
// Next, we'll make a new pending swap that we'll insert into the
@@ -243,6 +229,38 @@ func TestLoopInStore(t *testing.T) {
243229
ExternalHtlc: true,
244230
}
245231

232+
t.Run("loop in", func(t *testing.T) {
233+
testLoopInStore(t, pendingSwap)
234+
})
235+
236+
labelledSwap := pendingSwap
237+
labelledSwap.Label = "test label"
238+
t.Run("loop in with label", func(t *testing.T) {
239+
testLoopInStore(t, labelledSwap)
240+
})
241+
}
242+
243+
func testLoopInStore(t *testing.T, pendingSwap LoopInContract) {
244+
tempDirName, err := ioutil.TempDir("", "clientstore")
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
defer os.RemoveAll(tempDirName)
249+
250+
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
255+
// First, verify that an empty database has no active swaps.
256+
swaps, err := store.FetchLoopInSwaps()
257+
if err != nil {
258+
t.Fatal(err)
259+
}
260+
if len(swaps) != 0 {
261+
t.Fatal("expected empty store")
262+
}
263+
246264
// checkSwap is a test helper function that'll assert the state of a
247265
// swap.
248266
checkSwap := func(expectedState SwapState) {
@@ -269,6 +287,8 @@ func TestLoopInStore(t *testing.T) {
269287
}
270288
}
271289

290+
hash := sha256.Sum256(testPreimage[:])
291+
272292
// If we create a new swap, then it should show up as being initialized
273293
// right after.
274294
if err := store.CreateLoopIn(hash, &pendingSwap); err != nil {

loopin.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/btcsuite/btcd/chaincfg/chainhash"
1414
"github.com/btcsuite/btcd/wire"
1515
"github.com/lightninglabs/lndclient"
16+
"github.com/lightninglabs/loop/labels"
1617
"github.com/lightninglabs/loop/loopdb"
1718
"github.com/lightninglabs/loop/swap"
1819
"github.com/lightningnetwork/lnd/chainntnfs"
@@ -77,6 +78,11 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
7778
currentHeight int32, request *LoopInRequest) (*loopInInitResult,
7879
error) {
7980

81+
// Before we start, check that the label is valid.
82+
if err := labels.Validate(request.Label); err != nil {
83+
return nil, err
84+
}
85+
8086
// Request current server loop in terms and use these to calculate the
8187
// swap fee that we should subtract from the swap amount in the payment
8288
// request that we send to the server.
@@ -165,6 +171,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
165171
CltvExpiry: swapResp.expiry,
166172
MaxMinerFee: request.MaxMinerFee,
167173
MaxSwapFee: request.MaxSwapFee,
174+
Label: request.Label,
168175
},
169176
}
170177

loopout.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/btcsuite/btcd/wire"
1414
"github.com/btcsuite/btcutil"
1515
"github.com/lightninglabs/lndclient"
16+
"github.com/lightninglabs/loop/labels"
1617
"github.com/lightninglabs/loop/loopdb"
1718
"github.com/lightninglabs/loop/swap"
1819
"github.com/lightninglabs/loop/sweep"
@@ -87,6 +88,11 @@ type loopOutInitResult struct {
8788
func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
8889
currentHeight int32, request *OutRequest) (*loopOutInitResult, error) {
8990

91+
// Before we start, check that the label is valid.
92+
if err := labels.Validate(request.Label); err != nil {
93+
return nil, err
94+
}
95+
9096
// Generate random preimage.
9197
var swapPreimage [32]byte
9298
if _, err := rand.Read(swapPreimage[:]); err != nil {
@@ -154,6 +160,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
154160
CltvExpiry: request.Expiry,
155161
MaxMinerFee: request.MaxMinerFee,
156162
MaxSwapFee: request.MaxSwapFee,
163+
Label: request.Label,
157164
},
158165
OutgoingChanSet: chanSet,
159166
}

0 commit comments

Comments
 (0)