Skip to content

Commit e5c4a4c

Browse files
authored
Merge pull request #97 from bottlepay/routing-policy
allow non-zero routing policy
2 parents 12d4c6e + 9af6b56 commit e5c4a4c

File tree

6 files changed

+140
-30
lines changed

6 files changed

+140
-30
lines changed

cmd/lnmuxd/config.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"time"
99

10+
"github.com/bottlepay/lnmux"
1011
"github.com/urfave/cli/v2"
1112
"gopkg.in/yaml.v2"
1213
)
@@ -43,6 +44,22 @@ type Config struct {
4344
Logging LoggingConfig `yaml:"logging"`
4445

4546
DistributedLock DistributedLockConfig `yaml:"distributedLock"`
47+
48+
RoutingPolicy *RoutingPolicyConfig `yaml:"routingPolicy"`
49+
}
50+
51+
type RoutingPolicyConfig struct {
52+
// CltvDelta is the minimally required cltv delta for forwards towards the
53+
// virtual channel.
54+
CltvDelta int64 `yaml:"cltvDelta"`
55+
56+
// FeeBase is the base fee to charge for forwards towards the
57+
// virtual channel.
58+
FeeBaseMsat int64 `yaml:"feeBaseMsat"`
59+
60+
// FeeRate is the fee rate in ppm to charge for forwards towards the virtual
61+
// channel.
62+
FeeRatePpm int64 `yaml:"feeRatePpm"`
4663
}
4764

4865
// LoggingConfig contains options related to log outputs.
@@ -80,6 +97,24 @@ func (c *Config) GetIdentityKey() ([32]byte, error) {
8097
return key, nil
8198
}
8299

