Skip to content

Commit 5e70077

Browse files
committed
blindedpath: allow zero amount paths with MinHTLC policies
When building blinded paths for zero amount payments (where the sender decides the amount), skip MinHTLC validation during path construction. Zero amount payments indicate the final amount will be determined by the sender, so MinHTLC constraints should be validated at payment time rather than path construction time. This enables creation of blinded path invoices for zero amount payments even when intermediate channels have MinHTLC policies greater than zero.
1 parent b5c84ea commit 5e70077

File tree

2 files changed

+116
-4
lines changed

2 files changed

+116
-4
lines changed

routing/blindedpath/blinded_path.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,11 @@ func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
438438
}
439439
}
440440

441-
if policy.MinHTLCMsat > cfg.ValueMsat {
441+
// If the payment amount is 0, means the sender is deciding the
442+
// amount, so we don't need to check the min HTLC value while
443+
// building the path. Payment would fail if there is no
444+
// route with the min HTLC value from the sender's perspective.
445+
if cfg.ValueMsat > 0 && policy.MinHTLCMsat > cfg.ValueMsat {
442446
return nil, 0, 0, fmt.Errorf("%w: minHTLC of hop "+
443447
"policy larger than payment amt: sentAmt(%v), "+
444448
"minHTLC(%v)", errInvalidBlindedPath,
@@ -450,12 +454,13 @@ func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
450454
return nil, 0, 0, err
451455
}
452456

453-
// We only use the new buffered policy if the new minHTLC value
454-
// does not violate the sender amount.
457+
// We only use the new buffered policy if:
458+
// 1) Sender amount is 0, or
459+
// 2) The new minHTLC value does not violate the sender amount.
455460
//
456461
// NOTE: We don't check this for maxHTLC, because the payment
457462
// amount can always be splitted using MPP.
458-
if bufferPolicy.MinHTLCMsat <= cfg.ValueMsat {
463+
if cfg.ValueMsat == 0 || bufferPolicy.MinHTLCMsat <= cfg.ValueMsat {
459464
policy = bufferPolicy
460465
}
461466

routing/blindedpath/blinded_path_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,110 @@ func decryptAndDecodeHopData(t *testing.T, priv *btcec.PrivateKey,
10591059

10601060
return routeData, nextEphem
10611061
}
1062+
1063+
// TestBuildBlindedPathZeroAmount tests that blinded paths can be built
1064+
// successfully when ValueMsat is 0 (zero amount), even when channels have
1065+
// MinHTLC policies > 0. This is important for building blinded paths invoices
1066+
// for zero amount payments.
1067+
func TestBuildBlindedPathZeroAmount(t *testing.T) {
1068+
// Alice chooses the following path to herself for blinded path
1069+
// construction:
1070+
// Carol -> Bob -> Alice.
1071+
var (
1072+
_, pkC = btcec.PrivKeyFromBytes([]byte{1})
1073+
_, pkB = btcec.PrivKeyFromBytes([]byte{2})
1074+
_, pkA = btcec.PrivKeyFromBytes([]byte{3})
1075+
1076+
carol = route.NewVertex(pkC)
1077+
bob = route.NewVertex(pkB)
1078+
alice = route.NewVertex(pkA)
1079+
1080+
chanCB = uint64(1)
1081+
chanBA = uint64(2)
1082+
)
1083+
1084+
realRoute := &route.Route{
1085+
SourcePubKey: carol,
1086+
Hops: []*route.Hop{
1087+
{
1088+
PubKeyBytes: bob,
1089+
ChannelID: chanCB,
1090+
},
1091+
{
1092+
PubKeyBytes: alice,
1093+
ChannelID: chanBA,
1094+
},
1095+
},
1096+
}
1097+
1098+
realPolicies := map[uint64]*models.ChannelEdgePolicy{
1099+
chanCB: {
1100+
ChannelID: chanCB,
1101+
ToNode: bob,
1102+
MinHTLC: 1000, // MinHTLC > 0
1103+
MaxHTLC: lnwire.MaxMilliSatoshi,
1104+
FeeBaseMSat: 100,
1105+
FeeProportionalMillionths: 500,
1106+
TimeLockDelta: 144,
1107+
},
1108+
chanBA: {
1109+
ChannelID: chanBA,
1110+
ToNode: alice,
1111+
MinHTLC: 500, // MinHTLC > 0
1112+
MaxHTLC: lnwire.MaxMilliSatoshi,
1113+
FeeBaseMSat: 50,
1114+
FeeProportionalMillionths: 250,
1115+
TimeLockDelta: 72,
1116+
},
1117+
}
1118+
1119+
paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{
1120+
FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
1121+
error) {
1122+
1123+
return []*route.Route{realRoute}, nil
1124+
},
1125+
FetchChannelEdgesByID: func(chanID uint64) (
1126+
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
1127+
*models.ChannelEdgePolicy, error) {
1128+
1129+
return nil, realPolicies[chanID], nil, nil
1130+
},
1131+
BestHeight: func() (uint32, error) {
1132+
return 1000, nil
1133+
},
1134+
AddPolicyBuffer: func(policy *BlindedHopPolicy) (
1135+
*BlindedHopPolicy, error) {
1136+
// We don't mind about maxHTLCs
1137+
return AddPolicyBuffer(policy, 1.5, 0.0)
1138+
},
1139+
PathID: []byte{1, 2, 3},
1140+
ValueMsat: 0,
1141+
MinFinalCLTVExpiryDelta: 12,
1142+
BlocksUntilExpiry: 200,
1143+
})
1144+
1145+
require.NoError(t, err)
1146+
require.Len(t, paths, 1, "Should return exactly one blinded path")
1147+
1148+
path := paths[0]
1149+
require.Len(
1150+
t,
1151+
path.Hops, 3,
1152+
"Path should have 3 hops (intro + 2 relay hops)",
1153+
)
1154+
1155+
hop := path.Hops[0]
1156+
require.True(
1157+
t,
1158+
hop.BlindedNodePub.IsEqual(pkC),
1159+
"First hop should be Carol (introduction node)",
1160+
)
1161+
1162+
require.EqualValues(
1163+
t,
1164+
path.HTLCMinMsat,
1165+
uint64(1500),
1166+
"Path MinHTLC should be 1500 with 1.5x policy buffer applied",
1167+
)
1168+
}

0 commit comments

Comments
 (0)