Skip to content

Commit 2788204

Browse files
authored
Merge pull request #75 from halseth/batcher-impl
SwapPublicationDeadline for LoopOut contracts
2 parents a6bb2e5 + 7ea0e35 commit 2788204

19 files changed

+347
-167
lines changed

cmd/loop/loopin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func loopIn(ctx *cli.Context) error {
6868
}
6969

7070
limits := getInLimits(amt, quote)
71-
err = displayLimits(swap.TypeIn, amt, limits, external)
71+
err = displayLimits(swap.TypeIn, amt, limits, external, "")
7272
if err != nil {
7373
return err
7474
}

cmd/loop/loopout.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/lightninglabs/loop"
89
"github.com/lightninglabs/loop/looprpc"
@@ -44,6 +45,16 @@ var loopOutCommand = cli.Command{
4445
"should be swept within",
4546
Value: uint64(loop.DefaultSweepConfTarget),
4647
},
48+
cli.BoolFlag{
49+
Name: "fast",
50+
Usage: "Indicate you want to swap immediately, " +
51+
"paying potentially a higher fee. If not " +
52+
"set the swap server might choose to wait up " +
53+
"to 30 minutes before publishing the swap " +
54+
"HTLC on-chain, to save on chain fees. Not " +
55+
"setting this flag might result in a lower " +
56+
"swap fee.",
57+
},
4758
},
4859
Action: loopOut,
4960
}
@@ -92,9 +103,19 @@ func loopOut(ctx *cli.Context) error {
92103
return err
93104
}
94105

95-
limits := getLimits(amt, quote)
106+
// Show a warning if a slow swap was requested.
107+
fast := ctx.Bool("fast")
108+
warning := ""
109+
if fast {
110+
warning = "Fast swap requested."
111+
} else {
112+
warning = fmt.Sprintf("Regular swap speed requested, it "+
113+
"might take up to %v for the swap to be executed.",
114+
defaultSwapWaitTime)
115+
}
96116

97-
err = displayLimits(swap.TypeOut, amt, limits, false)
117+
limits := getLimits(amt, quote)
118+
err = displayLimits(swap.TypeOut, amt, limits, false, warning)
98119
if err != nil {
99120
return err
100121
}
@@ -104,16 +125,24 @@ func loopOut(ctx *cli.Context) error {
104125
unchargeChannel = ctx.Uint64("channel")
105126
}
106127