100+
var defaultRoutingPolicy = lnmux.RoutingPolicy{
101+
FeeBaseMsat: 0,
102+
FeeRatePpm: 0,
103+
CltvDelta: 40,
104+
}
105+
106+
func (c *Config) GetRoutingPolicy() lnmux.RoutingPolicy {
107+
if c.RoutingPolicy == nil {
108+
return defaultRoutingPolicy
109+
}
110+
111+
return lnmux.RoutingPolicy{
112+
CltvDelta: c.RoutingPolicy.CltvDelta,
113+
FeeBaseMsat: c.RoutingPolicy.FeeBaseMsat,
114+
FeeRatePpm: c.RoutingPolicy.FeeRatePpm,
115+
}
116+
}
117+
83118
type LndConfig struct {
84119
Nodes []struct {
85120
// PubKey is the public key of this node.

cmd/lnmuxd/run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ func run(ctx context.Context, cfg *Config) error {
131131
"key", key,
132132
"network", activeNetParams.Name)
133133

134+
// Get routing policy.
135+
routingPolicy := cfg.GetRoutingPolicy()
136+
134137
// Get a new creator instance.
135138
var gwPubKeys []common.PubKey
136139
for _, lnd := range lnds {
@@ -141,6 +144,7 @@ func run(ctx context.Context, cfg *Config) error {
141144
KeyRing: keyRing,
142145
GwPubKeys: gwPubKeys,
143146
ActiveNetParams: activeNetParams,
147+
RoutingPolicy: routingPolicy,
144148
},
145149
)
146150
if err != nil {
@@ -177,6 +181,7 @@ func run(ctx context.Context, cfg *Config) error {
177181
Logger: log,
178182
Registry: registry,
179183
SettledHandler: settledHandler,
184+
RoutingPolicy: routingPolicy,
180185
})
181186
if err != nil {
182187
return err

interceptor.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ type queuedReply struct {
7676
}
7777

7878
func (i *interceptor) start(ctx context.Context) error {
79-
var cancel func()
80-
ctx, cancel = context.WithCancel(ctx)
79+
var wg sync.WaitGroup
80+
defer wg.Wait()
81+
82+
ctx, cancel := context.WithCancel(ctx)
8183
defer cancel()
8284

8385
send, recv, err := i.lnd.HtlcInterceptor(ctx)
@@ -117,9 +119,6 @@ func (i *interceptor) start(ctx context.Context) error {
117119
replyChan = make(chan queuedReply, resolutionQueueSize)
118120
)
119121

120-
var wg sync.WaitGroup
121-
defer wg.Wait()
122-
123122
wg.Add(1)
124123
go func(ctx context.Context) {
125124
defer wg.Done()
@@ -233,7 +232,8 @@ func (i *interceptor) htlcReceiveLoop(ctx context.Context,
233232
onionBlob: htlc.OnionBlob,
234233
incomingAmountMsat: htlc.IncomingAmountMsat,
235234
outgoingAmountMsat: htlc.OutgoingAmountMsat,
236-
expiry: htlc.OutgoingExpiry,
235+
incomingExpiry: htlc.IncomingExpiry,
236+
outgoingExpiry: htlc.OutgoingExpiry,
237237
outgoingChanID: htlc.OutgoingRequestedChanId,
238238
reply: reply,
239239
}:

invoice_creator.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,17 @@ type InvoiceCreatorConfig struct {
3737
KeyRing keychain.SecretKeyRing
3838
GwPubKeys []common.PubKey
3939
ActiveNetParams *chaincfg.Params
40+
41+
// RoutingPolicy is the policy that is used for the hop hints.
42+
RoutingPolicy RoutingPolicy
4043
}
4144

4245
type InvoiceCreator struct {
4346
keyRing keychain.SecretKeyRing
4447
gwPubKeys []*btcec.PublicKey
4548
activeNetParams *chaincfg.Params
4649
idKeyDesc keychain.KeyDescriptor
50+
routingPolicy RoutingPolicy
4751
}
4852

4953
func NewInvoiceCreator(cfg *InvoiceCreatorConfig) (*InvoiceCreator, error) {
@@ -71,6 +75,7 @@ func NewInvoiceCreator(cfg *InvoiceCreatorConfig) (*InvoiceCreator, error) {
7175
activeNetParams: cfg.ActiveNetParams,
7276
keyRing: cfg.KeyRing,
7377
idKeyDesc: idKeyDesc,
78+
routingPolicy: cfg.RoutingPolicy,
7479
}, nil
7580
}
7681

@@ -172,10 +177,10 @@ func (c *InvoiceCreator) Create(amtMSat int64, expiry time.Duration,
172177

173178
hopHint := zpay32.HopHint{
174179
NodeID: gwPubKey,
175-
ChannelID: virtualChannel, // Rotate?
176-
FeeBaseMSat: 0,
177-
FeeProportionalMillionths: 0,
178-
CLTVExpiryDelta: 40, // Can be zero?
180+
ChannelID: virtualChannel,
181+
FeeBaseMSat: uint32(c.routingPolicy.FeeBaseMsat),
182+
FeeProportionalMillionths: uint32(c.routingPolicy.FeeRatePpm),
183+
CLTVExpiryDelta: uint16(c.routingPolicy.CltvDelta),
179184
}
180185

181186
options = append(options, zpay32.RouteHint([]zpay32.HopHint{hopHint}))

mux.go

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Mux struct {
2929
logger *zap.SugaredLogger
3030

3131
settledHandler *SettledHandler
32+
routingPolicy RoutingPolicy
3233
}
3334

3435
type MuxConfig struct {
@@ -39,6 +40,16 @@ type MuxConfig struct {
3940
Lnd []lnd.LndClient
4041
Logger *zap.SugaredLogger
4142
Registry *InvoiceRegistry
43+
44+
// RoutingPolicy is the policy that is enforced for the hop towards the
45+
// virtual channel.
46+
RoutingPolicy RoutingPolicy
47+
}
48+
49+
type RoutingPolicy struct {
50+
CltvDelta int64
51+
FeeBaseMsat int64
52+
FeeRatePpm int64
4253
}
4354

4455
func New(cfg *MuxConfig) (*Mux,
@@ -70,6 +81,7 @@ func New(cfg *MuxConfig) (*Mux,
7081
lnd: cfg.Lnd,
7182
logger: cfg.Logger,
7283
settledHandler: cfg.SettledHandler,
84+
routingPolicy: cfg.RoutingPolicy,
7385
}, nil
7486
}
7587

@@ -95,7 +107,8 @@ type interceptedHtlc struct {
95107
onionBlob []byte
96108
incomingAmountMsat uint64
97109
outgoingAmountMsat uint64
98-
expiry uint32
110+
incomingExpiry uint32
111+
outgoingExpiry uint32
99112
outgoingChanID uint64
100113

101114
reply func(*interceptedHtlcResponse) error
@@ -109,12 +122,17 @@ type interceptedHtlcResponse struct {
109122
}
110123

111124
func (p *Mux) run(mainCtx context.Context) error {
112-
ctx, cancel := context.WithCancel(mainCtx)
113-
defer cancel()
125+
p.logger.Infow("Routing policy",
126+
"cltvDelta", p.routingPolicy.CltvDelta,
127+
"feeBaseMsat", p.routingPolicy.FeeBaseMsat,
128+
"feeRatePpm", p.routingPolicy.FeeRatePpm)
114129

115130
var wg sync.WaitGroup
116131
defer wg.Wait()
117132

133+
ctx, cancel := context.WithCancel(mainCtx)
134+
defer cancel()
135+
118136
// Register for htlc interception and block events.
119137
htlcChan := make(chan *interceptedHtlc)
120138
heightChan := make(chan int)
@@ -192,10 +210,11 @@ func marshallFailureCode(code lnwire.FailCode) (
192210
case lnwire.CodeInvalidOnionKey:
193211
return lnrpc.Failure_INVALID_ONION_KEY, nil
194212

195-
case lnwire.CodeFeeInsufficient:
196-
// Unfortunately lnrpc.Failure_FEE_INSUFFICIENT is not supported by lnd.
197-
// Return 0, which is mapped to TemporaryChannelFailure.
198-
213+
// Unfortunately these codes are not supported by lnd. Return 0, which is
214+
// mapped to TemporaryChannelFailure.
215+
//
216+
// See https://github.com/lightningnetwork/lnd/pull/7067
217+
case lnwire.CodeFeeInsufficient, lnwire.CodeIncorrectCltvExpiry:
199218
return 0, nil
200219

201220
default:
@@ -229,13 +248,26 @@ func (p *Mux) ProcessHtlc(
229248
}
230249

231250
// Verify that the amount of the incoming htlc is at least what is forwarded
232-
// over the virtual channel.
233-
if htlc.incomingAmountMsat < htlc.outgoingAmountMsat {
234-
logger.Debugw("Insufficient incoming htlc amount")
251+
// over the virtual channel plus fee.
252+
//
253+
// TODO: Fee accounting for successful payments.
254+
expectedFee := uint64(p.routingPolicy.FeeBaseMsat) +
255+
(uint64(p.routingPolicy.FeeRatePpm)*htlc.outgoingAmountMsat)/1e6
256+
257+
if htlc.incomingAmountMsat < htlc.outgoingAmountMsat+expectedFee {
258+
logger.Debugw("Insufficient incoming htlc amount",
259+
"expectedFee", expectedFee)
235260

236261
return fail(lnwire.CodeFeeInsufficient)
237262
}
238263

264+
// Verify that the cltv delta is sufficiently large.
265+
if htlc.incomingExpiry < htlc.outgoingExpiry+uint32(p.routingPolicy.CltvDelta) {
266+
logger.Debugw("Cltv delta insufficient")
267+
268+
return fail(lnwire.CodeIncorrectCltvExpiry)
269+
}
270+
239271
// Try decode final hop onion. Expiry can be set to zero, because the
240272
// replay log is disabled.
241273
onionReader := bytes.NewReader(htlc.onionBlob)
@@ -278,7 +310,7 @@ func (p *Mux) ProcessHtlc(
278310
}
279311

280312
// Verify that the amount going out over the virtual channel matches what
281-
// the sender intended.
313+
// the sender intended. See BOLT 04.
282314
if uint64(payload.ForwardingInfo().AmountToForward) !=
283315
htlc.outgoingAmountMsat {
284316

@@ -289,6 +321,16 @@ func (p *Mux) ProcessHtlc(
289321
})
290322
}
291323

324+
// Verify that the expiry going out over the virtual channel matches what
325+
// the sender intended. See BOLT 04.
326+
if uint64(payload.ForwardingInfo().OutgoingCTLV) !=
327+
uint64(htlc.outgoingExpiry) {
328+
329+
logger.Debugw("Final expiry mismatch")
330+
331+
return failLocal(&lnwire.FailFinalExpiryTooSoon{})
332+
}
333+
292334
resolve := func(resolution HtlcResolution) error {
293335
// Determine required action for the resolution based on the type of
294336
// resolution we have received.
@@ -331,7 +373,7 @@ func (p *Mux) ProcessHtlc(
331373
&registryHtlc{
332374
rHash: htlc.hash,
333375
amtPaid: lnwire.MilliSatoshi(htlc.outgoingAmountMsat),
334-
expiry: htlc.expiry,
376+
expiry: htlc.outgoingExpiry,
335377
currentHeight: int32(height),
336378
circuitKey: htlc.circuitKey,
337379
payload: payload,

mux_test.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,18 @@ func TestMux(t *testing.T) {
118118
db, dropDB := setupTestDB(t)
119119
defer dropDB()
120120

121+
routingPolicy := RoutingPolicy{
122+
FeeBaseMsat: 1000,
123+
FeeRatePpm: 10000,
124+
CltvDelta: 10,
125+
}
126+
121127
creator, err := NewInvoiceCreator(
122128
&InvoiceCreatorConfig{
123129
KeyRing: keyRing,
124130
GwPubKeys: []common.PubKey{testPubKey1, testPubKey2},
125131
ActiveNetParams: activeNetParams,
132+
RoutingPolicy: routingPolicy,
126133
},
127134
)
128135
require.NoError(t, err)
@@ -159,6 +166,7 @@ func TestMux(t *testing.T) {
159166
Logger: logger.Sugar(),
160167
Registry: registry,
161168
SettledHandler: settledHandler,
169+
RoutingPolicy: routingPolicy,
162170
})
163171
require.NoError(t, err)
164172

@@ -199,13 +207,15 @@ func TestMux(t *testing.T) {
199207
require.NoError(t, err)
200208

201209
receiveHtlcInOut := func(sourceNodeIdx int, htlcID uint64,
202-
incomingAmt, outgoingAmt, amtToForward uint64) {
210+
incomingAmt, outgoingAmt, amtToForward uint64,
211+
inExpiry, outExpiry, payloadExpiry uint32) {
203212

204213
route := &route.Route{
205214
Hops: []*route.Hop{
206215
{
207-
AmtToForward: lnwire.MilliSatoshi(amtToForward),
208-
PubKeyBytes: dest,
216+
AmtToForward: lnwire.MilliSatoshi(amtToForward),
217+
OutgoingTimeLock: payloadExpiry,
218+
PubKeyBytes: dest,
209219
MPP: record.NewMPP(
210220
invoice.Value, invoice.PaymentAddr,
211221
),
@@ -226,10 +236,10 @@ func TestMux(t *testing.T) {
226236
l[sourceNodeIdx].htlcChan <- &routerrpc.ForwardHtlcInterceptRequest{
227237
IncomingCircuitKey: &routerrpc.CircuitKey{HtlcId: htlcID},
228238
PaymentHash: testHash[:],
229-
IncomingExpiry: 1050,
239+
IncomingExpiry: inExpiry,
230240
IncomingAmountMsat: incomingAmt,
231241
OutgoingAmountMsat: outgoingAmt,
232-
OutgoingExpiry: 1040,
242+
OutgoingExpiry: outExpiry,
233243
OnionBlob: onionBlob[:],
234244
OutgoingRequestedChanId: virtualChannel,
235245
}
@@ -238,8 +248,13 @@ func TestMux(t *testing.T) {
238248
receiveHtlc := func(sourceNodeIdx int, htlcID uint64,
239249
outgoingAmt uint64) {
240250

251+
// Calculate required routing fee.
252+
incomingAmt := outgoingAmt + uint64(routingPolicy.FeeBaseMsat+
253+
(routingPolicy.FeeRatePpm*int64(outgoingAmt))/1e6)
254+
241255
receiveHtlcInOut(
242-
sourceNodeIdx, htlcID, outgoingAmt, outgoingAmt, outgoingAmt,
256+
sourceNodeIdx, htlcID, incomingAmt, outgoingAmt, outgoingAmt,
257+
1050, 1040, 1040,
243258
)
244259
}
245260

@@ -253,11 +268,19 @@ func TestMux(t *testing.T) {
253268
}
254269

255270
// Test an htlc with an amount that is not high enough.
256-
receiveHtlcInOut(0, 20, 5000, 6000, 6000)
271+
receiveHtlcInOut(0, 20, 6000+1060-1, 6000, 6000, 1050, 1040, 1040)
257272
expectResponse(<-l[0].responseChan, 20, routerrpc.ResolveHoldForwardAction_FAIL)
258273

259274
// Test an htlc with an amount mismatch in the payload.
260-
receiveHtlcInOut(0, 20, 6000, 6000, 5900)
275+
receiveHtlcInOut(0, 20, 6000+1060, 6000, 5900, 1050, 1040, 1040)
276+
expectResponse(<-l[0].responseChan, 20, routerrpc.ResolveHoldForwardAction_FAIL)
277+
278+
// Test an htlc with a cltv delta that is too small.
279+
receiveHtlcInOut(0, 20, 6000+1060, 6000, 6000, 1049, 1040, 1040)
280+
expectResponse(<-l[0].responseChan, 20, routerrpc.ResolveHoldForwardAction_FAIL)
281+
282+
// Test an htlc with a cltv delta mismatch in the payload.
283+
receiveHtlcInOut(0, 20, 6000+1060, 6000, 6000, 1051, 1041, 1040)
261284
expectResponse(<-l[0].responseChan, 20, routerrpc.ResolveHoldForwardAction_FAIL)
262285

263286
// Notify arrival of part 1.

0 commit comments

Comments
 (0)