Skip to content

Commit b785254

Browse files
authored
Merge pull request #152 from joostjager/loop-in-last-hop
multi: allow loop in last hop restriction
2 parents 8771d06 + b69e8cb commit b785254

17 files changed

+363
-587
lines changed

client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,9 @@ func (s *Client) waitForInitialized(ctx context.Context) error {
437437
func (s *Client) LoopIn(globalCtx context.Context,
438438
request *LoopInRequest) (*lntypes.Hash, btcutil.Address, error) {
439439

440-
log.Infof("Loop in %v (channel: %v)",
440+
log.Infof("Loop in %v (last hop: %v)",
441441
request.Amount,
442-
request.LoopInChannel,
442+
request.LastHop,
443443
)
444444

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

cmd/loop/loopin.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,36 @@ import (
77
"github.com/btcsuite/btcutil"
88
"github.com/lightninglabs/loop/looprpc"
99
"github.com/lightninglabs/loop/swap"
10+
"github.com/lightningnetwork/lnd/routing/route"
1011
"github.com/urfave/cli"
1112
)
1213

13-
var loopInCommand = cli.Command{
14-
Name: "in",
15-
Usage: "perform an on-chain to off-chain swap (loop in)",
16-
ArgsUsage: "amt",
17-
Description: `
14+
var (
15+
lastHopFlag = cli.StringFlag{
16+
Name: "last_hop",
17+
Usage: "the pubkey of the last hop to use for this swap",
18+
}
19+
20+
loopInCommand = cli.Command{
21+
Name: "in",
22+
Usage: "perform an on-chain to off-chain swap (loop in)",
23+
ArgsUsage: "amt",
24+
Description: `
1825
Send the amount in satoshis specified by the amt argument off-chain.`,
19-
Flags: []cli.Flag{
20-
cli.Uint64Flag{
21-
Name: "amt",
22-
Usage: "the amount in satoshis to loop in",
23-
},
24-
cli.BoolFlag{
25-
Name: "external",
26-
Usage: "expect htlc to be published externally",
26+
Flags: []cli.Flag{
27+
cli.Uint64Flag{
28+
Name: "amt",
29+
Usage: "the amount in satoshis to loop in",
30+
},
31+
cli.BoolFlag{
32+
Name: "external",
33+
Usage: "expect htlc to be published externally",
34+
},
35+
lastHopFlag,
2736
},
28-
},
29-
Action: loopIn,
30-
}
37+
Action: loopIn,
38+
}
39+
)
3140

3241
func loopIn(ctx *cli.Context) error {
3342
args := ctx.Args()
@@ -73,12 +82,25 @@ func loopIn(ctx *cli.Context) error {
7382
return err
7483
}
7584

76-
resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
85+
req := &looprpc.LoopInRequest{
7786
Amt: int64(amt),
7887
MaxMinerFee: int64(limits.maxMinerFee),
7988
MaxSwapFee: int64(limits.maxSwapFee),
8089
ExternalHtlc: external,
81-
})
90+
}
91+
92+
if ctx.IsSet(lastHopFlag.Name) {
93+
lastHop, err := route.NewVertexFromStr(
94+
ctx.String(lastHopFlag.Name),
95+
)
96+
if err != nil {
97+
return err
98+
}
99+
100+
req.LastHop = lastHop[:]
101+
}
102+
103+
resp, err := client.LoopIn(context.Background(), req)
82104
if err != nil {
83105
return err
84106
}

interface.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/lightninglabs/loop/loopdb"
88
"github.com/lightninglabs/loop/swap"
99
"github.com/lightningnetwork/lnd/lntypes"
10+
"github.com/lightningnetwork/lnd/routing/route"
1011
)
1112

1213
// OutRequest contains the required parameters for a loop out swap.
@@ -173,9 +174,9 @@ type LoopInRequest struct {
173174
// client htlc tx.
174175
HtlcConfTarget int32
175176

176-
// LoopInChannel optionally specifies the short channel id of the
177-
// channel to loop in.
178-
LoopInChannel *uint64
177+
// LastHop optionally specifies the last hop to use for the loop in
178+
// payment.
179+
LastHop *route.Vertex
179180

180181
// ExternalHtlc specifies whether the htlc is published by an external
181182
// source.

loopd/swapclient_server.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/lightningnetwork/lnd/lntypes"
1212
"github.com/lightningnetwork/lnd/queue"
13+
"github.com/lightningnetwork/lnd/routing/route"
1314

1415
"github.com/lightninglabs/loop"
1516
"github.com/lightninglabs/loop/lndclient"
@@ -387,8 +388,12 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
387388
HtlcConfTarget: defaultConfTarget,
388389
ExternalHtlc: in.ExternalHtlc,
389390
}
390-
if in.LoopInChannel != 0 {
391-
req.LoopInChannel = &in.LoopInChannel
391+
if in.LastHop != nil {
392+
lastHop, err := route.NewVertexFromBytes(in.LastHop)
393+
if err != nil {
394+
return nil, err
395+
}
396+
req.LastHop = &lastHop
392397
}
393398
hash, htlc, err := s.impl.LoopIn(ctx, req)
394399
if err != nil {

loopdb/loopin.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/binary"
66
"fmt"
77
"time"
8+
9+
"github.com/lightningnetwork/lnd/routing/route"
810
)
911

1012
// LoopInContract contains the data that is serialized to persistent storage for
@@ -16,9 +18,8 @@ type LoopInContract struct {
1618
// client sweep tx.
1719
HtlcConfTarget int32
1820

19-
// LoopInChannel is the channel to charge. If zero, any channel may
20-
// be used.
21-
LoopInChannel *uint64
21+
// LastHop is the last hop to use for the loop in swap (optional).
22+
LastHop *route.Vertex
2223

2324
// ExternalHtlc specifies whether the htlc is published by an external
2425
// source.
@@ -96,11 +97,11 @@ func serializeLoopInContract(swap *LoopInContract) (
9697
return nil, err
9798
}
9899

99-
var chargeChannel uint64
100-
if swap.LoopInChannel != nil {
101-
chargeChannel = *swap.LoopInChannel
100+
var lastHop route.Vertex
101+
if swap.LastHop != nil {
102+
lastHop = *swap.LastHop
102103
}
103-
if err := binary.Write(&b, byteOrder, chargeChannel); err != nil {
104+
if err := binary.Write(&b, byteOrder, lastHop[:]); err != nil {
104105
return nil, err
105106
}
106107

@@ -167,12 +168,13 @@ func deserializeLoopInContract(value []byte) (*LoopInContract, error) {
167168
return nil, err
168169
}
169170

170-
var loopInChannel uint64
171-
if err := binary.Read(r, byteOrder, &loopInChannel); err != nil {
171+
var lastHop route.Vertex
172+
if err := binary.Read(r, byteOrder, lastHop[:]); err != nil {
172173
return nil, err
173174
}
174-
if loopInChannel != 0 {
175-
contract.LoopInChannel = &loopInChannel
175+
var noLastHop route.Vertex
176+
if lastHop != noLastHop {
177+
contract.LastHop = &lastHop
176178
}
177179

178180
if err := binary.Read(r, byteOrder, &contract.ExternalHtlc); err != nil {

loopdb/meta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var (
3535
migrations = []migration{
3636
migrateCosts,
3737
migrateSwapPublicationDeadline,
38+
migrateLastHop,
3839
}
3940

4041
latestDBVersion = uint32(len(migrations))

loopdb/migration_03_last_hop.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package loopdb
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/btcsuite/btcd/chaincfg"
9+
"github.com/coreos/bbolt"
10+
)
11+
12+
// migrateLastHop migrates the database to v03, replacing the never used loop in
13+
// channel by a last hop pubkey.
14+
func migrateLastHop(tx *bbolt.Tx, chainParams *chaincfg.Params) error {
15+
rootBucket := tx.Bucket(loopInBucketKey)
16+
if rootBucket == nil {
17+
return errors.New("bucket does not exist")
18+
}
19+
20+
return rootBucket.ForEach(func(swapHash, v []byte) error {
21+
// Only go into things that we know are sub-bucket
22+
// keys.
23+
if v != nil {
24+
return nil
25+
}
26+
27+
// From the root bucket, we'll grab the next swap
28+
// bucket for this swap from its swaphash.
29+
swapBucket := rootBucket.Bucket(swapHash)
30+
if swapBucket == nil {
31+
return fmt.Errorf("swap bucket %x not found",
32+
swapHash)
33+
}
34+
35+
// With the main swap bucket obtained, we'll grab the
36+
// raw swap contract bytes.
37+
contractBytes := swapBucket.Get(contractKey)
38+
if contractBytes == nil {
39+
return errors.New("contract not found")
40+
}
41+
42+
const (
43+
// deprecatedLoopInChannelStart is the starting offset
44+
// of the old loop in channel field: 8 (initiation time)
45+
// + 32 (preimage) + 8 (amount requested) + 33 + 33
46+
// (sender and receiver keys) + 4 (cltv expiry) + 8 + 8
47+
// (max miner and swap fee) + 4 (initiation height) + 4
48+
// (conf target) = 142.
49+
deprecatedLoopInChannelStart = 142
50+
51+
// expectedTotalLength is the expect total length of the
52+
// serialized contract. It adds 8 (old loop in channel)
53+
// + 1 (external htlc) = 9 bytes to
54+
// deprecatedLoopInChannelStart.
55+
expectedTotalLength = deprecatedLoopInChannelStart + 9
56+
)
57+
58+
// Sanity check to see if the constants above match the contract
59+
// bytes.
60+
if len(contractBytes) != expectedTotalLength {
61+
return errors.New("invalid serialized contract length")
62+
}
63+
64+
// Copy the unchanged fields into the buffer.
65+
b := &bytes.Buffer{}
66+
_, err := b.Write(contractBytes[:deprecatedLoopInChannelStart])
67+
if err != nil {
68+
return err
69+
}
70+
71+
// We now set the new last hop field to all zeroes to indicate
72+
// that there is no restriction.
73+
var noLastHop [33]byte
74+
if _, err := b.Write(noLastHop[:]); err != nil {
75+
return err
76+
}
77+
78+
// Append the remaining field ExternalHtlc.
79+
_, err = b.Write(contractBytes[deprecatedLoopInChannelStart+8:])
80+
if err != nil {
81+
return err
82+
}
83+
84+
return swapBucket.Put(contractKey, b.Bytes())
85+
})
86+
}

loopdb/store_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/coreos/bbolt"
1414
"github.com/lightninglabs/loop/test"
1515
"github.com/lightningnetwork/lnd/lntypes"
16+
"github.com/lightningnetwork/lnd/routing/route"
1617
)
1718

1819
var (
@@ -197,7 +198,7 @@ func TestLoopInStore(t *testing.T) {
197198

198199
// Next, we'll make a new pending swap that we'll insert into the
199200
// database shortly.
200-
loopInChannel := uint64(123)
201+
lastHop := route.Vertex{1, 2, 3}
201202

202203
pendingSwap := LoopInContract{
203204
SwapContract: SwapContract{
@@ -215,7 +216,7 @@ func TestLoopInStore(t *testing.T) {
215216
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
216217
},
217218
HtlcConfTarget: 2,
218-
LoopInChannel: &loopInChannel,
219+
LastHop: &lastHop,
219220
ExternalHtlc: true,
220221
}
221222

loopin.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
114114
// htlc.
115115
log.Infof("Initiating swap request at height %v", currentHeight)
116116
swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
117-
request.Amount, senderKey, swapInvoice,
117+
request.Amount, senderKey, swapInvoice, request.LastHop,
118118
)
119119
if err != nil {
120120
return nil, fmt.Errorf("cannot initiate swap: %v", err)
@@ -133,7 +133,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
133133

134134
contract := loopdb.LoopInContract{
135135
HtlcConfTarget: request.HtlcConfTarget,
136-
LoopInChannel: request.LoopInChannel,
136+
LastHop: request.LastHop,
137137
ExternalHtlc: request.ExternalHtlc,
138138
SwapContract: loopdb.SwapContract{
139139
InitiationHeight: currentHeight,

0 commit comments

Comments
 (0)