128+
// Set our maximum swap wait time. If a fast swap is requested we set
129+
// it to now, otherwise to 30 minutes in the future.
130+
swapDeadline := time.Now()
131+
if !fast {
132+
swapDeadline = time.Now().Add(defaultSwapWaitTime)
133+
}
134+
107135
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
108-
Amt: int64(amt),
109-
Dest: destAddr,
110-
MaxMinerFee: int64(limits.maxMinerFee),
111-
MaxPrepayAmt: int64(*limits.maxPrepayAmt),
112-
MaxSwapFee: int64(limits.maxSwapFee),
113-
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
114-
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
115-
LoopOutChannel: unchargeChannel,
116-
SweepConfTarget: sweepConfTarget,
136+
Amt: int64(amt),
137+
Dest: destAddr,
138+
MaxMinerFee: int64(limits.maxMinerFee),
139+
MaxPrepayAmt: int64(*limits.maxPrepayAmt),
140+
MaxSwapFee: int64(limits.maxSwapFee),
141+
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
142+
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
143+
LoopOutChannel: unchargeChannel,
144+
SweepConfTarget: sweepConfTarget,
145+
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
117146
})
118147
if err != nil {
119148
return err

cmd/loop/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var (
2626
maxRoutingFeeBase = btcutil.Amount(10)
2727

2828
maxRoutingFeeRate = int64(20000)
29+
30+
defaultSwapWaitTime = 30 * time.Minute
2931
)
3032

3133
func printRespJSON(resp proto.Message) {
@@ -117,7 +119,7 @@ func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
117119
}
118120

119121
func displayLimits(swapType swap.Type, amt btcutil.Amount, l *limits,
120-
externalHtlc bool) error {
122+
externalHtlc bool, warning string) error {
121123

122124
totalSuccessMax := l.maxMinerFee + l.maxSwapFee
123125
if l.maxSwapRoutingFee != nil {
@@ -138,6 +140,10 @@ func displayLimits(swapType swap.Type, amt btcutil.Amount, l *limits,
138140
amt, swapType, totalSuccessMax,
139141
)
140142

143+
if warning != "" {
144+
fmt.Println(warning)
145+
}
146+
141147
fmt.Printf("CONTINUE SWAP? (y/n), expand fee detail (x): ")
142148

143149
var answer string

cmd/loopd/swapclient_server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"sort"
8+
"time"
89

910
"github.com/lightningnetwork/lnd/queue"
1011

@@ -76,6 +77,9 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
7677
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
7778
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
7879
SweepConfTarget: sweepConfTarget,
80+
SwapPublicationDeadline: time.Unix(
81+
int64(in.SwapPublicationDeadline), 0,
82+
),
7983
}
8084
if in.LoopOutChannel != 0 {
8185
req.LoopOutChannel = &in.LoopOutChannel

interface.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ type OutRequest struct {
6464
SweepConfTarget int32
6565

6666
// LoopOutChannel optionally specifies the short channel id of the
67-
// channel to uncharge.
67+
// channel to loop out.
6868
LoopOutChannel *uint64
69+
70+
// SwapPublicationDeadline can be set by the client to allow the server
71+
// delaying publication of the swap HTLC to save on chain fees.
72+
SwapPublicationDeadline time.Time
6973
}
7074

7175
// Out contains the full details of a loop out request. This includes things
@@ -149,15 +153,15 @@ type LoopInRequest struct {
149153
// MaxSwapFee is the maximum we are willing to pay the server for the
150154
// swap. This value is not disclosed in the swap initiation call, but if
151155
// the server asks for a higher fee, we abort the swap. Typically this
152-
// value is taken from the response of the UnchargeQuote call. It
156+
// value is taken from the response of the LoopInQuote call. It
153157
// includes the prepay amount.
154158
MaxSwapFee btcutil.Amount
155159

156160
// MaxMinerFee is the maximum in on-chain fees that we are willing to
157161
// spent. If we publish the on-chain htlc and the fee estimate turns out
158162
// higher than this value, we cancel the swap.
159163
//
160-
// MaxMinerFee is typically taken from the response of the UnchargeQuote
164+
// MaxMinerFee is typically taken from the response of the LoopInQuote
161165
// call.
162166
MaxMinerFee btcutil.Amount
163167

@@ -166,15 +170,15 @@ type LoopInRequest struct {
166170
HtlcConfTarget int32
167171

168172
// LoopInChannel optionally specifies the short channel id of the
169-
// channel to charge.
173+
// channel to loop in.
170174
LoopInChannel *uint64
171175

172176
// ExternalHtlc specifies whether the htlc is published by an external
173177
// source.
174178
ExternalHtlc bool
175179
}
176180

177-
// LoopInTerms are the server terms on which it executes charge swaps.
181+
// LoopInTerms are the server terms on which it executes loop in swaps.
178182
type LoopInTerms struct {
179183
// MinSwapAmount is the minimum amount that the server requires for a
180184
// swap.

loopdb/loopout.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ type LoopOutContract struct {
4545
// MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be
4646
// paid for the prepayment to the server.
4747
MaxPrepayRoutingFee btcutil.Amount
48+
49+
// SwapPublicationDeadline is a timestamp that the server commits to
50+
// have the on-chain swap published by. It is set by the client to
51+
// allow the server to delay the publication in exchange for possibly
52+
// lower fees.
53+
SwapPublicationDeadline time.Time
4854
}
4955

5056
// LoopOut is a combination of the contract and the updates.
@@ -158,6 +164,13 @@ func deserializeLoopOutContract(value []byte, chainParams *chaincfg.Params) (
158164
contract.UnchargeChannel = &unchargeChannel
159165
}
160166

167+
var deadlineNano int64
168+
err = binary.Read(r, byteOrder, &deadlineNano)
169+
if err != nil {
170+
return nil, err
171+
}
172+
contract.SwapPublicationDeadline = time.Unix(0, deadlineNano)
173+
161174
return &contract, nil
162175
}
163176

@@ -243,5 +256,10 @@ func serializeLoopOutContract(swap *LoopOutContract) (
243256
return nil, err
244257
}
245258

259+
err = binary.Write(&b, byteOrder, swap.SwapPublicationDeadline.UnixNano())
260+
if err != nil {
261+
return nil, err
262+
}
263+
246264
return b.Bytes(), nil
247265
}

loopdb/meta.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/btcsuite/btcd/chaincfg"
78
"github.com/coreos/bbolt"
89
)
910

@@ -24,14 +25,17 @@ var (
2425
// migration is a function which takes a prior outdated version of the database
2526
// instances and mutates the key/bucket structure to arrive at a more
2627
// up-to-date version of the database.
27-
type migration func(tx *bbolt.Tx) error
28+
type migration func(tx *bbolt.Tx, chainParams *chaincfg.Params) error
2829

2930
var (
3031
// dbVersions is storing all versions of database. If current version
3132
// of database don't match with latest version this list will be used
3233
// for retrieving all migration function that are need to apply to the
3334
// current db.
34-
migrations = []migration{migrateCosts}
35+
migrations = []migration{
36+
migrateCosts,
37+
migrateSwapPublicationDeadline,
38+
}
3539

3640
latestDBVersion = uint32(len(migrations))
3741
)
@@ -76,7 +80,7 @@ func setDBVersion(tx *bbolt.Tx, version uint32) error {
7680
// syncVersions function is used for safe db version synchronization. It
7781
// applies migration functions to the current database and recovers the
7882
// previous state of db if at least one error/panic appeared during migration.
79-
func syncVersions(db *bbolt.DB) error {
83+
func syncVersions(db *bbolt.DB, chainParams *chaincfg.Params) error {
8084
currentVersion, err := getDBVersion(db)
8185
if err != nil {
8286
return err
@@ -112,7 +116,7 @@ func syncVersions(db *bbolt.DB) error {
112116
log.Infof("Applying migration #%v", v+1)
113117

114118
migration := migrations[v]
115-
if err := migration(tx); err != nil {
119+
if err := migration(tx, chainParams); err != nil {
116120
log.Infof("Unable to apply migration #%v",
117121
v+1)
118122
return err

loopdb/migration_01_costs.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/btcsuite/btcd/chaincfg"
78
"github.com/coreos/bbolt"
89
)
910

1011
// noMigrationAvailable is the fall back migration in case there is no migration
1112
// implemented.
12-
func migrateCosts(tx *bbolt.Tx) error {
13+
func migrateCosts(tx *bbolt.Tx, _ *chaincfg.Params) error {
1314
if err := migrateCostsForBucket(tx, loopInBucketKey); err != nil {
1415
return err
1516
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
// migrateSwapPublicationDeadline migrates the database to v02, by adding the
13+
// SwapPublicationDeadline field to loop out contracts.
14+
func migrateSwapPublicationDeadline(tx *bbolt.Tx, chainParams *chaincfg.Params) error {
15+
rootBucket := tx.Bucket(loopOutBucketKey)
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+
// Write the current contract serialization into a buffer.
43+
b := &bytes.Buffer{}
44+
if _, err := b.Write(contractBytes); err != nil {
45+
return err
46+
}
47+
48+
// We migrate to the new format by copying the first 8 bytes
49+
// (the creation time) to the end (the swap deadline)
50+
var swapDeadline [8]byte
51+
copy(swapDeadline[:], contractBytes[:8])
52+
if _, err := b.Write(swapDeadline[:]); err != nil {
53+
return err
54+
}
55+
56+
return swapBucket.Put(contractKey, b.Bytes())
57+
})
58+
}

loopdb/store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) (
135135

136136
// Finally, before we start, we'll sync the DB versions to pick up any
137137
// possible DB migrations.
138-
err = syncVersions(bdb)
138+
err = syncVersions(bdb, chainParams)
139139
if err != nil {
140140
return nil, err
141141
}

loopdb/store_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ func TestLoopOutStore(t *testing.T) {
8282
// doesn't interfere with DeepEqual.
8383
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
8484
},
85-
MaxPrepayRoutingFee: 40,
86-
PrepayInvoice: "prepayinvoice",
87-
DestAddr: destAddr,
88-
SwapInvoice: "swapinvoice",
89-
MaxSwapRoutingFee: 30,
90-
SweepConfTarget: 2,
85+
MaxPrepayRoutingFee: 40,
86+
PrepayInvoice: "prepayinvoice",
87+
DestAddr: destAddr,
88+
SwapInvoice: "swapinvoice",
89+
MaxSwapRoutingFee: 30,
90+
SweepConfTarget: 2,
91+
SwapPublicationDeadline: time.Unix(0, initiationTime.UnixNano()),
9192
}
9293

9394
// checkSwap is a test helper function that'll assert the state of a

0 commit comments

Comments
 (0)