Skip to content

Commit 49ba4f1

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 49ba4f1

File tree

2 files changed

+111
-1
lines changed

2 files changed

+111
-1
lines changed

routing/blindedpath/blinded_path.go

Lines changed: 5 additions & 1 deletion
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 policy.MinHTLCMsat > cfg.ValueMsat && cfg.ValueMsat > 0 {
442446
return nil, 0, 0, fmt.Errorf("%w: minHTLC of hop "+
443447
"policy larger than payment amt: sentAmt(%v), "+
444448
"minHTLC(%v)", errInvalidBlindedPath,

routing/blindedpath/blinded_path_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,109 @@ 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+
1137+
return policy, nil
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+
require.EqualValues(
1162+
t,
1163+
path.HTLCMinMsat,
1164+
uint64(1000),
1165+
"Path MinHTLC should be 1000 based on channel policies",
1166+
)
1167+
}

0 commit comments

Comments
 (0)