lnd/simulation/command-center/data/run.json
Olaoluwa Osuntokun a5f295409e simulation/command-center: v66, the boundary and the balance sheet
In this commit, we add findings sections 21 through 23: the foreign
balance sheet (exp-029, the ordering survives on balances nobody fit
to and the circularity caveat measures out to zero), the
misattribution mechanism (exp-030, only settlements prove innocence,
the quarantine keeps), and the closing of the compose world
(exp-031, both escapes failed, the evolution track at its measured
boundary). The index now names the release-candidate pair and states
the two-track split: evolution found the paradigm, engineering is
shipping it.
2026-07-31 00:14:16 -07:00

600 lines
No EOL
1.1 MiB

{
"run_id": "code_full2",
"reflection_lm": "codex:gpt-5.6-sol",
"mode": "generalization",
"status": "complete",
"seed_score": 0.2458,
"best_score": 0.8785,
"iterations": [
{
"i": 0,
"candidate_score": 0.2458,
"best_score": 0.2458,
"note": "seed"
},
{
"i": 1,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 2,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 3,
"candidate_score": 0.0517,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 4,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 5,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 6,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 7,
"candidate_score": 0.2792,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 8,
"candidate_score": 0.0465,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 9,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 10,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 11,
"candidate_score": 0.3981,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 12,
"candidate_score": 0.2829,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 13,
"candidate_score": 0.0337,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 14,
"candidate_score": 0.1385,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 15,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 16,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 17,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 18,
"candidate_score": 0.1257,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 19,
"candidate_score": 0.1572,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 20,
"candidate_score": 0.0785,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 21,
"candidate_score": 0.2723,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 22,
"candidate_score": 0.1486,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 23,
"candidate_score": 0.2782,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 24,
"candidate_score": 0.1976,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 25,
"candidate_score": 0.077,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 26,
"candidate_score": 0.2793,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 27,
"candidate_score": 0.0888,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 28,
"candidate_score": 0.0507,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 29,
"candidate_score": 0.0718,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 30,
"candidate_score": 0.0,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 31,
"candidate_score": 0.1489,
"best_score": 0.2458,
"note": "accepted"
},
{
"i": 32,
"candidate_score": 0.1922,
"best_score": 0.2458,
"note": "rejected"
},
{
"i": 33,
"candidate_score": 0.2965,
"best_score": 0.2965,
"note": "accepted"
},
{
"i": 34,
"candidate_score": 0.0,
"best_score": 0.2965,
"note": "rejected"
},
{
"i": 35,
"candidate_score": 0.0091,
"best_score": 0.2965,
"note": "accepted"
}
],
"seed_params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(5_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// reserved is cumulative load contributed by successful shards in this\n\t// payment. It models both held atomic shards and liquidity consumed by\n\t// already-settled non-atomic shards.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// localUpper is deliberately payment-local and stricter than the shared\n\t// belief. A failed load must be retried substantially lower, rather than\n\t// being sent repeatedly at nearly the same amount.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tbudgetGuard lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Shared failures remain soft because another payment or held shard\n\t\t// may have caused a transient depletion.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tspent := r.committedFees + r.budgetGuard\n\tif spent >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - spent\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\t// The snapshot makes the first hop nearly certain until an\n\t\t\t\t// observed local or concurrent reservation failure says\n\t\t\t\t// otherwise.\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(total) * weight / sum,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{40, 60}, {60, 40},\n\t\t\t{25, 75}, {75, 25},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{20, 30, 50},\n\t\t\t{50, 30, 20},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\t// Reserve a proportional share of a finite budget for later\n\t\t// shards. A small allowance avoids rejecting a shard merely due\n\t\t// to base-fee rounding.\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 20\n\t\t\tif allowance > 25_000 {\n\t\t\t\tallowance = 25_000\n\t\t\t}\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// When the whole route set cannot be formed, make one useful shard.\n\t// Each failure bound forces the next attempt onto another corridor or\n\t// onto a materially smaller amount.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tcap := feeBudget\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\tcap = feeBudget * shard / total\n\t\t\t\tif cap < 1_000 {\n\t\t\t\t\tcap = 1_000\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\n\t// MaxParts limits concurrent shards, not the lifetime number of\n\t// successful shards. Atomic held shards appear in inFlightHtlcs,\n\t// while settled non-atomic shards release their slots.\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports failure of its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.30\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// A source-side liquidity failure may be represented without a public\n\t// failure source. It is safe and useful to attribute that case to the\n\t// first channel; treating it as an uninformative dispatch refusal causes\n\t// an otherwise identical route to be retried until the runner times out.\n\tif failIdx < 0 &&\n\t\tcode == lnwire.CodeTemporaryChannelFailure &&\n\t\tlen(meta.path) > 0 {\n\n\t\tfailIdx = 0\n\t}\n\n\tpassed := failIdx\n\tif passed < 0 {\n\t\tpassed = 0\n\t}\n\tif passed > len(meta.path) {\n\t\tpassed = len(meta.path)\n\t}\n\n\tfor i := 0; i < passed; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tupdateBelief(edge.key, meta.loads[failIdx], false, now)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\n\t\t\tcount := r.localFails[edge.key]\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\t300_000 + float64(count)*175_000\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\t// Gossip-policy mismatches are deterministic within this\n\t\t\t// payment and should never poison liquidity estimates.\n\t\t\tr.blocked[edge.key] = true\n\t\t\tr.edgePenalty[edge.key] += 8_000_000\n\n\t\tdefault:\n\t\t\tr.edgePenalty[edge.key] += 2_000_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unidentified non-liquidity failure provides no trustworthy\n\t// channel bound. Penalize the whole route to obtain diversity. If a\n\t// finite budget was involved, retain a small guard against dispatch\n\t// accounting or rounding differences.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 500_000\n\t}\n\n\tif r.spec.FeeLimitMsat != lnwire.MaxMilliSatoshi {\n\t\tguard := meta.fee / 20\n\t\tif guard < 1_000 {\n\t\t\tguard = 1_000\n\t\t}\n\n\t\tremaining := r.remainingFeeBudget()\n\t\tif remaining != lnwire.MaxMilliSatoshi && guard > remaining {\n\t\t\tguard = remaining\n\t\t}\n\t\tr.budgetGuard += guard\n\t}\n\n\treturn nil\n}\n"
},
"best_candidate": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(45_000)\n\n\thopCostMsat = float64(75_000)\n\treusedEdgeCostMsat = float64(1_900_000)\n\textraPartCostMsat = float64(65_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 7\n\tmaxPlanBeam = 14\n\tmaxRouterAttempts = 32\n\tmaxConsecutiveMisses = 24\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\theld []*routeMeta\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]float64\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]float64),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := make([]*searchState, 0, len(old)+1)\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.80\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 7 / 8\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 3\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc removeReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif dst[edge.key] <= amount {\n\t\t\tdelete(dst, edge.key)\n\t\t} else {\n\t\t\tdst[edge.key] -= amount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) syncHeld(inFlight uint32) bool {\n\twanted := int(inFlight)\n\tif wanted >= len(r.held) {\n\t\treturn false\n\t}\n\n\tdrop := len(r.held) - wanted\n\tfor i := 0; i < drop; i++ {\n\t\tremoveReservationSet(r.reserved, r.held[i])\n\t}\n\n\tr.held = append([]*routeMeta(nil), r.held[drop:]...)\n\treturn true\n}\n\nfunc candidatePartCounts(slots uint32) []int {\n\tif slots == 0 {\n\t\treturn nil\n\t}\n\n\tmaxParts := int(slots)\n\tseen := make(map[int]bool)\n\tresult := make([]int, 0, 8)\n\n\tadd := func(parts int) {\n\t\tif parts < 1 || parts > maxParts || seen[parts] {\n\t\t\treturn\n\t\t}\n\t\tseen[parts] = true\n\t\tresult = append(result, parts)\n\t}\n\n\tadd(1)\n\tadd(2)\n\tadd(3)\n\tadd(4)\n\tadd(6)\n\tadd(8)\n\tif maxParts > 8 {\n\t\tadd((maxParts + 1) / 2)\n\t}\n\tadd(maxParts)\n\n\treturn result\n}\n\nfunc shardChoices(remaining lnwire.MilliSatoshi,\n\tpartsLeft int) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{remaining}\n\t}\n\n\tfair := (remaining + lnwire.MilliSatoshi(partsLeft) - 1) /\n\t\tlnwire.MilliSatoshi(partsLeft)\n\tminLater := lnwire.MilliSatoshi(partsLeft - 1)\n\n\traw := []lnwire.MilliSatoshi{\n\t\tfair,\n\t\tfair * 3 / 4,\n\t\tfair * 5 / 4,\n\t\tfair / 2,\n\t\tfair * 3 / 2,\n\t\tfair / 3,\n\t\tfair * 2,\n\t}\n\n\tif partsLeft == 2 {\n\t\traw = append(raw, remaining/3, remaining*2/3)\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tresult := make([]lnwire.MilliSatoshi, 0, len(raw))\n\n\tfor _, shard := range raw {\n\t\tif shard <= 0 || shard >= remaining {\n\t\t\tcontinue\n\t\t}\n\t\tif remaining-shard < minLater || seen[shard] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[shard] = true\n\t\tresult = append(result, shard)\n\t}\n\n\treturn result\n}\n\ntype planState struct {\n\tplan []*plannedRoute\n\treservations map[edgeKey]lnwire.MilliSatoshi\n\tfeeLeft lnwire.MilliSatoshi\n\tremaining lnwire.MilliSatoshi\n\tscore float64\n}\n\nfunc projectedPlanScore(state *planState,\n\ttotal lnwire.MilliSatoshi) float64 {\n\n\tdelivered := total - state.remaining\n\tif delivered <= 0 {\n\t\treturn math.Inf(1)\n\t}\n\n\treturn state.score * float64(total) / float64(delivered)\n}\n\nfunc (r *candidateRouter) beamPlan(total lnwire.MilliSatoshi,\n\tparts int, feeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil, 0, false\n\t}\n\n\tstates := []*planState{{\n\t\treservations: cloneReservations(r.reserved),\n\t\tfeeLeft: feeBudget,\n\t\tremaining: total,\n\t}}\n\n\tfor index := 0; index < parts; index++ {\n\t\tpartsLeft := parts - index\n\t\tnextStates := make([]*planState, 0, len(states)*5)\n\n\t\tfor _, state := range states {\n\t\t\tfor _, shard := range shardChoices(\n\t\t\t\tstate.remaining, partsLeft,\n\t\t\t) {\n\t\t\t\tpr, err := r.findRoute(\n\t\t\t\t\tshard, state.reservations, state.feeLeft,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfeeLeft := state.feeLeft\n\t\t\t\tif feeLeft != lnwire.MaxMilliSatoshi {\n\t\t\t\t\tif pr.meta.fee > feeLeft {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfeeLeft -= pr.meta.fee\n\t\t\t\t}\n\n\t\t\t\treservations := cloneReservations(\n\t\t\t\t\tstate.reservations,\n\t\t\t\t)\n\t\t\t\taddReservationSet(reservations, pr.meta)\n\n\t\t\t\tplan := append(\n\t\t\t\t\tappend([]*plannedRoute(nil), state.plan...), pr,\n\t\t\t\t)\n\t\t\t\tscore := state.score + pr.score\n\t\t\t\tif len(state.plan) > 0 {\n\t\t\t\t\tscore += extraPartCostMsat\n\t\t\t\t}\n\n\t\t\t\tnextStates = append(nextStates, &planState{\n\t\t\t\t\tplan: plan,\n\t\t\t\t\treservations: reservations,\n\t\t\t\t\tfeeLeft: feeLeft,\n\t\t\t\t\tremaining: state.remaining - shard,\n\t\t\t\t\tscore: score,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(nextStates) == 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tsort.Slice(nextStates, func(i, j int) bool {\n\t\t\treturn projectedPlanScore(nextStates[i], total) <\n\t\t\t\tprojectedPlanScore(nextStates[j], total)\n\t\t})\n\t\tif len(nextStates) > maxPlanBeam {\n\t\t\tnextStates = nextStates[:maxPlanBeam]\n\t\t}\n\t\tstates = nextStates\n\t}\n\n\tvar best *planState\n\tfor _, state := range states {\n\t\tif state.remaining != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif best == nil || state.score < best.score {\n\t\t\tbest = state\n\t\t}\n\t}\n\tif best == nil {\n\t\treturn nil, 0, false\n\t}\n\n\treturn best.plan, best.score, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range candidatePartCounts(slots) {\n\t\tplan, score, ok := r.beamPlan(total, parts, feeBudget)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tif r.syncHeld(inFlightHtlcs) {\n\t\tr.plan = nil\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.78, now,\n\t\t\t)\n\t\t\tr.edgePenalty[edge.key] *= 0.15\n\t\t\tr.liquiditySuspects[edge.key] *= 0.20\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\taddReservationSet(r.reserved, meta)\n\t\tr.held = append(r.held, meta)\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\tcode := result.Failure.Code()\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key] += 1\n\t\tsuspicion := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + suspicion*400_000\n\n\t\tif failIdx > 0 {\n\t\t\tneighbor := meta.path[failIdx-1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\t\tif failIdx+1 < len(meta.path) {\n\t\t\tneighbor := meta.path[failIdx+1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\n\t\tif suspicion >= 1.8 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.42, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_700_000 + float64(count)*1_100_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 700_000\n\t}\n\n\treturn nil\n}"
},
"stats": {
"evals_done": 400,
"distinct_candidates": 36
},
"candidates": [
{
"id": 0,
"parent": null,
"score": 0.2458,
"accepted": true,
"frontier": true,
"role": "seed",
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(5_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// reserved is cumulative load contributed by successful shards in this\n\t// payment. It models both held atomic shards and liquidity consumed by\n\t// already-settled non-atomic shards.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// localUpper is deliberately payment-local and stricter than the shared\n\t// belief. A failed load must be retried substantially lower, rather than\n\t// being sent repeatedly at nearly the same amount.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tbudgetGuard lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Shared failures remain soft because another payment or held shard\n\t\t// may have caused a transient depletion.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tspent := r.committedFees + r.budgetGuard\n\tif spent >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - spent\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\t// The snapshot makes the first hop nearly certain until an\n\t\t\t\t// observed local or concurrent reservation failure says\n\t\t\t\t// otherwise.\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(total) * weight / sum,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{40, 60}, {60, 40},\n\t\t\t{25, 75}, {75, 25},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{20, 30, 50},\n\t\t\t{50, 30, 20},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\t// Reserve a proportional share of a finite budget for later\n\t\t// shards. A small allowance avoids rejecting a shard merely due\n\t\t// to base-fee rounding.\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 20\n\t\t\tif allowance > 25_000 {\n\t\t\t\tallowance = 25_000\n\t\t\t}\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// When the whole route set cannot be formed, make one useful shard.\n\t// Each failure bound forces the next attempt onto another corridor or\n\t// onto a materially smaller amount.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tcap := feeBudget\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\tcap = feeBudget * shard / total\n\t\t\t\tif cap < 1_000 {\n\t\t\t\t\tcap = 1_000\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\n\t// MaxParts limits concurrent shards, not the lifetime number of\n\t// successful shards. Atomic held shards appear in inFlightHtlcs,\n\t// while settled non-atomic shards release their slots.\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports failure of its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.30\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// A source-side liquidity failure may be represented without a public\n\t// failure source. It is safe and useful to attribute that case to the\n\t// first channel; treating it as an uninformative dispatch refusal causes\n\t// an otherwise identical route to be retried until the runner times out.\n\tif failIdx < 0 &&\n\t\tcode == lnwire.CodeTemporaryChannelFailure &&\n\t\tlen(meta.path) > 0 {\n\n\t\tfailIdx = 0\n\t}\n\n\tpassed := failIdx\n\tif passed < 0 {\n\t\tpassed = 0\n\t}\n\tif passed > len(meta.path) {\n\t\tpassed = len(meta.path)\n\t}\n\n\tfor i := 0; i < passed; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tupdateBelief(edge.key, meta.loads[failIdx], false, now)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\n\t\t\tcount := r.localFails[edge.key]\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\t300_000 + float64(count)*175_000\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\t// Gossip-policy mismatches are deterministic within this\n\t\t\t// payment and should never poison liquidity estimates.\n\t\t\tr.blocked[edge.key] = true\n\t\t\tr.edgePenalty[edge.key] += 8_000_000\n\n\t\tdefault:\n\t\t\tr.edgePenalty[edge.key] += 2_000_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unidentified non-liquidity failure provides no trustworthy\n\t// channel bound. Penalize the whole route to obtain diversity. If a\n\t// finite budget was involved, retain a small guard against dispatch\n\t// accounting or rounding differences.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 500_000\n\t}\n\n\tif r.spec.FeeLimitMsat != lnwire.MaxMilliSatoshi {\n\t\tguard := meta.fee / 20\n\t\tif guard < 1_000 {\n\t\t\tguard = 1_000\n\t\t}\n\n\t\tremaining := r.remainingFeeBudget()\n\t\tif remaining != lnwire.MaxMilliSatoshi && guard > remaining {\n\t\t\tguard = remaining\n\t\t}\n\t\tr.budgetGuard += guard\n\t}\n\n\treturn nil\n}\n"
}
},
{
"id": 1,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\triskPriceMsat = float64(420_000)\n\thopCostMsat = float64(12_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 || outbound > int64(lnwire.MaxMilliSatoshi-amt) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\t// The inbound fee is assessed on the amount arriving before the\n\t// inbound adjustment: the forwarded amount plus the outbound fee.\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// Successful shards either consume or hold this much liquidity.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Payment-local failure state. A reported failure must be corroborated\n\t// before it becomes a hard amount ceiling.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tfailVotes map[edgeKey]int\n\tfailLoads map[edgeKey]lnwire.MilliSatoshi\n\tpolicyVotes map[edgeKey]int\n\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tminPlanParts int\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tfailVotes: make(map[edgeKey]int),\n\t\tfailLoads: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpolicyVotes: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t\tminPlanParts: 1,\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Failure bounds remain soft because they can describe temporary\n\t\t// self-contention or stale liquidity.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge == lnwire.MaxMilliSatoshi ||\n\t\t\t\t\tnodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\triskPriceMsat*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge == lnwire.MaxMilliSatoshi ||\n\t\t\tcharge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif sum <= 0 || len(weights) == 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\tbase := total / lnwire.MilliSatoshi(sum)\n\tremainder := int64(total % lnwire.MilliSatoshi(sum))\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := base*lnwire.MilliSatoshi(weight) +\n\t\t\tlnwire.MilliSatoshi(remainder*weight/sum)\n\n\t\tminRemaining := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minRemaining {\n\t\t\tpart = remaining - minRemaining\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc sameAllocation(a, b []lnwire.MilliSatoshi) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc appendAllocation(patterns *[][]lnwire.MilliSatoshi,\n\tallocation []lnwire.MilliSatoshi) {\n\n\tfor _, existing := range *patterns {\n\t\tif sameAllocation(existing, allocation) {\n\t\t\treturn\n\t\t}\n\t}\n\n\t*patterns = append(*patterns, allocation)\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tvar patterns [][]lnwire.MilliSatoshi\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\tappendAllocation(&patterns, weightedAllocation(total, equal))\n\n\tif parts == 2 {\n\t\tfor left := int64(1); left < 10; left++ {\n\t\t\tappendAllocation(\n\t\t\t\t&patterns,\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{left, 10 - left},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\treturn patterns\n\t}\n\n\tif parts == 3 {\n\t\t// A compact composition grid covers strongly unequal three-way\n\t\t// splits without making the route-set search explode.\n\t\tconst units = int64(8)\n\t\tfor first := int64(1); first <= units-2; first++ {\n\t\t\tfor second := int64(1);\n\t\t\t\tsecond <= units-first-1; second++ {\n\n\t\t\t\tthird := units - first - second\n\t\t\t\tappendAllocation(\n\t\t\t\t\t&patterns,\n\t\t\t\t\tweightedAllocation(\n\t\t\t\t\t\ttotal,\n\t\t\t\t\t\t[]int64{first, second, third},\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn patterns\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t}\n\tappendAllocation(\n\t\t&patterns, weightedAllocation(total, ascending),\n\t)\n\tappendAllocation(\n\t\t&patterns, weightedAllocation(total, descending),\n\t)\n\n\tfor favored := 0; favored < parts; favored++ {\n\t\theavy := make([]int64, parts)\n\t\tlight := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\theavy[i] = 2\n\t\t\tlight[i] = 3\n\t\t}\n\t\theavy[favored] = int64(parts + 3)\n\t\tlight[favored] = 1\n\n\t\tappendAllocation(\n\t\t\t&patterns, weightedAllocation(total, heavy),\n\t\t)\n\t\tappendAllocation(\n\t\t\t&patterns, weightedAllocation(total, light),\n\t\t)\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, remainingBudget,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tstartParts := r.minPlanParts\n\tif startParts < 1 {\n\t\tstartParts = 1\n\t}\n\tif startParts > int(slots) {\n\t\tstartParts = int(slots)\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := startParts; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// If a complete route set cannot be formed, try to make useful\n\t// progress with one conservative shard. This also covers policy\n\t// ceilings that require a split finer than the allocation grid.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tif r.reserved[key] > lnwire.MaxMilliSatoshi-amount {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) increaseSplitPressure() {\n\tmaxParts := int(r.spec.MaxParts)\n\tif maxParts <= 1 {\n\t\treturn\n\t}\n\n\tdesired := 1 + r.consecutiveMisses/2\n\tif desired > maxParts {\n\t\tdesired = maxParts\n\t}\n\tif desired > r.minPlanParts {\n\t\tr.minPlanParts = desired\n\t}\n}\n\nfunc (r *candidateRouter) penalizeNeighborhood(meta *routeMeta,\n\tidx int, center, neighbor float64) {\n\n\tfor delta := -1; delta <= 1; delta++ {\n\t\ti := idx + delta\n\t\tif i < 0 || i >= len(meta.path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := neighbor\n\t\tif delta == 0 {\n\t\t\tpenalty = center\n\t\t}\n\t\tr.edgePenalty[meta.path[i].key] += penalty\n\t}\n}\n\nfunc (r *candidateRouter) noteLiquidityFailure(meta *routeMeta,\n\tidx int, trusted bool, now time.Time) {\n\n\tedge := meta.path[idx]\n\tload := meta.loads[idx]\n\n\tr.penalizeNeighborhood(meta, idx, 750_000, 220_000)\n\n\tif old := r.failLoads[edge.key]; old == 0 || load < old {\n\t\tr.failLoads[edge.key] = load\n\t}\n\n\tr.failVotes[edge.key]++\n\tif trusted && r.failVotes[edge.key] < 2 {\n\t\tr.failVotes[edge.key] = 2\n\t}\n\n\t// A single readable report can still be shifted by one hop. The\n\t// second matching report makes a false hard bound much less likely.\n\tif r.failVotes[edge.key] < 2 {\n\t\treturn\n\t}\n\n\tbound := r.failLoads[edge.key]\n\tr.recordLocalFailure(edge.key, bound)\n\tupdateBelief(edge.key, bound, false, now)\n\n\tr.edgePenalty[edge.key] +=\n\t\t250_000 + float64(r.localFails[edge.key])*125_000\n}\n\nfunc (r *candidateRouter) notePolicyFailure(meta *routeMeta,\n\tidx int, trusted bool) {\n\n\tedge := meta.path[idx]\n\tr.penalizeNeighborhood(meta, idx, 4_000_000, 500_000)\n\n\tr.policyVotes[edge.key]++\n\tif trusted && r.policyVotes[edge.key] < 2 {\n\t\tr.policyVotes[edge.key] = 2\n\t}\n\n\tif r.policyVotes[edge.key] >= 2 {\n\t\tr.blocked[edge.key] = true\n\t\tr.edgePenalty[edge.key] += 8_000_000\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tdelete(r.failVotes, edge.key)\n\t\t\tdelete(r.failLoads, edge.key)\n\t\t\tdelete(r.policyVotes, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\tr.minPlanParts = 1\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\tr.increaseSplitPressure()\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// With one-hop attribution shifts, only edges strictly before the\n\t// reported edge's predecessor are certain to have passed.\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tif safePassed > len(meta.path) {\n\t\tsafePassed = len(meta.path)\n\t}\n\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\ttrusted := rt != nil &&\n\t\t\tresult.FailureSource == rt.SourcePubKey &&\n\t\t\tfailIdx == 0\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.noteLiquidityFailure(meta, failIdx, trusted, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.notePolicyFailure(meta, failIdx, trusted)\n\n\t\tdefault:\n\t\t\tr.penalizeNeighborhood(\n\t\t\t\tmeta, failIdx, 1_000_000, 250_000,\n\t\t\t)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unreadable result supplies no channel attribution. Distribute a\n\t// small payment-local route-diversity penalty, but write no liquidity\n\t// or policy facts and do not consume any fee budget.\n\tif len(meta.path) > 0 {\n\t\tperEdge := float64(360_000) / float64(len(meta.path))\n\t\tfor _, edge := range meta.path {\n\t\t\tr.edgePenalty[edge.key] += perEdge\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 2,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(5_000)\n\treusedEdgeCostMsat = float64(1_250_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 9\n\tmaxRouterAttempts = 72\n\tplanBeamWidth = 12\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\n\t// The inbound fee is charged on the amount arriving at the forwarding\n\t// node, including that node's outbound fee.\n\tinboundBase := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBase*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Failure attribution is noisy. A single report only adds a soft\n\t// penalty. Repeated reports are required before installing a local\n\t// liquidity ceiling or blocking a deterministic policy mismatch.\n\tfailureVotes map[edgeKey]float64\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tfailureVotes: make(map[edgeKey]float64),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tstrength float64, now time.Time) {\n\n\tif amt <= 0 || strength <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+strength)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.65\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) localProbability(key edgeKey,\n\tload lnwire.MilliSatoshi, p float64) float64 {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn p\n\t}\n\n\tswitch {\n\tcase load >= upper:\n\t\tp *= 0.035\n\n\tcase load*4 >= upper*3:\n\t\tp *= 0.22\n\n\tcase load*2 >= upper:\n\t\tp *= 0.58\n\t}\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\n\treturn p\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tp = r.localProbability(edge.key, totalLoad, p)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = r.localProbability(\n\t\t\t\t\t\tedge.key, totalLoad, 0.985,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc scaledAmount(amt lnwire.MilliSatoshi,\n\tnumerator, denominator int64) lnwire.MilliSatoshi {\n\n\tif amt <= 0 || numerator <= 0 || denominator <= 0 {\n\t\treturn 0\n\t}\n\n\tq := int64(amt) / denominator\n\trem := int64(amt) % denominator\n\treturn lnwire.MilliSatoshi(\n\t\tq*numerator + rem*numerator/denominator,\n\t)\n}\n\nfunc appendAmount(dst []lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool, amt,\n\tmax lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 || amt > max || seen[amt] {\n\t\treturn dst\n\t}\n\n\tseen[amt] = true\n\treturn append(dst, amt)\n}\n\nfunc shardCandidates(remaining lnwire.MilliSatoshi,\n\tpartsLeft int) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{remaining}\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tresult := make([]lnwire.MilliSatoshi, 0, 10)\n\tmaxShard := remaining - lnwire.MilliSatoshi(partsLeft-1)\n\taverage := (remaining + lnwire.MilliSatoshi(partsLeft) - 1) /\n\t\tlnwire.MilliSatoshi(partsLeft)\n\n\tresult = appendAmount(result, seen, average, maxShard)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(average, 3, 4), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(average, 5, 4), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(average, 3, 2), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(average, 2, 1), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(remaining, 1, 2), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(remaining, 2, 3), maxShard,\n\t)\n\tresult = appendAmount(\n\t\tresult, seen, scaledAmount(remaining, 3, 4), maxShard,\n\t)\n\tresult = appendAmount(result, seen, maxShard, maxShard)\n\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i] > result[j]\n\t})\n\n\treturn result\n}\n\ntype planState struct {\n\troutes []*plannedRoute\n\treservations map[edgeKey]lnwire.MilliSatoshi\n\tbudget lnwire.MilliSatoshi\n\tremaining lnwire.MilliSatoshi\n\tscore float64\n\tdelivered lnwire.MilliSatoshi\n}\n\nfunc planStateRank(s *planState) float64 {\n\tif s.delivered <= 0 {\n\t\treturn math.Inf(1)\n\t}\n\n\treturn s.score / float64(s.delivered)\n}\n\nfunc (r *candidateRouter) makeJointPlan(total lnwire.MilliSatoshi,\n\tslots uint32, feeBudget lnwire.MilliSatoshi) []*plannedRoute {\n\n\tinitial := &planState{\n\t\treservations: cloneReservations(r.reserved),\n\t\tbudget: feeBudget,\n\t\tremaining: total,\n\t}\n\n\tbeam := []*planState{initial}\n\tvar best *planState\n\n\tfor used := 0; used < int(slots) && len(beam) > 0; used++ {\n\t\tpartsLeft := int(slots) - used\n\t\tnextBeam := make([]*planState, 0, planBeamWidth*4)\n\n\t\tfor _, state := range beam {\n\t\t\tfor _, shard := range shardCandidates(\n\t\t\t\tstate.remaining, partsLeft,\n\t\t\t) {\n\t\t\t\tpr, err := r.findRoute(\n\t\t\t\t\tshard, state.reservations, state.budget,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnextBudget := state.budget\n\t\t\t\tif nextBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\t\tif pr.meta.fee > nextBudget {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tnextBudget -= pr.meta.fee\n\t\t\t\t}\n\n\t\t\t\treservations := cloneReservations(\n\t\t\t\t\tstate.reservations,\n\t\t\t\t)\n\t\t\t\taddReservationSet(reservations, pr.meta)\n\n\t\t\t\troutes := make(\n\t\t\t\t\t[]*plannedRoute, len(state.routes), len(state.routes)+1,\n\t\t\t\t)\n\t\t\t\tcopy(routes, state.routes)\n\t\t\t\troutes = append(routes, pr)\n\n\t\t\t\tpartPenalty := float64(0)\n\t\t\t\tif len(state.routes) > 0 {\n\t\t\t\t\tpartPenalty = extraPartCostMsat\n\t\t\t\t}\n\n\t\t\t\tnext := &planState{\n\t\t\t\t\troutes: routes,\n\t\t\t\t\treservations: reservations,\n\t\t\t\t\tbudget: nextBudget,\n\t\t\t\t\tremaining: state.remaining - shard,\n\t\t\t\t\tscore: state.score + pr.score + partPenalty,\n\t\t\t\t\tdelivered: state.delivered + shard,\n\t\t\t\t}\n\n\t\t\t\tif next.remaining == 0 {\n\t\t\t\t\tif best == nil || next.score < best.score {\n\t\t\t\t\t\tbest = next\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif used+1 < int(slots) {\n\t\t\t\t\tnextBeam = append(nextBeam, next)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tsort.Slice(nextBeam, func(i, j int) bool {\n\t\t\tri := planStateRank(nextBeam[i])\n\t\t\trj := planStateRank(nextBeam[j])\n\t\t\tif ri == rj {\n\t\t\t\treturn nextBeam[i].remaining <\n\t\t\t\t\tnextBeam[j].remaining\n\t\t\t}\n\t\t\treturn ri < rj\n\t\t})\n\n\t\tif len(nextBeam) > planBeamWidth {\n\t\t\tnextBeam = nextBeam[:planBeamWidth]\n\t\t}\n\t\tbeam = nextBeam\n\t}\n\n\tif best == nil {\n\t\treturn nil\n\t}\n\n\treturn best.routes\n}\n\nfunc (r *candidateRouter) fallbackShard(total lnwire.MilliSatoshi,\n\tslots uint32, feeBudget lnwire.MilliSatoshi) *plannedRoute {\n\n\tparts := int(slots)\n\tif parts < 1 {\n\t\tparts = 1\n\t}\n\n\tcandidates := shardCandidates(total, parts)\n\tif parts == 1 {\n\t\tcandidates = append(\n\t\t\tcandidates,\n\t\t\tscaledAmount(total, 3, 4),\n\t\t\tscaledAmount(total, 1, 2),\n\t\t\tscaledAmount(total, 1, 3),\n\t\t\tscaledAmount(total, 1, 4),\n\t\t)\n\t}\n\n\tvar best *plannedRoute\n\tfor _, shard := range candidates {\n\t\tif shard <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif best == nil ||\n\t\t\tpr.meta.delivered > best.meta.delivered ||\n\t\t\t(pr.meta.delivered == best.meta.delivered &&\n\t\t\t\tpr.score < best.score) {\n\n\t\t\tbest = pr\n\t\t}\n\t}\n\n\treturn best\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tplan := r.makeJointPlan(total, slots, feeBudget)\n\tif len(plan) > 0 {\n\t\treturn plan, nil\n\t}\n\n\t// A complete route set is preferred, but a useful partial shard can\n\t// expose or occupy a viable corridor and allow replanning around the\n\t// resulting evidence.\n\tfallback := r.fallbackShard(total, slots, feeBudget)\n\tif fallback != nil {\n\t\treturn []*plannedRoute{fallback}, nil\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 &&\n\t\t(queuedAmount(r.plan) != amt ||\n\t\t\tuint32(len(r.plan)) > slots) {\n\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordLiquidityReport(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tedge := meta.path[failIdx]\n\tkey := edge.key\n\n\tr.failureVotes[key] += 1\n\tr.localFails[key]++\n\tcount := r.localFails[key]\n\n\t// The first report is deliberately soft because one in ten readable\n\t// reports may blame an adjacent hop. Repetition corroborates it.\n\tr.edgePenalty[key] += 650_000 + float64(count)*190_000\n\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 55_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 55_000\n\t}\n\n\tif r.failureVotes[key] < 1.75 {\n\t\treturn\n\t}\n\n\tload := meta.loads[failIdx]\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\n\t// Corroborated failures are still softer than successes because the\n\t// depleted liquidity may be temporarily reserved by another payment.\n\tupdateBelief(key, load, false, 0.28, now)\n}\n\nfunc (r *candidateRouter) recordPolicyReport(meta *routeMeta,\n\tfailIdx int) {\n\n\tedge := meta.path[failIdx]\n\tkey := edge.key\n\n\tr.policyFails[key]++\n\tr.edgePenalty[key] += 2_500_000\n\n\t// Deterministic policy failures repeat. Requiring a second report avoids\n\t// permanently blocking an innocent adjacent edge after shifted blame.\n\tif r.policyFails[key] >= 2 {\n\t\tr.blocked[key] = true\n\t\tr.edgePenalty[key] += 8_000_000\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.22\n\t\t\tr.failureVotes[edge.key] *= 0.25\n\t\t\tr.localFails[edge.key] = 0\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.recordLiquidityReport(meta, failIdx, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.recordPolicyReport(meta, failIdx)\n\n\t\tdefault:\n\t\t\tr.edgePenalty[meta.path[failIdx].key] += 1_250_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unreadable failure carries no channel information. It must not\n\t// create liquidity bounds, inferred prefix successes, policy blocks, or\n\t// fee-budget reservations. A modest route-wide penalty only encourages\n\t// the next attempt to explore a different corridor.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 240_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 3,
"parent": 0,
"score": 0.0517,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(35_000)\n\n\thopCostMsat = float64(35_000)\n\treusedEdgeCostMsat = float64(650_000)\n\textraPartCostMsat = float64(55_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\ttempBlames map[edgeKey]int\n\tpolicyBlames map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tshardCeiling lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\ttempBlames: make(map[edgeKey]int),\n\t\tpolicyBlames: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.60)\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.42)\n\t}\n\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) activeReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\n\tfor _, meta := range r.issued {\n\t\tfor i, edge := range meta.path {\n\t\t\tresult[edge.key] += meta.amounts[i]\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tspent := r.committedFees\n\tfor _, meta := range r.issued {\n\t\tif meta.fee > lnwire.MaxMilliSatoshi-spent {\n\t\t\treturn 0\n\t\t}\n\t\tspent += meta.fee\n\t}\n\n\tif spent >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - spent\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.65\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.45\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tbase := int64(total) / sum\n\tremainder := int64(total) % sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tvalue := base*weight + remainder*weight/sum\n\t\tpart := lnwire.MilliSatoshi(value)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc appendRotations(dst *[][]lnwire.MilliSatoshi,\n\ttotal lnwire.MilliSatoshi, weights []int64) {\n\n\tn := len(weights)\n\tfor shift := 0; shift < n; shift++ {\n\t\trotated := make([]int64, n)\n\t\tfor i := range weights {\n\t\t\trotated[i] = weights[(i+shift)%n]\n\t\t}\n\t\t*dst = append(*dst, weightedAllocation(total, rotated))\n\t}\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor left := int64(10); left <= 90; left += 10 {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, []int64{left, 100 - left}),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tappendRotations(&patterns, total, []int64{1, 1, 2})\n\t\tappendRotations(&patterns, total, []int64{1, 2, 3})\n\t\tappendRotations(&patterns, total, []int64{1, 2, 2})\n\n\tcase 4:\n\t\tappendRotations(&patterns, total, []int64{1, 1, 2, 3})\n\t\tappendRotations(&patterns, total, []int64{1, 2, 3, 4})\n\n\tdefault:\n\t\tascending := make([]int64, parts)\n\t\tdescending := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\tascending[i] = int64(i + 1)\n\t\t\tdescending[i] = int64(parts - i)\n\t\t}\n\n\t\tpatterns = append(\n\t\t\tpatterns,\n\t\t\tweightedAllocation(total, ascending),\n\t\t\tweightedAllocation(total, descending),\n\t\t)\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := r.activeReservations()\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tif r.shardCeiling > 0 && shard > r.shardCeiling {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 12\n\t\t\tif allowance > 50_000 {\n\t\t\t\tallowance = 50_000\n\t\t\t}\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) fallbackShard(total lnwire.MilliSatoshi,\n\tslots uint32, feeBudget lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tshard := total\n\tif slots > 1 {\n\t\tshard = (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\t}\n\tif r.shardCeiling > 0 && shard > r.shardCeiling {\n\t\tshard = r.shardCeiling\n\t}\n\tif shard <= 0 {\n\t\treturn nil, errors.New(\"invalid shard amount\")\n\t}\n\n\treservations := r.activeReservations()\n\n\tfor shard > 0 {\n\t\tcap := feeBudget\n\t\tif feeBudget != lnwire.MaxMilliSatoshi && total > shard {\n\t\t\tcap = feeBudget * shard / total\n\t\t\tallowance := feeBudget / 10\n\t\t\tif allowance > 50_000 {\n\t\t\t\tallowance = 50_000\n\t\t\t}\n\t\t\tif cap <= feeBudget-allowance {\n\t\t\t\tcap += allowance\n\t\t\t} else {\n\t\t\t\tcap = feeBudget\n\t\t\t}\n\t\t\tif cap < 1_000 && feeBudget >= 1_000 {\n\t\t\t\tcap = 1_000\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, cap)\n\t\tif err == nil {\n\t\t\treturn pr, nil\n\t\t}\n\n\t\tif shard <= 10_000 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 3 / 4\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(allocation, feeBudget)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tpr, err := r.fallbackShard(total, slots, feeBudget)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn []*plannedRoute{pr}, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) tightenShardCeiling(\n\tdelivered lnwire.MilliSatoshi) {\n\n\tif delivered <= 1 {\n\t\treturn\n\t}\n\n\tfactor := lnwire.MilliSatoshi(3)\n\tdivisor := lnwire.MilliSatoshi(4)\n\tif r.consecutiveMisses >= 3 {\n\t\tfactor = 2\n\t\tdivisor = 3\n\t}\n\n\tceiling := delivered * factor / divisor\n\tif ceiling <= 0 {\n\t\tceiling = 1\n\t}\n\n\tif r.shardCeiling == 0 || ceiling < r.shardCeiling {\n\t\tr.shardCeiling = ceiling\n\t}\n}\n\nfunc (r *candidateRouter) addAmbiguousPenalty(meta *routeMeta,\n\tidx int, exact, adjacent float64) {\n\n\tif idx >= 0 && idx < len(meta.path) {\n\t\tr.edgePenalty[meta.path[idx].key] += exact\n\t}\n\tif idx-1 >= 0 {\n\t\tr.edgePenalty[meta.path[idx-1].key] += adjacent\n\t}\n\tif idx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[idx+1].key] += adjacent\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t\tdelete(r.tempBlames, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\tr.shardCeiling = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\tr.tightenShardCeiling(meta.delivered)\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\t\tkey := edge.key\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.tempBlames[key]++\n\t\t\tr.addAmbiguousPenalty(\n\t\t\t\tmeta, failIdx,\n\t\t\t\t450_000+float64(r.tempBlames[key])*120_000,\n\t\t\t\t180_000,\n\t\t\t)\n\n\t\t\t// Failure attribution may be shifted. A single report is\n\t\t\t// treated as suspicion; a repeated report corroborates it.\n\t\t\tif r.tempBlames[key] >= 2 {\n\t\t\t\tupdateBelief(key, meta.loads[failIdx], false, now)\n\t\t\t\tr.recordLocalFailure(key, meta.loads[failIdx])\n\t\t\t}\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyBlames[key]++\n\t\t\tr.addAmbiguousPenalty(meta, failIdx, 3_000_000, 500_000)\n\t\t\tif r.policyBlames[key] >= 2 {\n\t\t\t\tr.blocked[key] = true\n\t\t\t\tr.edgePenalty[key] += 8_000_000\n\t\t\t}\n\n\t\tdefault:\n\t\t\tr.addAmbiguousPenalty(meta, failIdx, 1_200_000, 300_000)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unreadable failure carries no channel attribution. Do not write\n\t// bounds or infer that any prefix succeeded. Diversify the whole path\n\t// and reduce the next shard amount instead.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 220_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 4,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(40_000)\n\treusedEdgeCostMsat = float64(700_000)\n\textraPartCostMsat = float64(55_000)\n\n\tmaxSearchHops = 26\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 88\n\tmaxConsecutiveMisses = 80\n\n\tfailureConfirmThreshold = 1.70\n\tfailureConfirmMargin = 0.40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 || outbound > int64(lnwire.MaxMilliSatoshi-amt) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\t// The inbound fee is charged on the amount arriving at the forwarding\n\t// node, including that node's outbound forwarding fee.\n\tinboundSubject := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) + int64(\n\t\tfloat64(inboundSubject)*float64(in.inboundRate)/1_000_000,\n\t)\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// reserved is the cumulative load contributed by shards already accepted\n\t// for this payment. For atomic MPP it represents held liquidity; for\n\t// non-atomic MPP it represents liquidity already moved.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Failure attribution may be shifted by one hop. blame accumulates\n\t// corroborating evidence over a three-edge neighborhood. A hard\n\t// payment-local ceiling is installed only once one edge clearly wins.\n\tblame map[edgeKey]float64\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\n\tpolicyReports map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tblame: make(map[edgeKey]float64),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyReports: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc clampProbability(p float64) float64 {\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\n\treturn clampProbability(0.005 + lowMode + highMode)\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\t// Evidence softens under background traffic, but slowly enough that\n\t// adjacent payments still benefit from one another.\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Failure bounds remain soft because held shards, concurrent\n\t\t// payments, stale state, and imperfect attribution can all make a\n\t\t// genuine observation transient.\n\t\tevidence = 0.015\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.015 + 0.980*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\treturn clampProbability(prior*(1-conf) + evidence*conf)\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.50)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 2 / 3\n\tif r.localFails[key] >= 4 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge == lnwire.MaxMilliSatoshi ||\n\t\t\t\t\tnodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\t// The local snapshot is strong evidence, but not certainty:\n\t\t\t\t// sibling payments can reserve the same balance after the\n\t\t\t\t// router was created.\n\t\t\t\tlocalP := 0.995\n\t\t\t\tbalance := r.localBalances[edge.chanID]\n\t\t\t\tif balance > 0 {\n\t\t\t\t\tx := float64(totalLoad) / float64(balance)\n\t\t\t\t\tif x > 0.75 {\n\t\t\t\t\t\tlocalP = 0.10 + 0.895/\n\t\t\t\t\t\t\t(1+math.Exp((x-0.96)/0.035))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tp = clampProbability(0.80*localP + 0.20*p)\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = math.Min(p, 0.72)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.60\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge == lnwire.MaxMilliSatoshi ||\n\t\t\tcharge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tpartsLeft := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part > remaining-partsLeft {\n\t\t\tpart = remaining - partsLeft\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor left := int64(10); left <= 45; left += 5 {\n\t\t\tright := 100 - left\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, []int64{left, right}),\n\t\t\t\tweightedAllocation(total, []int64{right, left}),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 25, 65}, {65, 25, 10},\n\t\t\t{15, 35, 50}, {50, 35, 15},\n\t\t\t{20, 30, 50}, {50, 30, 20},\n\t\t\t{20, 40, 40}, {40, 40, 20},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 15, 25, 50}, {50, 25, 15, 10},\n\t\t\t{15, 20, 25, 40}, {40, 25, 20, 15},\n\t\t\t{20, 20, 30, 30}, {30, 30, 20, 20},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tascending := make([]int64, parts)\n\t\tdescending := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\tascending[i] = int64(i + 1)\n\t\t\tdescending[i] = int64(parts - i)\n\t\t}\n\t\tpatterns = append(\n\t\t\tpatterns,\n\t\t\tweightedAllocation(total, ascending),\n\t\t\tweightedAllocation(total, descending),\n\t\t)\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc proportionalFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi || remaining <= shard {\n\t\treturn budget\n\t}\n\n\tcap := budget * shard / remaining\n\n\t// Leave most of the budget proportional, while allowing for base fees\n\t// and integer rounding on this shard.\n\tallowance := budget / 25\n\tif allowance > 20_000 {\n\t\tallowance = 20_000\n\t}\n\tif cap <= budget-allowance {\n\t\tcap += allowance\n\t} else {\n\t\tcap = budget\n\t}\n\tif cap <= 0 {\n\t\tcap = 1\n\t}\n\n\treturn cap\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tvar remainingAmount lnwire.MilliSatoshi\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := proportionalFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) partialPlan(total lnwire.MilliSatoshi,\n\tslots uint32, feeBudget lnwire.MilliSatoshi) ([]*plannedRoute, error) {\n\n\tif slots <= 1 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tfractions := [][2]int64{\n\t\t{9, 10}, {4, 5}, {3, 4}, {2, 3},\n\t\t{3, 5}, {1, 2}, {2, 5}, {1, 3},\n\t\t{1, 4}, {1, 5}, {1, 8},\n\t}\n\n\tfor _, fraction := range fractions {\n\t\tshard := lnwire.MilliSatoshi(\n\t\t\tint64(total) * fraction[0] / fraction[1],\n\t\t)\n\t\tif shard <= 0 || shard >= total {\n\t\t\tcontinue\n\t\t}\n\n\t\tcap := proportionalFeeCap(feeBudget, shard, total)\n\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// A complete route-set may fail because a greedy early assignment\n\t// consumed the only corridor usable by a later shard. Commit the largest\n\t// useful partial shard and re-plan the residual with fresh feedback.\n\treturn r.partialPlan(total, slots, feeBudget)\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports the failure of its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) addFailureNeighborhood(meta *routeMeta,\n\tfailIdx int, centerPenalty, neighborPenalty float64) {\n\n\tfor offset := -1; offset <= 1; offset++ {\n\t\tidx := failIdx + offset\n\t\tif idx < 0 || idx >= len(meta.path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tweight := 0.35\n\t\tpenalty := neighborPenalty\n\t\tif offset == 0 {\n\t\t\tweight = 1\n\t\t\tpenalty = centerPenalty\n\t\t}\n\n\t\tkey := meta.path[idx].key\n\t\tr.blame[key] += weight\n\t\tr.edgePenalty[key] += penalty\n\t}\n}\n\nfunc (r *candidateRouter) confirmedFailure(meta *routeMeta) int {\n\tbestIdx := -1\n\tbest := float64(0)\n\tsecond := float64(0)\n\n\tfor i, edge := range meta.path {\n\t\tvalue := r.blame[edge.key]\n\t\tif value > best {\n\t\t\tsecond = best\n\t\t\tbest = value\n\t\t\tbestIdx = i\n\t\t} else if value > second {\n\t\t\tsecond = value\n\t\t}\n\t}\n\n\tif bestIdx < 0 || best < failureConfirmThreshold ||\n\t\tbest-second < failureConfirmMargin {\n\n\t\treturn -1\n\t}\n\n\treturn bestIdx\n}\n\nfunc (r *candidateRouter) recordCertainPrefix(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\t// Attribution may be shifted one hop in either direction. Only edges\n\t// strictly before failIdx-1 are certain to have passed.\n\tlimit := failIdx - 1\n\tif limit < 0 {\n\t\tlimit = 0\n\t}\n\tif limit > len(meta.path) {\n\t\tlimit = len(meta.path)\n\t}\n\n\tfor i := 0; i < limit; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t\tr.edgePenalty[meta.path[i].key] *= 0.85\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tr.blame[edge.key] *= 0.20\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\t// An unreadable failure contains no channel information. A modest\n\t\t// route-wide penalty creates diversity without poisoning liquidity\n\t\t// bounds or pretending that the first channel failed.\n\t\tfor _, edge := range meta.path {\n\t\t\tr.edgePenalty[edge.key] += 180_000\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.recordCertainPrefix(meta, failIdx, now)\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.addFailureNeighborhood(meta, failIdx, 1_350_000, 350_000)\n\n\t\tconfirmed := r.confirmedFailure(meta)\n\t\tif confirmed >= 0 {\n\t\t\tedge := meta.path[confirmed]\n\t\t\tload := meta.loads[confirmed]\n\n\t\t\tupdateBelief(edge.key, load, false, now)\n\t\t\tr.recordLocalFailure(edge.key, load)\n\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\t650_000 + float64(r.localFails[edge.key])*225_000\n\n\t\t\t// Consume enough accumulated evidence that a single confirmed\n\t\t\t// cluster does not immediately harden several adjacent edges.\n\t\t\tr.blame[edge.key] *= 0.55\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t// Policy failures are deterministic, but their source can still be\n\t\t// shifted. Quarantine only after the same reported edge repeats.\n\t\tcenter := meta.path[failIdx]\n\t\tr.policyReports[center.key]++\n\t\tr.edgePenalty[center.key] += 3_000_000\n\n\t\tfor _, offset := range []int{-1, 1} {\n\t\t\tidx := failIdx + offset\n\t\t\tif idx >= 0 && idx < len(meta.path) {\n\t\t\t\tr.edgePenalty[meta.path[idx].key] += 500_000\n\t\t\t}\n\t\t}\n\n\t\tif r.policyReports[center.key] >= 2 {\n\t\t\tr.blocked[center.key] = true\n\t\t\tr.edgePenalty[center.key] += 8_000_000\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[meta.path[failIdx].key] += 1_200_000\n\n\t\tfor _, offset := range []int{-1, 1} {\n\t\t\tidx := failIdx + offset\n\t\t\tif idx >= 0 && idx < len(meta.path) {\n\t\t\t\tr.edgePenalty[meta.path[idx].key] += 250_000\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 5,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(20_000)\n\n\thopCostMsat = float64(5_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 48\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\n\t// Inbound fees are assessed on the amount arriving at the forwarding\n\t// node: the amount forwarded onward plus that node's outbound fee.\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// Successful shards either hold or consume this liquidity.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Hard local bounds require corroboration because failure attribution\n\t// can be shifted. A first observation only creates a soft retry ceiling.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tsuspectUpper map[edgeKey]lnwire.MilliSatoshi\n\tfailureVotes map[edgeKey]int\n\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n\tliquidityMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tsuspectUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tfailureVotes: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Failure evidence remains soft because reservations and background\n\t\t// traffic can make a real failure transient.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tif upper := r.localUpper[key]; upper > 0 {\n\t\tcutoff := upper * 3 / 4\n\t\tif r.localFails[key] >= 3 {\n\t\t\tcutoff = upper / 2\n\t\t}\n\t\tif cutoff <= 0 {\n\t\t\treturn 1\n\t\t}\n\n\t\treturn cutoff\n\t}\n\n\t// A single blame report is not trusted enough to create a hard bound,\n\t// but it should prevent an identical high-load retry.\n\tif upper := r.suspectUpper[key]; upper > 0 {\n\t\tcutoff := upper * 9 / 10\n\t\tif cutoff <= 0 {\n\t\t\treturn 1\n\t\t}\n\n\t\treturn cutoff\n\t}\n\n\treturn 0\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.75\n\t\t\t\t} else if r.suspectUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.88\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tif parts == 1 {\n\t\treturn patterns\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfrontHeavy := make([]int64, parts)\n\tbackHeavy := make([]int64, parts)\n\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t\tfrontHeavy[i] = 2\n\t\tbackHeavy[i] = 2\n\t}\n\tfrontHeavy[0] = int64(parts + 2)\n\tbackHeavy[parts-1] = int64(parts + 2)\n\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, descending),\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, frontHeavy),\n\t\tweightedAllocation(total, backHeavy),\n\t)\n\n\tif parts == 2 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{3, 5}, {5, 3}, {1, 3}, {3, 1},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\n\t\t\t// Base fees do not scale with shard size, so reserve a modest\n\t\t\t// allowance while retaining most of the later shards' budget.\n\t\t\tallowance := remainingBudget / 16\n\t\t\tif allowance > 30_000 {\n\t\t\t\tallowance = 30_000\n\t\t\t}\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) minimumPlanParts(slots uint32) int {\n\tif slots <= 1 || r.liquidityMisses == 0 {\n\t\treturn 1\n\t}\n\n\t// Failed large shards are evidence that the next commitment should use\n\t// more corridors. Increase deliberately instead of discovering a\n\t// halving ladder one failed probe at a time.\n\tminParts := 1 + (r.liquidityMisses+1)/2\n\tif minParts > int(slots) {\n\t\tminParts = int(slots)\n\t}\n\n\treturn minParts\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tminParts := r.minimumPlanParts(slots)\n\n\tfor parts := minParts; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(allocation, feeBudget)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// If no complete route set exists, dispatch one useful shard. This is\n\t// also the escape hatch for asymmetric public HTLC limits that are not\n\t// captured by the allocation templates.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tcap := feeBudget\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\tcap = feeBudget * shard / total\n\t\t\t\tallowance := feeBudget / 20\n\t\t\t\tif allowance > 20_000 {\n\t\t\t\t\tallowance = 20_000\n\t\t\t\t}\n\t\t\t\tif cap <= feeBudget-allowance {\n\t\t\t\t\tcap += allowance\n\t\t\t\t} else {\n\t\t\t\t\tcap = feeBudget\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports a failure on its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) observeLiquidityFailure(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tedge := meta.path[failIdx]\n\tload := meta.loads[failIdx]\n\tkey := edge.key\n\n\tr.failureVotes[key]++\n\tif old := r.suspectUpper[key]; old == 0 || load < old {\n\t\tr.suspectUpper[key] = load\n\t}\n\n\tvotes := r.failureVotes[key]\n\tr.edgePenalty[key] += 450_000 + float64(votes)*175_000\n\n\t// A shifted attribution is usually adjacent to the real failure. Give\n\t// adjacent edges only a reversible diversity penalty, never a bound.\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 90_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 90_000\n\t}\n\n\t// Two matching reports are required before writing a reusable liquidity\n\t// bound. This sharply reduces poisoning from shifted blame.\n\tif votes < 2 {\n\t\treturn\n\t}\n\n\tupper := r.suspectUpper[key]\n\tupdateBelief(key, upper, false, now)\n\tr.recordLocalFailure(key, upper)\n\n\t// Keep some memory so repeated corroborated failures tighten quickly,\n\t// without treating one later transient as a permanent fact.\n\tr.failureVotes[key] = 1\n}\n\nfunc (r *candidateRouter) recordSuccessfulRoute(meta *routeMeta,\n\tnow time.Time) {\n\n\tfor i, edge := range meta.path {\n\t\tkey := edge.key\n\t\tupdateBelief(key, meta.loads[i], true, now)\n\t\tr.reserved[key] += meta.amounts[i]\n\t\tr.edgePenalty[key] *= 0.25\n\n\t\tif upper := r.localUpper[key];\n\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\tdelete(r.localUpper, key)\n\t\t\tdelete(r.localFails, key)\n\t\t}\n\t\tif upper := r.suspectUpper[key];\n\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\tdelete(r.suspectUpper, key)\n\t\t\tdelete(r.failureVotes, key)\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tr.recordSuccessfulRoute(meta, now)\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\tif r.liquidityMisses > 0 {\n\t\t\tr.liquidityMisses--\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif code == lnwire.CodeTemporaryChannelFailure || code == 0 {\n\t\tr.liquidityMisses++\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\t\tkey := edge.key\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.observeLiquidityFailure(meta, failIdx, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\t// A policy failure is deterministic, but shifted attribution\n\t\t\t// still makes the first report unsafe to blacklist permanently.\n\t\t\tr.policyFails[key]++\n\t\t\tr.edgePenalty[key] += 4_000_000\n\t\t\tif r.policyFails[key] >= 2 {\n\t\t\t\tr.blocked[key] = true\n\t\t\t\tr.edgePenalty[key] += 8_000_000\n\t\t\t}\n\n\t\tdefault:\n\t\t\tr.edgePenalty[key] += 1_200_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unreadable failure proves only that this route failed. It must not\n\t// write success prefixes, liquidity ceilings, or fee-budget deductions.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 225_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 6,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\triskPriceMsat = float64(420_000)\n\thopCostMsat = float64(55_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 12\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 48\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBase := int64(amt) + outbound\n\tif inboundBase < int64(amt) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBase*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tpenalty map[edgeKey]float64\n\n\tliquidityBlame map[edgeKey]int\n\tblameLoad map[edgeKey]lnwire.MilliSatoshi\n\tpolicyBlame map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tpenalty: make(map[edgeKey]float64),\n\t\tliquidityBlame: make(map[edgeKey]int),\n\t\tblameLoad: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpolicyBlame: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFee := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFee].fee {\n\t\t\t\tminFee = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge == lnwire.MaxMilliSatoshi ||\n\t\t\t\t\tnodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.70\n\t\t\t\t} else if r.liquidityBlame[edge.key] > 0 {\n\t\t\t\t\tp = 0.88\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\triskPriceMsat*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.penalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge == lnwire.MaxMilliSatoshi ||\n\t\t\tcharge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(int64(total) * weight / sum)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationKey(a []lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(a)*9)\n\tfor _, amt := range a {\n\t\tv := uint64(amt)\n\t\tfor i := 0; i < 8; i++ {\n\t\t\tbuf = append(buf, byte(v))\n\t\t\tv >>= 8\n\t\t}\n\t\tbuf = append(buf, 0xff)\n\t}\n\treturn string(buf)\n}\n\nfunc (r *candidateRouter) allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 24)\n\tseen := make(map[string]bool)\n\n\tadd := func(a []lnwire.MilliSatoshi) {\n\t\tif len(a) != parts {\n\t\t\treturn\n\t\t}\n\n\t\tvar sum lnwire.MilliSatoshi\n\t\tfor _, part := range a {\n\t\t\tif part <= 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsum += part\n\t\t}\n\t\tif sum != total {\n\t\t\treturn\n\t\t}\n\n\t\tkey := allocationKey(a)\n\t\tif seen[key] {\n\t\t\treturn\n\t\t}\n\t\tseen[key] = true\n\t\tpatterns = append(patterns, a)\n\t}\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\tadd(weightedAllocation(total, equal))\n\n\tswitch parts {\n\tcase 2:\n\t\tfor split := int64(1); split <= 9; split++ {\n\t\t\tadd(weightedAllocation(\n\t\t\t\ttotal, []int64{split, 10 - split},\n\t\t\t))\n\t\t}\n\n\t\taddBound := func(bound lnwire.MilliSatoshi) {\n\t\t\tif bound <= 0 || bound >= total {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tadd([]lnwire.MilliSatoshi{bound, total - bound})\n\t\t\tadd([]lnwire.MilliSatoshi{total - bound, bound})\n\n\t\t\tsafe := bound * 19 / 20\n\t\t\tif safe > 0 && safe < total {\n\t\t\t\tadd([]lnwire.MilliSatoshi{safe, total - safe})\n\t\t\t\tadd([]lnwire.MilliSatoshi{total - safe, safe})\n\t\t\t}\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\t\tbound := edge.capacity\n\t\t\tif edge.maxHTLC > 0 && edge.maxHTLC < bound {\n\t\t\t\tbound = edge.maxHTLC\n\t\t\t}\n\t\t\taddBound(bound)\n\t\t}\n\n\t\tfor _, edges := range r.incomingEdges {\n\t\t\tfor _, edge := range edges {\n\t\t\t\tif edge.from != r.source {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tbound := edge.capacity\n\t\t\t\tif edge.maxHTLC > 0 && edge.maxHTLC < bound {\n\t\t\t\t\tbound = edge.maxHTLC\n\t\t\t\t}\n\t\t\t\tif balance, ok := r.localBalances[edge.chanID];\n\t\t\t\t\tok && balance < bound {\n\n\t\t\t\t\tbound = balance\n\t\t\t\t}\n\n\t\t\t\taddBound(bound * 19 / 20)\n\t\t\t}\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{2, 3, 5}, {5, 3, 2},\n\t\t\t{2, 4, 4}, {4, 4, 2},\n\t\t\t{1, 3, 6}, {6, 3, 1},\n\t\t} {\n\t\t\tadd(weightedAllocation(total, weights))\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{1, 2, 3, 4}, {4, 3, 2, 1},\n\t\t\t{1, 3, 3, 3}, {3, 3, 3, 1},\n\t\t} {\n\t\t\tadd(weightedAllocation(total, weights))\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\t\t\tadd(weightedAllocation(total, ascending))\n\t\t\tadd(weightedAllocation(total, descending))\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tproportionalBudget bool) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tvar remainingAmount lnwire.MilliSatoshi\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\tvar totalScore float64\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif proportionalBudget &&\n\t\t\tremainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 10\n\t\t\tif allowance > 100_000 {\n\t\t\t\tallowance = 100_000\n\t\t\t}\n\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range r.allocationPatterns(total, parts) {\n\t\t\tfor _, proportional := range []bool{true, false} {\n\t\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\t\tallocation, feeBudget, proportional,\n\t\t\t\t)\n\t\t\t\tif ok && score < bestScore {\n\t\t\t\t\tbest = plan\n\t\t\t\t\tbestScore = score\n\t\t\t\t}\n\n\t\t\t\tif feeBudget == lnwire.MaxMilliSatoshi {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tnext := shard * 3 / 4\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) observeLiquidityBlame(meta *routeMeta, idx int,\n\tnow time.Time) {\n\n\tif idx < 0 || idx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfor offset := -1; offset <= 1; offset++ {\n\t\ti := idx + offset\n\t\tif i < 0 || i >= len(meta.path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tweight := float64(100_000)\n\t\tif offset == 0 {\n\t\t\tweight = 450_000\n\t\t}\n\t\tr.penalty[meta.path[i].key] += weight\n\t}\n\n\tedge := meta.path[idx]\n\tkey := edge.key\n\tload := meta.loads[idx]\n\n\tr.liquidityBlame[key]++\n\tif old := r.blameLoad[key]; old == 0 || load < old {\n\t\tr.blameLoad[key] = load\n\t}\n\n\tif r.liquidityBlame[key] >= 2 {\n\t\tbound := r.blameLoad[key]\n\t\tupdateBelief(key, bound, false, now)\n\t\tr.recordLocalFailure(key, bound)\n\n\t\tcount := r.localFails[key]\n\t\tr.penalty[key] += 250_000 + float64(count)*125_000\n\t}\n}\n\nfunc (r *candidateRouter) observePolicyBlame(meta *routeMeta, idx int) {\n\tif idx < 0 || idx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tedge := meta.path[idx]\n\tr.policyBlame[edge.key]++\n\tr.penalty[edge.key] += 1_500_000\n\n\tif r.policyBlame[edge.key] >= 2 {\n\t\tr.blocked[edge.key] = true\n\t\tr.penalty[edge.key] += 8_000_000\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.penalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t\tdelete(r.liquidityBlame, edge.key)\n\t\t\t\tdelete(r.blameLoad, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// Attribution can move by one hop. Edges strictly before the edge\n\t// preceding the reported culprit therefore passed regardless of\n\t// whether the report was exact or shifted.\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tif safePassed > len(meta.path) {\n\t\tsafePassed = len(meta.path)\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t\tr.penalty[meta.path[i].key] *= 0.70\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.observeLiquidityBlame(meta, failIdx, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.observePolicyBlame(meta, failIdx)\n\n\t\tdefault:\n\t\t\tr.penalty[meta.path[failIdx].key] += 1_000_000\n\t\t}\n\t\treturn nil\n\t}\n\n\t// An unreadable report contains no channel evidence. Diversify the next\n\t// route, but do not create liquidity bounds or consume fee budget.\n\tfor _, edge := range meta.path {\n\t\tr.penalty[edge.key] += 350_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 7,
"parent": 0,
"score": 0.2792,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(18_000)\n\tdepthCostMsat = float64(3_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 44\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 64\n\tmaxConsecutiveMisses = 36\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tbasis := int64(amt) + outbound\n\tif basis < 0 {\n\t\treturn 0\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tbasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype failureEvidence struct {\n\tcount int\n\tload lnwire.MilliSatoshi\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tpenalty map[edgeKey]float64\n\n\tliquidityEvidence map[edgeKey]failureEvidence\n\tpolicyEvidence map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tpenalty: make(map[edgeKey]float64),\n\t\tliquidityEvidence: make(map[edgeKey]failureEvidence),\n\t\tpolicyEvidence: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(\n\tb liquidityBelief, now time.Time) float64 {\n\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\tif weight > 1 {\n\t\tweight = 1\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf += weight * (1 - b.conf)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.60\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] ||\n\t\t\t\tcontainsNode(cur, edge.from) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(\n\t\t\t\t\tedge, cur.nextEdge, cur.amount,\n\t\t\t\t)\n\t\t\t\tif nodeCharge >\n\t\t\t\t\tlnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.75\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tfloat64(cur.hops)*depthCostMsat +\n\t\t\t\tr.penalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\tmeta.fee > feeCap {\n\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(\n\t\t\tpath[i], path[i+1], amounts[i+1],\n\t\t)\n\t\tif charge >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(total) * weight / sum,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tvar variants [][]int64\n\tswitch parts {\n\tcase 2:\n\t\tvariants = [][]int64{\n\t\t\t{40, 60}, {60, 40},\n\t\t\t{33, 67}, {67, 33},\n\t\t\t{25, 75}, {75, 25},\n\t\t\t{15, 85}, {85, 15},\n\t\t}\n\n\tcase 3:\n\t\tvariants = [][]int64{\n\t\t\t{20, 30, 50}, {50, 30, 20},\n\t\t\t{15, 35, 50}, {50, 35, 15},\n\t\t\t{20, 40, 40}, {40, 40, 20},\n\t\t\t{10, 30, 60}, {60, 30, 10},\n\t\t}\n\n\tcase 4:\n\t\tvariants = [][]int64{\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t\t{10, 20, 30, 40},\n\t\t\t{40, 30, 20, 10},\n\t\t\t{15, 25, 30, 30},\n\t\t\t{30, 30, 25, 15},\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\t\t\tgentleUp := make([]int64, parts)\n\t\t\tgentleDown := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t\tgentleUp[i] = int64(parts + i)\n\t\t\t\tgentleDown[i] = int64(2*parts - i - 1)\n\t\t\t}\n\n\t\t\tvariants = append(\n\t\t\t\tvariants, ascending, descending,\n\t\t\t\tgentleUp, gentleDown,\n\t\t\t)\n\t\t}\n\t}\n\n\tfor _, weights := range variants {\n\t\tpatterns = append(\n\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t)\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(\n\tdst map[edgeKey]lnwire.MilliSatoshi, meta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute,\n\tfloat64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tproportional :=\n\t\t\t\tremainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 8\n\t\t\tif allowance > 50_000 {\n\t\t\t\tallowance = 50_000\n\t\t\t}\n\n\t\t\tif proportional <= remainingBudget-allowance {\n\t\t\t\tshardCap = proportional + allowance\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, shardCap,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore +=\n\t\tfloat64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(\n\ttotal lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(\n\t\t\ttotal, parts,\n\t\t) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tpr, err := r.findRoute(\n\t\t\t\tshard, r.reserved, feeBudget,\n\t\t\t)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(\n\tplan []*plannedRoute) lnwire.MilliSatoshi {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\n\t\t\t\"no progress after repeated failures\",\n\t\t)\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(\n\trt *route.Route) *routeMeta {\n\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(\n\t\t\tloads, amount+r.reserved[key],\n\t\t)\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered :=\n\t\trt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(\n\tkey edgeKey, load lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(\n\trt *route.Route, result routing.SimHtlcResult) int {\n\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes != result.FailureSource {\n\t\t\tcontinue\n\t\t}\n\n\t\tnext := i + 1\n\t\tif next >= len(rt.Hops) {\n\t\t\treturn len(rt.Hops) - 1\n\t\t}\n\n\t\treturn next\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) addLiquidityEvidence(\n\tkey edgeKey, load lnwire.MilliSatoshi) bool {\n\n\tev := r.liquidityEvidence[key]\n\tev.count++\n\tif ev.load == 0 || load < ev.load {\n\t\tev.load = load\n\t}\n\tr.liquidityEvidence[key] = ev\n\n\treturn ev.count >= 2\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.penalty[edge.key] *= 0.25\n\n\t\t\tdelete(r.liquidityEvidence, edge.key)\n\t\t\tdelete(r.policyEvidence, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t/*\n\t\tA shifted attribution can only invalidate the observation\n\t\timmediately adjacent to the reported failure. Earlier edges are\n\t\tsafe evidence that the HTLC passed, but the edge directly before\n\t\tthe report is deliberately left untouched.\n\t*/\n\tif failIdx >= 2 {\n\t\tfor i := 0; i < failIdx-1 &&\n\t\t\ti < len(meta.path); i++ {\n\n\t\t\tupdateBelief(\n\t\t\t\tmeta.path[i].key, meta.loads[i],\n\t\t\t\ttrue, 0.38, now,\n\t\t\t)\n\t\t}\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tconfirmed := r.addLiquidityEvidence(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\n\t\t\tr.penalty[edge.key] += 225_000\n\t\t\tif confirmed {\n\t\t\t\tev := r.liquidityEvidence[edge.key]\n\t\t\t\tupdateBelief(\n\t\t\t\t\tedge.key, ev.load, false,\n\t\t\t\t\t0.48, now,\n\t\t\t\t)\n\t\t\t\tr.recordLocalFailure(edge.key, ev.load)\n\n\t\t\t\tcount := r.localFails[edge.key]\n\t\t\t\tr.penalty[edge.key] +=\n\t\t\t\t\t180_000 +\n\t\t\t\t\t\tfloat64(count)*120_000\n\t\t\t}\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyEvidence[edge.key]++\n\t\t\tr.penalty[edge.key] += 1_500_000\n\n\t\t\tif r.policyEvidence[edge.key] >= 2 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t\tr.penalty[edge.key] += 6_000_000\n\t\t\t}\n\n\t\tdefault:\n\t\t\tr.penalty[edge.key] += 700_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t/*\n\t\tAn unreadable failure carries no channel attribution. Apply only\n\t\ta distributed, payment-local route penalty. It encourages a\n\t\tdifferent attempt without manufacturing a liquidity ceiling on\n\t\tany channel.\n\t*/\n\tfor _, edge := range meta.path {\n\t\tr.penalty[edge.key] += 45_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 8,
"parent": 0,
"score": 0.0465,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(12_000)\n\treusedEdgeCostMsat = float64(1_350_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 6\n\tplanBeamWidth = 14\n\tmaxRouterAttempts = 56\n\tmaxConsecutiveMisses = 36\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype failureSuspicion struct {\n\tweight float64\n\tupper lnwire.MilliSatoshi\n\tat time.Time\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\n\tliquiditySuspect map[edgeKey]failureSuspicion\n\tpolicySuspect map[edgeKey]failureSuspicion\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tliquiditySuspect: make(map[edgeKey]failureSuspicion),\n\t\tpolicySuspect: make(map[edgeKey]failureSuspicion),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tstrength float64, now time.Time) {\n\n\tif amt <= 0 || strength <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+strength)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) amountAwarePenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif upper == 0 {\n\t\tupper = r.liquiditySuspect[key].upper\n\t}\n\tif upper <= 0 || load >= upper {\n\t\treturn penalty\n\t}\n\n\tswitch {\n\tcase load*2 <= upper:\n\t\treturn penalty * 0.06\n\tcase load*4 <= upper*3:\n\t\treturn penalty * 0.22\n\tdefault:\n\t\treturn penalty * 0.55\n\t}\n}\n\nfunc (r *candidateRouter) adjustProbability(key edgeKey,\n\tload lnwire.MilliSatoshi, p float64) float64 {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn p\n\t}\n\n\tswitch {\n\tcase load >= upper:\n\t\tp *= 0.10\n\tcase load*4 >= upper*3:\n\t\tp *= 0.40\n\tcase load*2 >= upper:\n\t\tp *= 0.75\n\t}\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\treturn p\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t}\n\t\t\tp = r.adjustProbability(edge.key, totalLoad, p)\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.amountAwarePenalty(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.60\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\ntype planState struct {\n\tplan []*plannedRoute\n\treservations map[edgeKey]lnwire.MilliSatoshi\n\tremaining lnwire.MilliSatoshi\n\tbudget lnwire.MilliSatoshi\n\tscore float64\n}\n\nfunc planRank(s *planState, total lnwire.MilliSatoshi) float64 {\n\tplaced := total - s.remaining\n\tif placed <= 0 {\n\t\treturn math.Inf(1)\n\t}\n\n\treturn s.score * float64(total) / float64(placed)\n}\n\nfunc candidateShardAmounts(remaining lnwire.MilliSatoshi,\n\tslotsLeft int) []lnwire.MilliSatoshi {\n\n\tif slotsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{remaining}\n\t}\n\n\tfair := remaining / lnwire.MilliSatoshi(slotsLeft)\n\tif fair <= 0 {\n\t\treturn nil\n\t}\n\n\tmultipliers := []int64{\n\t\t100, 90, 110, 80, 120, 65, 140, 50, 160, 35, 180, 20,\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tresult := make([]lnwire.MilliSatoshi, 0, len(multipliers))\n\n\tfor _, multiplier := range multipliers {\n\t\tshard := lnwire.MilliSatoshi(\n\t\t\tint64(fair) * multiplier / 100,\n\t\t)\n\n\t\tmaxShard := remaining -\n\t\t\tlnwire.MilliSatoshi(slotsLeft-1)\n\t\tif shard > maxShard {\n\t\t\tshard = maxShard\n\t\t}\n\t\tif shard <= 0 || shard >= remaining || seen[shard] {\n\t\t\tcontinue\n\t\t}\n\n\t\tseen[shard] = true\n\t\tresult = append(result, shard)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) exactPlan(total lnwire.MilliSatoshi,\n\tparts int, feeBudget lnwire.MilliSatoshi) ([]*plannedRoute,\n\tfloat64, bool) {\n\n\tinitial := &planState{\n\t\treservations: cloneReservations(r.reserved),\n\t\tremaining: total,\n\t\tbudget: feeBudget,\n\t}\n\n\tbeam := []*planState{initial}\n\n\tfor depth := 0; depth < parts; depth++ {\n\t\tslotsLeft := parts - depth\n\t\tnextBeam := make([]*planState, 0, len(beam)*8)\n\n\t\tfor _, state := range beam {\n\t\t\tfor _, shard := range candidateShardAmounts(\n\t\t\t\tstate.remaining, slotsLeft,\n\t\t\t) {\n\t\t\t\tpr, err := r.findRoute(\n\t\t\t\t\tshard, state.reservations, state.budget,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnextBudget := state.budget\n\t\t\t\tif nextBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\t\tif pr.meta.fee > nextBudget {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tnextBudget -= pr.meta.fee\n\t\t\t\t}\n\n\t\t\t\treservations := cloneReservations(\n\t\t\t\t\tstate.reservations,\n\t\t\t\t)\n\t\t\t\taddReservationSet(reservations, pr.meta)\n\n\t\t\t\tplan := make([]*plannedRoute, len(state.plan), parts)\n\t\t\t\tcopy(plan, state.plan)\n\t\t\t\tplan = append(plan, pr)\n\n\t\t\t\tnextBeam = append(nextBeam, &planState{\n\t\t\t\t\tplan: plan,\n\t\t\t\t\treservations: reservations,\n\t\t\t\t\tremaining: state.remaining - shard,\n\t\t\t\t\tbudget: nextBudget,\n\t\t\t\t\tscore: state.score + pr.score,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(nextBeam) == 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tsort.Slice(nextBeam, func(i, j int) bool {\n\t\t\treturn planRank(nextBeam[i], total) <\n\t\t\t\tplanRank(nextBeam[j], total)\n\t\t})\n\n\t\tif len(nextBeam) > planBeamWidth {\n\t\t\tnextBeam = nextBeam[:planBeamWidth]\n\t\t}\n\t\tbeam = nextBeam\n\t}\n\n\tvar best *planState\n\tbestScore := math.Inf(1)\n\n\tfor _, state := range beam {\n\t\tif state.remaining != 0 || len(state.plan) != parts {\n\t\t\tcontinue\n\t\t}\n\n\t\tscore := state.score +\n\t\t\tfloat64(parts-1)*extraPartCostMsat\n\t\tif score < bestScore {\n\t\t\tbest = state\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, 0, false\n\t}\n\n\treturn best.plan, bestScore, true\n}\n\nfunc (r *candidateRouter) targetShardCeiling() lnwire.MilliSatoshi {\n\tvar ceiling lnwire.MilliSatoshi\n\n\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\tavailable := edge.capacity - r.reserved[edge.key]\n\t\tif available <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tedgeCeiling := available\n\t\tif edge.maxHTLC > 0 && edge.maxHTLC < edgeCeiling {\n\t\t\tedgeCeiling = edge.maxHTLC\n\t\t}\n\t\tif edgeCeiling > ceiling {\n\t\t\tceiling = edgeCeiling\n\t\t}\n\t}\n\n\treturn ceiling\n}\n\nfunc (r *candidateRouter) minimumParts(total lnwire.MilliSatoshi) int {\n\tceiling := r.targetShardCeiling()\n\tif ceiling <= 0 {\n\t\treturn 1\n\t}\n\n\tparts := int((total + ceiling - 1) / ceiling)\n\tif parts < 1 {\n\t\tparts = 1\n\t}\n\treturn parts\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32, allowPartial bool) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tminParts := r.minimumParts(total)\n\tif minParts > int(slots) {\n\t\tif !allowPartial {\n\t\t\treturn nil, errors.New(\"public htlc limits require more parts\")\n\t\t}\n\t\tminParts = int(slots)\n\t}\n\n\tmaxParts := minParts + 4\n\tif maxParts > int(slots) {\n\t\tmaxParts = int(slots)\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := minParts; parts <= maxParts; parts++ {\n\t\tplan, score, ok := r.exactPlan(total, parts, feeBudget)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif allowPartial && total > 1 {\n\t\tdivisors := []lnwire.MilliSatoshi{2, 3, 4, 6, 8}\n\t\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\t\tfor _, divisor := range divisors {\n\t\t\tshard := total / divisor\n\t\t\tif shard <= 0 || shard >= total || seen[shard] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[shard] = true\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(\n\t\t\tamt, slots, inFlightHtlcs == 0,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes != result.FailureSource {\n\t\t\tcontinue\n\t\t}\n\n\t\tidx := i + 1\n\t\tif idx >= len(rt.Hops) {\n\t\t\tidx = len(rt.Hops) - 1\n\t\t}\n\t\treturn idx\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) addLiquiditySuspicion(key edgeKey,\n\tload lnwire.MilliSatoshi, weight float64, now time.Time) bool {\n\n\ts := r.liquiditySuspect[key]\n\tif !s.at.IsZero() && (now.Before(s.at) ||\n\t\tnow.Sub(s.at) > 10*time.Minute) {\n\n\t\ts = failureSuspicion{}\n\t}\n\n\ts.weight += weight\n\tif s.upper == 0 || load < s.upper {\n\t\ts.upper = load\n\t}\n\ts.at = now\n\n\tconfirmed := s.weight >= 1.20\n\tif confirmed {\n\t\ts.weight *= 0.20\n\t}\n\tr.liquiditySuspect[key] = s\n\n\treturn confirmed\n}\n\nfunc (r *candidateRouter) addPolicySuspicion(key edgeKey,\n\tweight float64, now time.Time) bool {\n\n\ts := r.policySuspect[key]\n\tif !s.at.IsZero() && (now.Before(s.at) ||\n\t\tnow.Sub(s.at) > 10*time.Minute) {\n\n\t\ts = failureSuspicion{}\n\t}\n\n\ts.weight += weight\n\ts.at = now\n\n\tconfirmed := s.weight >= 1.80\n\tif confirmed {\n\t\ts.weight *= 0.20\n\t}\n\tr.policySuspect[key] = s\n\n\treturn confirmed\n}\n\nfunc (r *candidateRouter) confirmLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi, now time.Time) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n\n\tupdateBelief(key, load, false, 0.38, now)\n}\n\nfunc (r *candidateRouter) suspectRange(meta *routeMeta, center int,\n\tnow time.Time, policy bool) {\n\n\tfor offset := -1; offset <= 1; offset++ {\n\t\tidx := center + offset\n\t\tif idx < 0 || idx >= len(meta.path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tweight := 0.22\n\t\tpenalty := float64(350_000)\n\t\tif offset == 0 {\n\t\t\tweight = 0.65\n\t\t\tpenalty = 900_000\n\t\t}\n\n\t\tedge := meta.path[idx]\n\t\tr.edgePenalty[edge.key] += penalty\n\n\t\tif policy {\n\t\t\tif r.addPolicySuspicion(edge.key, weight, now) {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif r.addLiquiditySuspicion(\n\t\t\tedge.key, meta.loads[idx], weight, now,\n\t\t) {\n\t\t\tr.confirmLocalFailure(\n\t\t\t\tedge.key, meta.loads[idx], now,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.60, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.20\n\n\t\t\tdelete(r.liquiditySuspect, edge.key)\n\t\t\tdelete(r.policySuspect, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t\tdelete(r.blocked, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 120_000\n\t}\n\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn nil\n\t}\n\n\tcode := result.Failure.Code()\n\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.20, now,\n\t\t)\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.suspectRange(meta, failIdx, now, false)\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.suspectRange(meta, failIdx, now, true)\n\n\tdefault:\n\t\tr.edgePenalty[meta.path[failIdx].key] += 900_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 9,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(8_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 12\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 48\n\tminFallbackShard = lnwire.MilliSatoshi(10_000)\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 || int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\t// The inbound rate applies to the amount arriving at the forwarding\n\t// node: the amount forwarded onward plus its outbound forwarding fee.\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tfailureVotes map[edgeKey]float64\n\tpolicyVotes map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tinFlightHint uint32\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tfailureVotes: make(map[edgeKey]float64),\n\t\tpolicyVotes: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tgain := 0.35\n\tif success {\n\t\tgain = 0.65\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+gain)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge == lnwire.MaxMilliSatoshi ||\n\t\t\t\t\tnodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t} else if r.inFlightHint > 0 {\n\t\t\t\t\tp = 0.97\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge == lnwire.MaxMilliSatoshi ||\n\t\t\tcharge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tpartsLeft := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tmaxPart := remaining - partsLeft\n\t\tif part > maxPart {\n\t\t\tpart = maxPart\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc sameAllocation(a, b []lnwire.MilliSatoshi) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc appendAllocation(patterns *[][]lnwire.MilliSatoshi,\n\ttotal lnwire.MilliSatoshi, weights []int64) {\n\n\tallocation := weightedAllocation(total, weights)\n\tfor _, old := range *patterns {\n\t\tif sameAllocation(old, allocation) {\n\t\t\treturn\n\t\t}\n\t}\n\t*patterns = append(*patterns, allocation)\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 2*parts+8)\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\tappendAllocation(&patterns, total, equal)\n\n\tif parts == 1 {\n\t\treturn patterns\n\t}\n\n\tif parts == 2 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{5, 95}, {95, 5},\n\t\t\t{10, 90}, {90, 10},\n\t\t\t{20, 80}, {80, 20},\n\t\t\t{33, 67}, {67, 33},\n\t\t\t{40, 60}, {60, 40},\n\t\t} {\n\t\t\tappendAllocation(&patterns, total, weights)\n\t\t}\n\t\treturn patterns\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t}\n\tappendAllocation(&patterns, total, ascending)\n\tappendAllocation(&patterns, total, descending)\n\n\t// Rotations make route ordering part of the joint plan. This matters\n\t// because earlier shards reserve their corridors while later shards are\n\t// searched.\n\tfor heavy := 0; heavy < parts; heavy++ {\n\t\tweights := make([]int64, parts)\n\t\tfor i := range weights {\n\t\t\tweights[i] = 2\n\t\t}\n\t\tweights[heavy] = int64(parts * 3)\n\t\tappendAllocation(&patterns, total, weights)\n\t}\n\n\tfor light := 0; light < parts; light++ {\n\t\tweights := make([]int64, parts)\n\t\tfor i := range weights {\n\t\t\tweights[i] = 4\n\t\t}\n\t\tweights[light] = 1\n\t\tappendAllocation(&patterns, total, weights)\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc proportionalFeeCap(remainingBudget, shard,\n\tremainingAmount lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif remainingBudget == lnwire.MaxMilliSatoshi ||\n\t\tremainingAmount <= shard {\n\n\t\treturn remainingBudget\n\t}\n\n\tcap := remainingBudget * shard / remainingAmount\n\tallowance := remainingBudget / 20\n\tif allowance > 25_000 {\n\t\tallowance = 25_000\n\t}\n\n\tif cap <= remainingBudget-allowance {\n\t\tcap += allowance\n\t} else {\n\t\tcap = remainingBudget\n\t}\n\n\treturn cap\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tif remainingAmount > lnwire.MaxMilliSatoshi-shard {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := proportionalFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tmaxParts := int(slots)\n\tif lnwire.MilliSatoshi(maxParts) > total {\n\t\tmaxParts = int(total)\n\t}\n\n\tfor parts := 1; parts <= maxParts; parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// A single-slot sender may still complete a non-atomic MPP payment by\n\t// settling sequential shards. It must therefore retain the same lower\n\t// amount fallback used when several concurrent slots are available.\n\tdivisor := lnwire.MilliSatoshi(slots)\n\tif divisor < 1 {\n\t\tdivisor = 1\n\t}\n\n\tshard := (total + divisor - 1) / divisor\n\tif shard > total {\n\t\tshard = total\n\t}\n\n\tfor shard > 0 {\n\t\tcap := proportionalFeeCap(feeBudget, shard, total)\n\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= minFallbackShard {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 2 / 3\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tif next < minFallbackShard && shard > minFallbackShard {\n\t\t\tnext = minFallbackShard\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tr.inFlightHint = inFlightHtlcs\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes != result.FailureSource {\n\t\t\tcontinue\n\t\t}\n\n\t\t// A forwarding node reports the failure of its outgoing edge.\n\t\tidx := i + 1\n\t\tif idx >= len(rt.Hops) {\n\t\t\treturn -1\n\t\t}\n\t\treturn idx\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeRoute(meta *routeMeta, amount float64) {\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += amount\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tr.failureVotes[edge.key] *= 0.20\n\t\t\tr.policyVotes[edge.key] = 0\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// Every failure gets a small route-wide diversity penalty. No liquidity\n\t// fact is inferred from an unreadable source, and no prefix is marked as\n\t// successful because a shifted attribution can place the real failure\n\t// inside that prefix.\n\tr.penalizeRoute(meta, 75_000)\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeRoute(meta, 225_000)\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.failureVotes[edge.key]++\n\t\tvotes := r.failureVotes[edge.key]\n\n\t\tr.edgePenalty[edge.key] += 450_000 + votes*175_000\n\n\t\t// One plausible attribution is enough to explore another corridor,\n\t\t// but not enough to poison a liquidity bound. A second matching\n\t\t// report corroborates the observation.\n\t\tif votes >= 1.75 {\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t\tupdateBelief(edge.key, meta.loads[failIdx], false, now)\n\t\t}\n\n\t\t// Shifted blame is normally adjacent. A very small adjacent penalty\n\t\t// diversifies around that ambiguity without manufacturing bounds.\n\t\tif failIdx > 0 {\n\t\t\tprev := meta.path[failIdx-1]\n\t\t\tr.edgePenalty[prev.key] += 90_000\n\t\t}\n\t\tif failIdx+1 < len(meta.path) {\n\t\t\tnext := meta.path[failIdx+1]\n\t\t\tr.edgePenalty[next.key] += 90_000\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyVotes[edge.key]++\n\t\tr.edgePenalty[edge.key] += 2_500_000\n\n\t\t// Gossip-policy errors are deterministic, but attribution can still\n\t\t// be shifted. Require corroboration before permanently blocking the\n\t\t// directed edge for this payment.\n\t\tif r.policyVotes[edge.key] >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t\tr.edgePenalty[edge.key] += 6_000_000\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 1_500_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 10,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\triskPriceMsat = float64(420_000)\n\thopCostMsat = float64(8_000)\n\treusedEdgeCost = float64(1_100_000)\n\textraPartCost = float64(45_000)\n\tunknownRouteCost = float64(180_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 60\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBase := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBase*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\texploreSalt uint64\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc mix64(x uint64) uint64 {\n\tx ^= x >> 30\n\tx *= 0xbf58476d1ce4e5b9\n\tx ^= x >> 27\n\tx *= 0x94d049bb133111eb\n\tx ^= x >> 31\n\treturn x\n}\n\nfunc (r *candidateRouter) explorationCost(key edgeKey) float64 {\n\tif r.exploreSalt == 0 {\n\t\treturn 0\n\t}\n\n\tx := key.chanID ^ (r.exploreSalt * 0x9e3779b97f4a7c15)\n\treturn float64(mix64(x)%90_001)\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tvar cutoff lnwire.MilliSatoshi\n\tswitch {\n\tcase r.localFails[key] <= 1:\n\t\tcutoff = upper * 7 / 8\n\tcase r.localFails[key] == 2:\n\t\tcutoff = upper * 3 / 4\n\tdefault:\n\t\tcutoff = upper * 9 / 16\n\t}\n\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.998\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.72\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\triskPriceMsat*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key] +\n\t\t\t\tr.explorationCost(edge.key)\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCost\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := remaining * lnwire.MilliSatoshi(weight) /\n\t\t\tlnwire.MilliSatoshi(remainingWeight)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc reverseWeights(weights []int64) []int64 {\n\tresult := make([]int64, len(weights))\n\tfor i := range weights {\n\t\tresult[len(weights)-1-i] = weights[i]\n\t}\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tweights := [][]int64{equal}\n\n\tswitch parts {\n\tcase 2:\n\t\tweights = append(weights,\n\t\t\t[]int64{45, 55},\n\t\t\t[]int64{35, 65},\n\t\t\t[]int64{25, 75},\n\t\t\t[]int64{15, 85},\n\t\t\t[]int64{8, 92},\n\t\t)\n\n\tcase 3:\n\t\tweights = append(weights,\n\t\t\t[]int64{20, 30, 50},\n\t\t\t[]int64{15, 35, 50},\n\t\t\t[]int64{10, 25, 65},\n\t\t\t[]int64{10, 45, 45},\n\t\t)\n\n\tcase 4:\n\t\tweights = append(weights,\n\t\t\t[]int64{15, 20, 25, 40},\n\t\t\t[]int64{10, 15, 25, 50},\n\t\t\t[]int64{10, 20, 35, 35},\n\t\t)\n\n\tdefault:\n\t\tascending := make([]int64, parts)\n\t\tfrontHeavy := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\tascending[i] = int64(i + 1)\n\t\t\tfrontHeavy[i] = 1\n\t\t}\n\t\tfrontHeavy[0] = int64(parts)\n\t\tweights = append(weights, ascending, frontHeavy)\n\t}\n\n\tresult := make([][]lnwire.MilliSatoshi, 0, len(weights)*2)\n\tfor i, pattern := range weights {\n\t\tresult = append(result, weightedAllocation(total, pattern))\n\t\tif i > 0 {\n\t\t\tresult = append(\n\t\t\t\tresult,\n\t\t\t\tweightedAllocation(total, reverseWeights(pattern)),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tproportionalBudget bool) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif proportionalBudget &&\n\t\t\tremainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 12\n\t\t\tif allowance > 40_000 {\n\t\t\t\tallowance = 40_000\n\t\t\t}\n\t\t\tif allowance < 1_000 {\n\t\t\t\tallowance = 1_000\n\t\t\t}\n\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCost\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tfor _, proportional := range []bool{true, false} {\n\t\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\t\tallocation, feeBudget, proportional,\n\t\t\t\t)\n\t\t\t\tif ok && score < bestScore {\n\t\t\t\t\tbest = plan\n\t\t\t\t\tbestScore = score\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 1_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 3 / 4\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) int {\n\n\tif load <= 0 {\n\t\treturn r.localFails[key]\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n\n\treturn r.localFails[key]\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) diversifyUnknown(meta *routeMeta) {\n\tr.exploreSalt++\n\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := unknownRouteCost / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\tr.exploreSalt++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.diversifyUnknown(meta)\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tcount := r.recordLocalFailure(\n\t\t\tedge.key, meta.loads[failIdx],\n\t\t)\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t220_000 + float64(count)*140_000\n\n\t\t// A single attributed failure is useful for payment-local\n\t\t// down-sizing, but is not trusted enough to poison the shared\n\t\t// store. A repeated observation corroborates both the failing\n\t\t// edge and the successfully traversed prefix.\n\t\tif count >= 2 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, now,\n\t\t\t)\n\t\t\tfor i := 0; i < failIdx; i++ {\n\t\t\t\tupdateBelief(\n\t\t\t\t\tmeta.path[i].key, meta.loads[i], true, now,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyFails[edge.key]++\n\t\tr.edgePenalty[edge.key] += 2_000_000\n\n\t\t// Policy blame is also subject to shifted attribution. Require\n\t\t// corroboration before removing a directed edge for the payment.\n\t\tif r.policyFails[edge.key] >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 450_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 11,
"parent": 0,
"score": 0.3981,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(5_000)\n\treusedEdgeCostMsat = float64(1_100_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 80\n\tmaxConsecutiveMisses = 56\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBase := int64(amt) + outbound\n\tif inboundBase < 0 {\n\t\tinboundBase = 0\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBase*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tstrength float64, now time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+strength)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source &&\n\t\t\t\tr.localUpper[edge.key] == 0 {\n\n\t\t\t\tp = 0.995\n\t\t\t}\n\n\t\t\tlocalPenalty := float64(0)\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tfailures := r.localFails[edge.key]\n\t\t\t\tif failures < 1 {\n\t\t\t\t\tfailures = 1\n\t\t\t\t}\n\n\t\t\t\tlocalPenalty = 650_000 +\n\t\t\t\t\tfloat64(failures)*450_000\n\n\t\t\t\tif p > 0.18 {\n\t\t\t\t\tp = 0.18\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key] +\n\t\t\t\tlocalPenalty\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif sum <= 0 || total < lnwire.MilliSatoshi(len(weights)) {\n\t\treturn result\n\t}\n\n\tremaining := total\n\ttotalInt := int64(total)\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpartInt := (totalInt/sum)*weight +\n\t\t\t(totalInt%sum)*weight/sum\n\t\tpart := lnwire.MilliSatoshi(partInt)\n\n\t\tminRemaining := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minRemaining {\n\t\t\tpart = remaining - minRemaining\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 {\n\t\treturn nil\n\t}\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tif parts == 1 {\n\t\treturn patterns\n\t}\n\n\tunits := 8\n\tif parts == 2 {\n\t\tunits = 20\n\t}\n\n\tif parts <= 4 && parts <= units {\n\t\tweights := make([]int64, parts)\n\n\t\tvar compose func(int, int)\n\t\tcompose = func(pos, left int) {\n\t\t\tif pos == parts-1 {\n\t\t\t\tif left <= 0 {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tweights[pos] = int64(left)\n\t\t\t\tcopyWeights := append([]int64(nil), weights...)\n\t\t\t\tpatterns = append(\n\t\t\t\t\tpatterns,\n\t\t\t\t\tweightedAllocation(total, copyWeights),\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tminLeft := parts - pos - 1\n\t\t\tfor weight := 1; weight <= left-minLeft; weight++ {\n\t\t\t\tweights[pos] = int64(weight)\n\t\t\t\tcompose(pos+1, left-weight)\n\t\t\t}\n\t\t}\n\n\t\tcompose(0, units)\n\t\treturn patterns\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tcenterHeavy := make([]int64, parts)\n\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t\tcenterHeavy[i] = 1\n\t}\n\tcenterHeavy[parts/2] = int64(parts)\n\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, descending),\n\t\tweightedAllocation(total, centerHeavy),\n\t)\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, remainingBudget,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tcandidates := []lnwire.MilliSatoshi{\n\t\t\t(total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\t\tlnwire.MilliSatoshi(slots),\n\t\t\ttotal / 2,\n\t\t\ttotal * 2 / 5,\n\t\t\ttotal / 3,\n\t\t\ttotal / 4,\n\t\t\ttotal / 5,\n\t\t}\n\n\t\tfor _, shard := range candidates {\n\t\t\tif shard <= 0 || shard >= total {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.65, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\n\t\t\tstrength := 0.28\n\t\t\tif r.localFails[edge.key] >= 2 {\n\t\t\t\tstrength = 0.50\n\t\t\t}\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false,\n\t\t\t\tstrength, now,\n\t\t\t)\n\n\t\t\tcount := r.localFails[edge.key]\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\t250_000 + float64(count)*175_000\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyFails[edge.key]++\n\t\t\tr.edgePenalty[edge.key] += 2_000_000\n\n\t\t\tif r.policyFails[edge.key] >= 2 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t\tr.edgePenalty[edge.key] += 6_000_000\n\t\t\t}\n\n\t\tdefault:\n\t\t\tr.edgePenalty[edge.key] += 1_250_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 300_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 12,
"parent": 0,
"score": 0.2829,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(22_000)\n\treusedEdgeCostMsat = float64(1_150_000)\n\textraPartCostMsat = float64(55_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 64\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tcount := r.localFails[key]\n\tif upper <= 0 || count < 2 {\n\t\treturn 0\n\t}\n\n\tvar cutoff lnwire.MilliSatoshi\n\tswitch {\n\tcase count >= 5:\n\t\tcutoff = upper * 11 / 20\n\tcase count >= 3:\n\t\tcutoff = upper * 3 / 4\n\tdefault:\n\t\tcutoff = upper * 9 / 10\n\t}\n\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) amountPenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif penalty <= 0 || upper <= 0 || load >= upper {\n\t\treturn penalty\n\t}\n\n\tratio := float64(load) / float64(upper)\n\tfactor := 0.08 + 0.92*ratio*ratio\n\treturn penalty * factor\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localFails[edge.key] == 1 {\n\t\t\t\t\tp = 0.88\n\t\t\t\t} else if r.localFails[edge.key] > 1 {\n\t\t\t\t\tp = 0.70\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.amountPenalty(edge.key, totalLoad)\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := remaining * lnwire.MilliSatoshi(weight) /\n\t\t\tlnwire.MilliSatoshi(remainingWeight)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\tif parts == 1 {\n\t\treturn patterns\n\t}\n\n\tif parts <= 5 {\n\t\tfor pivot := 0; pivot < parts; pivot++ {\n\t\t\tmoderate := make([]int64, parts)\n\t\t\tlarge := make([]int64, parts)\n\t\t\tsmall := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tmoderate[i] = 1\n\t\t\t\tlarge[i] = 1\n\t\t\t\tsmall[i] = 2\n\t\t\t}\n\t\t\tmoderate[pivot] = 2\n\t\t\tlarge[pivot] = 4\n\t\t\tsmall[pivot] = 1\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, moderate),\n\t\t\t\tweightedAllocation(total, large),\n\t\t\t\tweightedAllocation(total, small),\n\t\t\t)\n\t\t}\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t}\n\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, descending),\n\t)\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, remainingBudget,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// A full route set may be impossible because of a public HTLC ceiling,\n\t// an uneven corridor set, or a narrow fee budget. Send the largest\n\t// useful shard near the fair-share size. This also permits sequential\n\t// splitting when MaxParts is one and completed shards release the slot.\n\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\tlnwire.MilliSatoshi(slots)\n\tif shard > total {\n\t\tshard = total\n\t}\n\n\tfor tries := 0; tries < 24 && shard > 0; tries++ {\n\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= 1_000 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 4 / 5\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) int {\n\n\tif load <= 0 {\n\t\treturn r.localFails[key]\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n\n\treturn r.localFails[key]\n}\n\nfunc (r *candidateRouter) addPenalty(key edgeKey, amount float64) {\n\tr.edgePenalty[key] += amount\n\tif r.edgePenalty[key] > 20_000_000 {\n\t\tr.edgePenalty[key] = 20_000_000\n\t}\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t\tdelete(r.policyFails, edge.key)\n\t\t\tdelete(r.blocked, edge.key)\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcount := r.recordLocalFailure(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\n\t\t\t// A single attribution is only a suspect observation. A\n\t\t\t// second matching report is required before it becomes shared\n\t\t\t// liquidity evidence.\n\t\t\tif count >= 2 {\n\t\t\t\tupdateBelief(\n\t\t\t\t\tedge.key, meta.loads[failIdx], false, now,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tr.addPenalty(\n\t\t\t\tedge.key, 420_000+float64(count)*190_000,\n\t\t\t)\n\n\t\t\t// Shifted blame is normally adjacent to the true edge. Give\n\t\t\t// neighbors a small diversity penalty without writing bounds.\n\t\t\tif failIdx > 0 {\n\t\t\t\tr.addPenalty(meta.path[failIdx-1].key, 110_000)\n\t\t\t}\n\t\t\tif failIdx+1 < len(meta.path) {\n\t\t\t\tr.addPenalty(meta.path[failIdx+1].key, 110_000)\n\t\t\t}\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyFails[edge.key]++\n\t\t\tr.addPenalty(edge.key, 4_000_000)\n\n\t\t\t// Policy errors are deterministic, but attribution can be\n\t\t\t// shifted. Require corroboration before removing an edge.\n\t\t\tif r.policyFails[edge.key] >= 2 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\n\t\tdefault:\n\t\t\tr.addPenalty(edge.key, 1_000_000)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An off-route failure source or an unreadable failure carries no\n\t// channel-specific information. Diversify away from the attempted\n\t// route, but never write a liquidity bound or block a policy edge.\n\tperEdge := float64(160_000)\n\tif code == lnwire.CodeTemporaryChannelFailure {\n\t\tperEdge = 240_000\n\t}\n\n\tfor _, edge := range meta.path {\n\t\tr.addPenalty(edge.key, perEdge)\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 13,
"parent": 0,
"score": 0.0337,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(15_000)\n\treusedEdgeCostMsat = float64(1_150_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 64\n\tmaxConsecutiveMisses = 30\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundAmount := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundAmount*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// This ledger contains liquidity committed or consumed by successful\n\t// shards from this payment. It is also used while constructing a joint\n\t// route set so sibling shards do not assume the same liquidity twice.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Failure evidence is corroborated before it becomes a hard local\n\t// retry ceiling. This limits damage from shifted failure attribution.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tfailureVotes map[edgeKey]float64\n\tpolicyVotes map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\tcurrentFlight uint32\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tfailureVotes: make(map[edgeKey]float64),\n\t\tpolicyVotes: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief,\n\tnow time.Time) float64 {\n\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// A failure ceiling remains soft because another payment or held\n\t\t// shard may have caused a temporary depletion.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, now time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState,\n\tnode route.Vertex) bool {\n\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.currentFlight > 0 {\n\t\t\t\t\tp = 0.97\n\t\t\t\t}\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{60, 40}, {40, 60},\n\t\t\t{70, 30}, {30, 70},\n\t\t\t{80, 20}, {20, 80},\n\t\t\t{90, 10}, {10, 90},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{50, 30, 20}, {20, 30, 50},\n\t\t\t{60, 25, 15}, {15, 25, 60},\n\t\t\t{45, 40, 15}, {15, 40, 45},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{40, 25, 20, 15},\n\t\t\t{15, 20, 25, 40},\n\t\t\t{55, 20, 15, 10},\n\t\t\t{10, 15, 20, 55},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(\n\tdst map[edgeKey]lnwire.MilliSatoshi, meta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 20\n\t\t\tif allowance > 25_000 {\n\t\t\t\tallowance = 25_000\n\t\t\t}\n\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc addAmountCandidate(dst *[]lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool,\n\tamt, total, minimum lnwire.MilliSatoshi) {\n\n\tif amt > total {\n\t\tamt = total\n\t}\n\tif amt < minimum || amt <= 0 || seen[amt] {\n\t\treturn\n\t}\n\n\tseen[amt] = true\n\t*dst = append(*dst, amt)\n}\n\nfunc (r *candidateRouter) partialCandidates(total,\n\tminimum lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tceiling := total\n\tswitch {\n\tcase r.consecutiveMisses >= 16:\n\t\tceiling = total / 8\n\tcase r.consecutiveMisses >= 11:\n\t\tceiling = total / 4\n\tcase r.consecutiveMisses >= 7:\n\t\tceiling = total / 3\n\tcase r.consecutiveMisses >= 4:\n\t\tceiling = total / 2\n\tcase r.consecutiveMisses >= 2:\n\t\tceiling = total * 3 / 4\n\tdefault:\n\t\tceiling = total * 5 / 6\n\t}\n\n\tif ceiling < minimum {\n\t\tceiling = minimum\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tresult := make([]lnwire.MilliSatoshi, 0, 16)\n\n\tfor _, amt := range []lnwire.MilliSatoshi{\n\t\tceiling,\n\t\tceiling * 7 / 8,\n\t\tceiling * 3 / 4,\n\t\tceiling * 2 / 3,\n\t\tceiling / 2,\n\t\ttotal * 3 / 4,\n\t\ttotal * 2 / 3,\n\t\ttotal / 2,\n\t\ttotal / 3,\n\t\ttotal / 4,\n\t\tminimum,\n\t} {\n\t\taddAmountCandidate(&result, seen, amt, total, minimum)\n\t}\n\n\tfor key, upper := range r.localUpper {\n\t\treserved := r.reserved[key]\n\t\tif upper <= reserved {\n\t\t\tcontinue\n\t\t}\n\n\t\tavailable := upper - reserved\n\t\taddAmountCandidate(\n\t\t\t&result, seen, available*2/3, total, minimum,\n\t\t)\n\t\taddAmountCandidate(\n\t\t\t&result, seen, available/2, total, minimum,\n\t\t)\n\t}\n\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i] > result[j]\n\t})\n\n\treturn result\n}\n\nfunc proportionalFeeCap(budget, shard,\n\ttotal lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\n\tcap := budget * shard / total\n\tallowance := budget / 20\n\tif allowance > 25_000 {\n\t\tallowance = 25_000\n\t}\n\n\tif cap <= budget-allowance {\n\t\tcap += allowance\n\t} else {\n\t\tcap = budget\n\t}\n\n\tif cap <= 0 && budget > 0 {\n\t\tcap = budget\n\t}\n\n\treturn cap\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32, allowPartial bool) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\t// With several slots, plan the complete route set before dispatching.\n\t// With one free slot, try complete payments briefly, then deliberately\n\t// reduce the shard instead of repeating full-size liquidity probes.\n\ttryComplete := slots > 1 ||\n\t\tr.consecutiveMisses < 2 ||\n\t\t!allowPartial\n\n\tif tryComplete {\n\t\tvar best []*plannedRoute\n\t\tbestScore := math.Inf(1)\n\n\t\tfor parts := 1; parts <= int(slots); parts++ {\n\t\t\tif total < lnwire.MilliSatoshi(parts) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\t\tallocation, feeBudget,\n\t\t\t\t)\n\t\t\t\tif ok && score < bestScore {\n\t\t\t\t\tbest = plan\n\t\t\t\t\tbestScore = score\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(best) > 0 {\n\t\t\treturn best, nil\n\t\t}\n\t}\n\n\tif !allowPartial {\n\t\treturn nil, errors.New(\"no complete route set found\")\n\t}\n\n\t// In an atomic payment, a partial shard must leave enough capacity in\n\t// the remaining slots to finish. For a single, currently unoccupied\n\t// slot this also supports sequential non-atomic MPP.\n\tminimum := lnwire.MilliSatoshi(1)\n\tif slots > 1 {\n\t\tminimum = (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\t}\n\tif minimum > total {\n\t\tminimum = total\n\t}\n\n\tfor _, shard := range r.partialCandidates(total, minimum) {\n\t\tif shard >= total && tryComplete {\n\t\t\tcontinue\n\t\t}\n\n\t\tcap := proportionalFeeCap(feeBudget, shard, total)\n\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(\n\tplan []*plannedRoute) lnwire.MilliSatoshi {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tr.currentFlight = inFlightHtlcs\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\t// Partial sequential routing is safe when no shard is currently\n\t\t// held. When shards are already held, the remaining free slots must\n\t\t// be planned as a complete set.\n\t\tallowPartial := inFlightHtlcs == 0 || slots > 1\n\n\t\tplan, err := r.makePlan(amt, slots, allowPartial)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(\n\trt *route.Route) *routeMeta {\n\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(\n\tkey edgeKey, load lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports failure of its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) addNeighborPenalty(\n\tmeta *routeMeta, idx int, penalty float64) {\n\n\tif idx < 0 || idx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tr.edgePenalty[meta.path[idx].key] += penalty\n}\n\nfunc (r *candidateRouter) recordAttributedLiquidityFailure(\n\tmeta *routeMeta, failIdx int, now time.Time) {\n\n\tedge := meta.path[failIdx]\n\tkey := edge.key\n\n\t// The reported edge receives most of the penalty. Adjacent edges get a\n\t// smaller diversity penalty because attribution can be shifted by one\n\t// hop, but they receive no liquidity bounds.\n\tr.edgePenalty[key] += 650_000\n\tr.addNeighborPenalty(meta, failIdx-1, 160_000)\n\tr.addNeighborPenalty(meta, failIdx+1, 160_000)\n\n\tr.failureVotes[key] += 1\n\tif r.failureVotes[key] < 1.8 {\n\t\treturn\n\t}\n\n\t// Two consistent observations are required before a potentially lying\n\t// failure source can write a liquidity ceiling.\n\tupdateBelief(key, meta.loads[failIdx], false, now)\n\tr.recordLocalFailure(key, meta.loads[failIdx])\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tr.failureVotes[edge.key] *= 0.20\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// An off-route source explicitly carries no channel information. A\n\t// destination source also cannot name an outgoing edge. Neither case\n\t// writes success or failure bounds.\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tfor _, edge := range meta.path {\n\t\t\tr.edgePenalty[edge.key] += 550_000\n\t\t}\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.recordAttributedLiquidityFailure(meta, failIdx, now)\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t// Policy attribution may also be shifted. Require a repeat before\n\t\t// permanently excluding the advertised direction.\n\t\tr.policyVotes[edge.key]++\n\t\tr.edgePenalty[edge.key] += 2_500_000\n\t\tr.addNeighborPenalty(meta, failIdx-1, 350_000)\n\t\tr.addNeighborPenalty(meta, failIdx+1, 350_000)\n\n\t\tif r.policyVotes[edge.key] >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t\tr.edgePenalty[edge.key] += 6_000_000\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 1_500_000\n\t\tr.addNeighborPenalty(meta, failIdx-1, 250_000)\n\t\tr.addNeighborPenalty(meta, failIdx+1, 250_000)\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 14,
"parent": 0,
"score": 0.1385,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(20_000)\n\treusedEdgeCostMsat = float64(950_000)\n\textraPartCostMsat = float64(55_000)\n\n\tmaxSearchHops = 32\n\tmaxLabelsPerNode = 7\n\tmaxRouterAttempts = 64\n\tmaxConsecutiveMisses = 48\n\tmaxPlannedParts = 12\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBasis := int64(amt) + outbound\n\tif inboundBasis < 0 {\n\t\treturn 0\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityVotes map[edgeKey]float64\n\tsuspectLoad map[edgeKey]lnwire.MilliSatoshi\n\tpolicyVotes map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityVotes: make(map[edgeKey]float64),\n\t\tsuspectLoad: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpolicyVotes: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.55)\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.40)\n\t}\n\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn result\n\t}\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 {\n\t\t\treturn result\n\t\t}\n\t\tsum += weight\n\t}\n\tif sum <= 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\ttotal64 := int64(total)\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tq := total64 / sum\n\t\trem := total64 % sum\n\t\tpart64 := q*weight + rem*weight/sum\n\t\tpart := lnwire.MilliSatoshi(part64)\n\n\t\tminLeft := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLeft {\n\t\t\tpart = remaining - minLeft\n\t\t}\n\t\tif part <= 0 {\n\t\t\treturn make([]lnwire.MilliSatoshi, len(weights))\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{40, 60}, {60, 40},\n\t\t\t{25, 75}, {75, 25},\n\t\t\t{10, 90}, {90, 10},\n\t\t\t{20, 80}, {80, 20},\n\t\t\t{33, 67}, {67, 33},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{20, 30, 50}, {50, 30, 20},\n\t\t\t{15, 35, 50}, {50, 35, 15},\n\t\t\t{20, 60, 20}, {45, 10, 45},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t\t{10, 20, 30, 40},\n\t\t\t{40, 30, 20, 10},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc reducedPatterns(patterns [][]lnwire.MilliSatoshi,\n\tattempts int) [][]lnwire.MilliSatoshi {\n\n\tif attempts < 10 || len(patterns) <= 5 {\n\t\treturn patterns\n\t}\n\n\tif len(patterns) >= 11 {\n\t\treturn [][]lnwire.MilliSatoshi{\n\t\t\tpatterns[0],\n\t\t\tpatterns[5],\n\t\t\tpatterns[6],\n\t\t\tpatterns[len(patterns)-2],\n\t\t\tpatterns[len(patterns)-1],\n\t\t}\n\t}\n\n\treturn [][]lnwire.MilliSatoshi{\n\t\tpatterns[0],\n\t\tpatterns[1],\n\t\tpatterns[len(patterns)/2],\n\t\tpatterns[len(patterns)-2],\n\t\tpatterns[len(patterns)-1],\n\t}\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, remainingBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tmaxParts := int(slots)\n\tif maxParts > maxPlannedParts {\n\t\tmaxParts = maxPlannedParts\n\t}\n\n\tpartCounts := make([]int, 0, maxParts)\n\tif r.attempts < 10 {\n\t\tfor parts := 1; parts <= maxParts; parts++ {\n\t\t\tpartCounts = append(partCounts, parts)\n\t\t}\n\t} else {\n\t\tpartCounts = append(partCounts, 1)\n\t\tif maxParts > 1 {\n\t\t\tpartCounts = append(partCounts, maxParts)\n\t\t}\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range partCounts {\n\t\tpatterns := reducedPatterns(\n\t\t\tallocationPatterns(total, parts), r.attempts,\n\t\t)\n\n\t\tfor _, allocation := range patterns {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor tries := 0; tries < 8 && shard > 0; tries++ {\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tnext := shard * 3 / 4\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) noteLiquidityFailure(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.20\n\t\tpenalty := float64(130_000)\n\t\tif i == failIdx {\n\t\t\tweight = 0.70\n\t\t\tpenalty = 475_000\n\t\t}\n\n\t\tr.liquidityVotes[key] += weight\n\t\tload := meta.loads[i]\n\t\tif old := r.suspectLoad[key]; old == 0 || load < old {\n\t\t\tr.suspectLoad[key] = load\n\t\t}\n\t\tr.edgePenalty[key] += penalty\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.liquidityVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx < 0 || best < 1.35 || best-second < 0.45 {\n\t\treturn\n\t}\n\n\tkey := meta.path[bestIdx].key\n\tload := r.suspectLoad[key]\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupdateBelief(key, load, false, now)\n\tr.recordLocalFailure(key, load)\n\tr.edgePenalty[key] +=\n\t\t250_000 + float64(r.localFails[key])*150_000\n\n\tlo := bestIdx - 1\n\tif lo < 0 {\n\t\tlo = 0\n\t}\n\thi := bestIdx + 1\n\tif hi >= len(meta.path) {\n\t\thi = len(meta.path) - 1\n\t}\n\tfor i := lo; i <= hi; i++ {\n\t\tk := meta.path[i].key\n\t\tr.liquidityVotes[k] *= 0.20\n\t}\n}\n\nfunc (r *candidateRouter) notePolicyFailure(meta *routeMeta,\n\tfailIdx int) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.20\n\t\tpenalty := float64(600_000)\n\t\tif i == failIdx {\n\t\t\tweight = 0.80\n\t\t\tpenalty = 3_000_000\n\t\t}\n\n\t\tr.policyVotes[key] += weight\n\t\tr.edgePenalty[key] += penalty\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.policyVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx >= 0 && best >= 1.55 && best-second >= 0.55 {\n\t\tkey := meta.path[bestIdx].key\n\t\tr.blocked[key] = true\n\t\tr.edgePenalty[key] += 8_000_000\n\t\tr.policyVotes[key] = 0\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tr.liquidityVotes[edge.key] *= 0.25\n\t\t\tr.policyVotes[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.noteLiquidityFailure(meta, failIdx, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.notePolicyFailure(meta, failIdx)\n\n\t\tdefault:\n\t\t\tr.edgePenalty[meta.path[failIdx].key] += 1_250_000\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 425_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 15,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(18_000)\n\treusedEdgeCostMsat = float64(1_200_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 48\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 72\n\tmaxConsecutiveMisses = 52\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinbound := int64(in.inboundBase) +\n\t\tint64(amt)*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype failureObservation struct {\n\tcount int\n\tminLoad lnwire.MilliSatoshi\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// Successful shards either hold liquidity in an atomic payment or have\n\t// already consumed it in a non-atomic payment.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Failure attribution is noisy. A single report only changes route\n\t// preference. A second consistent report is required before it becomes\n\t// a payment-local liquidity ceiling.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tliquidityReports map[edgeKey]failureObservation\n\tpolicyReports map[edgeKey]int\n\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n\tplanFailures int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tliquidityReports: make(map[edgeKey]failureObservation),\n\t\tpolicyReports: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\t// Failures may reflect temporary reservations or background traffic.\n\t// Evidence therefore softens, while fresh truthful successes remain\n\t// valuable during the current payment burst.\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.015\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.015 + 0.98*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tif success {\n\t\tb.conf = math.Min(1, b.conf+0.58)\n\t} else {\n\t\tb.conf = math.Min(0.92, b.conf+0.42)\n\t}\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.55\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 4 / 5\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 3\n\t}\n\tif r.localFails[key] >= 5 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\n\t\t\tif edge.from == r.source {\n\t\t\t\t// The local balance snapshot is stronger than a prior, but\n\t\t\t\t// reservations and corroborated local failures still matter.\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.72\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{45, 55}, {55, 45},\n\t\t\t{35, 65}, {65, 35},\n\t\t\t{25, 75}, {75, 25},\n\t\t\t{15, 85}, {85, 15},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{20, 30, 50}, {50, 30, 20},\n\t\t\t{15, 35, 50}, {50, 35, 15},\n\t\t\t{20, 40, 40}, {40, 40, 20},\n\t\t\t{10, 25, 65}, {65, 25, 10},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 20, 30, 40},\n\t\t\t{40, 30, 20, 10},\n\t\t\t{10, 20, 20, 50},\n\t\t\t{50, 20, 20, 10},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\t// Reserve most of the proportional budget for later shards,\n\t\t\t// while allowing the current route enough room for base fees.\n\t\t\tproportional := remainingBudget * shard / remainingAmount\n\t\t\tallowance := remainingBudget / 12\n\t\t\tif allowance > 50_000 {\n\t\t\t\tallowance = 50_000\n\t\t\t}\n\n\t\t\tif proportional <= remainingBudget-allowance {\n\t\t\t\tshardCap = proportional + allowance\n\t\t\t}\n\t\t\tif shardCap <= 0 {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) minimumPlanParts(slots uint32) int {\n\tminParts := 1\n\tif slots > 1 && r.planFailures >= 2 {\n\t\tminParts = 2\n\t}\n\tif slots > 2 && r.planFailures >= 7 {\n\t\tminParts = 3\n\t}\n\tif minParts > int(slots) {\n\t\tminParts = int(slots)\n\t}\n\n\treturn minParts\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tminParts := r.minimumPlanParts(slots)\n\n\tfor parts := minParts; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// After the first full-size miss, make another full-size probe\n\t\t\t// compete against deliberate MPP rather than winning solely\n\t\t\t// because it avoids an extra base fee.\n\t\t\tif parts == 1 && r.planFailures > 0 {\n\t\t\t\tscore += float64(r.planFailures) * 300_000\n\t\t\t}\n\n\t\t\tif score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// If a complete route set cannot be assembled, issue a useful shard.\n\t// This supports non-atomic payments and lets a later call plan against\n\t// the proven reservation. It also avoids a long ladder of near-identical\n\t// full-amount probes.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tif r.planFailures >= 4 {\n\t\t\tsmaller := total / 3\n\t\t\tif smaller > 0 && smaller < shard {\n\t\t\t\tshard = smaller\n\t\t\t}\n\t\t}\n\n\t\tfor shard > 0 {\n\t\t\tcap := feeBudget\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\tcap = feeBudget * shard / total\n\t\t\t\tallowance := feeBudget / 15\n\t\t\t\tif allowance > 30_000 {\n\t\t\t\t\tallowance = 30_000\n\t\t\t\t}\n\t\t\t\tif cap <= feeBudget-allowance {\n\t\t\t\t\tcap += allowance\n\t\t\t\t} else {\n\t\t\t\t\tcap = feeBudget\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, cap)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t// A forwarding node reports the failure of its outgoing edge.\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) addNeighborPenalty(meta *routeMeta, idx int,\n\tpenalty float64) {\n\n\tif idx > 0 {\n\t\tr.edgePenalty[meta.path[idx-1].key] += penalty\n\t}\n\tif idx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[idx+1].key] += penalty\n\t}\n}\n\nfunc (r *candidateRouter) observeLiquidityFailure(meta *routeMeta, idx int,\n\tnow time.Time, weak bool) {\n\n\tif idx < 0 || idx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tedge := meta.path[idx]\n\tobs := r.liquidityReports[edge.key]\n\tobs.count++\n\tif obs.minLoad == 0 || meta.loads[idx] < obs.minLoad {\n\t\tobs.minLoad = meta.loads[idx]\n\t}\n\tr.liquidityReports[edge.key] = obs\n\n\tr.edgePenalty[edge.key] +=\n\t\t400_000 + float64(obs.count)*125_000\n\tr.addNeighborPenalty(meta, idx, 110_000)\n\n\trequired := 2\n\tif weak {\n\t\trequired = 3\n\t}\n\tif obs.count < required {\n\t\treturn\n\t}\n\n\tupdateBelief(edge.key, obs.minLoad, false, now)\n\tr.recordLocalFailure(edge.key, obs.minLoad)\n}\n\nfunc (r *candidateRouter) safePassedPrefix(meta *routeMeta, failIdx int,\n\tnow time.Time) {\n\n\t// Attribution may move by one hop. Edges at least two positions before\n\t// the reported failure are still known to have forwarded successfully.\n\tlimit := failIdx - 1\n\tif limit < 0 {\n\t\treturn\n\t}\n\tif limit > len(meta.path) {\n\t\tlimit = len(meta.path)\n\t}\n\n\tfor i := 0; i < limit; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t\tr.edgePenalty[meta.path[i].key] *= 0.75\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t\tdelete(r.liquidityReports, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\tr.planFailures = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 {\n\t\tr.safePassedPrefix(meta, failIdx, now)\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.planFailures++\n\n\t\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\t\tr.observeLiquidityFailure(meta, failIdx, now, false)\n\t\t\treturn nil\n\t\t}\n\n\t\t// A destination-attributed channel failure is consistent with a\n\t\t// one-hop shift from the final edge. Treat it as weak evidence.\n\t\tif failIdx == len(meta.path) && len(meta.path) > 0 {\n\t\t\tr.observeLiquidityFailure(\n\t\t\t\tmeta, len(meta.path)-1, now, true,\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\t\tedge := meta.path[failIdx]\n\t\t\tr.policyReports[edge.key]++\n\t\t\tr.edgePenalty[edge.key] += 3_000_000\n\t\t\tr.addNeighborPenalty(meta, failIdx, 350_000)\n\n\t\t\t// Policy errors are deterministic, but attribution can still\n\t\t\t// shift. Require corroboration before permanently blocking.\n\t\t\tif r.policyReports[edge.key] >= 2 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\tdefault:\n\t\t// Most unreadable failures are liquidity failures with their code\n\t\t// stripped. Encourage an earlier deliberate split without writing\n\t\t// any channel-specific evidence.\n\t\tr.planFailures++\n\t}\n\n\t// An off-route or otherwise unreadable source carries no trustworthy\n\t// channel information. A small whole-route diversity penalty prevents\n\t// an immediate identical retry without poisoning liquidity bounds.\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 220_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 16,
"parent": 0,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(120_000)\n\n\thopCostMsat = float64(35_000)\n\treusedEdgeCostMsat = float64(1_600_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 32\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 80\n\tmaxConsecutiveMisses = 64\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := out.fee(amt)\n\tif outbound < 0 || outbound > lnwire.MaxMilliSatoshi-amt {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\t// Inbound fees are charged on the amount arriving at the forwarding\n\t// node: the amount forwarded onward plus that node's outbound fee.\n\tinboundAmount := amt + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tint64(inboundAmount)*int64(in.inboundRate)/1_000_000\n\n\ttotal := int64(outbound) + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\t// Failure attribution is noisy. The first observation is a suspicion\n\t// that affects scoring; repeated blame is required before it becomes a\n\t// hard payment-local constraint or a shared liquidity observation.\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tpolicyFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tpolicyFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.035)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+0.55)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) activeReservations(\n) map[edgeKey]lnwire.MilliSatoshi {\n\n\tresult := cloneReservations(r.reserved)\n\n\t// If a plan is rebuilt while another attempt is unresolved, that\n\t// attempt still occupies liquidity even though it is no longer queued.\n\tfor _, meta := range r.issued {\n\t\taddReservationSet(result, meta)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 1.5\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tcount := r.localFails[key]\n\tif upper <= 0 || count < 2 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif count >= 4 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) localProbability(key edgeKey,\n\tload lnwire.MilliSatoshi, p float64) float64 {\n\n\tupper := r.localUpper[key]\n\tcount := r.localFails[key]\n\tif upper <= 0 || count == 0 {\n\t\treturn p\n\t}\n\n\tswitch {\n\tcase load*10 >= upper*9 && count == 1:\n\t\tp *= 0.25\n\n\tcase load*10 >= upper*9:\n\t\tp *= 0.08\n\n\tcase load*3 >= upper*2:\n\t\tp *= 0.60\n\t}\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\treturn p\n}\n\nfunc (r *candidateRouter) addPenalty(key edgeKey, amount, cap float64) {\n\tnext := r.edgePenalty[key] + amount\n\tif next > cap {\n\t\tnext = cap\n\t}\n\tr.edgePenalty[key] = next\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge == lnwire.MaxMilliSatoshi ||\n\t\t\t\t\tnodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.985\n\t\t\t}\n\t\t\tp = r.localProbability(edge.key, totalLoad, p)\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge == lnwire.MaxMilliSatoshi ||\n\t\t\tcharge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tsum += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tpart := lnwire.MilliSatoshi(int64(total) * weight / sum)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part >= remaining {\n\t\t\tpart = remaining - 1\n\t\t}\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\t// A dense ratio sweep captures unequal public HTLC ceilings and\n\t\t// asymmetric bimodal corridors without first spending attempts on a\n\t\t// halving ladder.\n\t\tfor first := int64(5); first <= 95; first += 5 {\n\t\t\tif first == 50 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{first, 100 - first},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 30, 60}, {10, 60, 30},\n\t\t\t{30, 10, 60}, {30, 60, 10},\n\t\t\t{60, 10, 30}, {60, 30, 10},\n\t\t\t{20, 30, 50}, {50, 30, 20},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 15, 25, 50},\n\t\t\t{50, 25, 15, 10},\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardCap := remainingBudget\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > shard {\n\n\t\t\tshardCap = remainingBudget * shard / remainingAmount\n\n\t\t\tallowance := remainingBudget / 8\n\t\t\tif allowance > 50_000 {\n\t\t\t\tallowance = 50_000\n\t\t\t}\n\t\t\tif shardCap <= remainingBudget-allowance {\n\t\t\t\tshardCap += allowance\n\t\t\t} else {\n\t\t\t\tshardCap = remainingBudget\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tbaseReservations := r.activeReservations()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget, baseReservations,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// If a complete route set cannot be constructed, send a useful shard.\n\t// This remains a fallback; the normal path commits a full joint plan.\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor shard > 0 {\n\t\t\tcap := feeBudget\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi {\n\t\t\t\tcap = feeBudget * shard / total\n\t\t\t\tif cap < 1_000 {\n\t\t\t\t\tcap = 1_000\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, baseReservations, cap)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tif shard <= 10_000 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnext := shard * 2 / 3\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) int {\n\n\tif load <= 0 {\n\t\treturn r.localFails[key]\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\n\tr.localFails[key]++\n\treturn r.localFails[key]\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t\tdelete(r.policyFails, edge.key)\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\t// Attribution may move by one hop. Only the prefix ending two edges\n\t// before the reported edge is certain to have passed.\n\tsafePrefix := failIdx - 1\n\tif safePrefix < 0 {\n\t\tsafePrefix = 0\n\t}\n\tif safePrefix > len(meta.path) {\n\t\tsafePrefix = len(meta.path)\n\t}\n\tfor i := 0; i < safePrefix; i++ {\n\t\tupdateBelief(meta.path[i].key, meta.loads[i], true, now)\n\t}\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tedge := meta.path[failIdx]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcount := r.recordLocalFailure(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\n\t\t\t// A single report steers the next attempt without poisoning the\n\t\t\t// persistent store. Repeated blame corroborates the observation.\n\t\t\tif count >= 2 {\n\t\t\t\tupdateBelief(\n\t\t\t\t\tedge.key, meta.loads[failIdx], false, now,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tr.addPenalty(\n\t\t\t\tedge.key,\n\t\t\t\t350_000+float64(count)*225_000,\n\t\t\t\t2_500_000,\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyFails[edge.key]++\n\t\t\tif r.policyFails[edge.key] >= 2 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\t\t\tr.addPenalty(edge.key, 1_000_000, 8_000_000)\n\n\t\tdefault:\n\t\t\tr.addPenalty(edge.key, 750_000, 2_500_000)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unreadable failure contains no channel evidence. Apply a bounded\n\t// diversity cost only; do not invent a first-hop failure, liquidity\n\t// bound, or fee-budget deduction.\n\tfor _, edge := range meta.path {\n\t\tr.addPenalty(edge.key, 120_000, 900_000)\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 17,
"parent": 1,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(25_000)\n\treusedEdgeCostMsat = float64(950_000)\n\textraPartCostMsat = float64(55_000)\n\talternativeEdgeCost = float64(850_000)\n\n\tmaxSearchHops = 18\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 64\n\tmaxPlannedParts = 12\n\n\tplannerBeamWidth = 5\n\tplannerAlternatives = 3\n\talternativeSearches = 6\n\tmaxEdgePenaltyMsat = float64(12_000_000)\n\tunknownRoutePenalty = float64(180_000)\n\ttemporaryDirectCost = float64(1_800_000)\n\ttemporaryAdjacentCost = float64(300_000)\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBasis := int64(amt) + outbound\n\tif inboundBasis < 0 {\n\t\treturn 0\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tpenalty map[edgeKey]float64\n\n\tliquidityVotes map[edgeKey]float64\n\tsuspectLoad map[edgeKey]lnwire.MilliSatoshi\n\tpolicyVotes map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tattempts int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif spec == nil {\n\t\treturn nil, errors.New(\"nil payment specification\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tpenalty: make(map[edgeKey]float64),\n\t\tliquidityVotes: make(map[edgeKey]float64),\n\t\tsuspectLoad: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpolicyVotes: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tnow time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.55)\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.35)\n\t}\n\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc cloneCosts(src map[edgeKey]float64) map[edgeKey]float64 {\n\tdst := make(map[edgeKey]float64, len(src))\n\tfor key, cost := range src {\n\t\tdst[key] = cost\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) addPenalty(key edgeKey, amount float64) {\n\tnext := r.penalty[key] + amount\n\tif next > maxEdgePenaltyMsat {\n\t\tnext = maxEdgePenaltyMsat\n\t}\n\tr.penalty[key] = next\n}\n\nfunc (r *candidateRouter) retryCost(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 || load <= 0 {\n\t\treturn 0\n\t}\n\n\tfails := r.localFails[key]\n\tfactor := 0.62\n\tif fails >= 2 {\n\t\tfactor = 0.48\n\t}\n\tif fails >= 4 {\n\t\tfactor = 0.32\n\t}\n\n\tcutoff := float64(upper) * factor\n\tif float64(load) < cutoff {\n\t\treturn 0\n\t}\n\n\tratio := float64(load) / float64(upper)\n\tif ratio > 2 {\n\t\tratio = 2\n\t}\n\n\treturn 1_200_000 + 2_200_000*ratio\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi,\n\textra map[edgeKey]float64) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif upper := r.localUpper[edge.key]; upper > 0 {\n\t\t\t\t\tif totalLoad >= upper {\n\t\t\t\t\t\tp = 0.035\n\t\t\t\t\t} else {\n\t\t\t\t\t\tp = 0.78\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.penalty[edge.key] +\n\t\t\t\textra[edge.key] +\n\t\t\t\tr.retryCost(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn result\n\t}\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 {\n\t\t\treturn result\n\t\t}\n\t\tsum += weight\n\t}\n\tif sum <= 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\ttotal64 := int64(total)\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tq := total64 / sum\n\t\trem := total64 % sum\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tq*weight + rem*weight/sum,\n\t\t)\n\n\t\tminLeft := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLeft {\n\t\t\tpart = remaining - minLeft\n\t\t}\n\t\tif part <= 0 {\n\t\t\treturn make([]lnwire.MilliSatoshi, len(weights))\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc percentAmount(total lnwire.MilliSatoshi,\n\tpercent int64) lnwire.MilliSatoshi {\n\n\tq := int64(total) / 100\n\trem := int64(total) % 100\n\treturn lnwire.MilliSatoshi(q*percent + rem*percent/100)\n}\n\nfunc appendSplit(patterns *[][]lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool, total,\n\tfirst lnwire.MilliSatoshi) {\n\n\tif first <= 0 || first >= total || seen[first] {\n\t\treturn\n\t}\n\tseen[first] = true\n\t*patterns = append(*patterns, []lnwire.MilliSatoshi{\n\t\tfirst, total - first,\n\t})\n}\n\nfunc edgeCeiling(edge *candidateEdge) lnwire.MilliSatoshi {\n\tlimit := edge.capacity\n\tif edge.maxHTLC > 0 && edge.maxHTLC < limit {\n\t\tlimit = edge.maxHTLC\n\t}\n\treturn limit\n}\n\nfunc (r *candidateRouter) allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\tif parts == 1 {\n\t\treturn [][]lnwire.MilliSatoshi{{total}}\n\t}\n\n\tif parts == 2 {\n\t\tpatterns := make([][]lnwire.MilliSatoshi, 0, 64)\n\t\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\t\tappendSplit(&patterns, seen, total, total/2)\n\n\t\tfor p := int64(5); p <= 95; p += 5 {\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tpercentAmount(total, p),\n\t\t\t)\n\t\t}\n\t\tfor _, p := range []int64{\n\t\t\t1, 2, 3, 47, 48, 49, 51, 52, 53, 97, 98, 99,\n\t\t} {\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tpercentAmount(total, p),\n\t\t\t)\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\t\tlimit := edgeCeiling(edge)\n\t\t\tappendSplit(&patterns, seen, total, limit)\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tlimit*95/100,\n\t\t\t)\n\t\t\tif limit < total {\n\t\t\t\tappendSplit(\n\t\t\t\t\t&patterns, seen, total,\n\t\t\t\t\ttotal-limit,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tfor _, edge := range r.edgeLookup {\n\t\t\tif edge.from != r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlimit := edgeCeiling(edge)\n\t\t\tif balance, ok := r.localBalances[edge.chanID];\n\t\t\t\tok && balance < limit {\n\n\t\t\t\tlimit = balance\n\t\t\t}\n\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tlimit*97/100,\n\t\t\t)\n\t\t\tif limit < total {\n\t\t\t\tappendSplit(\n\t\t\t\t\t&patterns, seen, total,\n\t\t\t\t\ttotal-limit*97/100,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tfor _, upper := range r.localUpper {\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tupper*60/100,\n\t\t\t)\n\t\t\tappendSplit(\n\t\t\t\t&patterns, seen, total,\n\t\t\t\tupper*45/100,\n\t\t\t)\n\t\t}\n\n\t\treturn patterns\n\t}\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tcentered := make([]int64, parts)\n\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t\tcentered[i] = 2\n\t\tif i == parts/2 {\n\t\t\tcentered[i] = int64(parts + 1)\n\t\t}\n\t}\n\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, descending),\n\t\tweightedAllocation(total, centered),\n\t)\n\n\tif parts == 3 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{15, 35, 50},\n\t\t\t{50, 35, 15},\n\t\t\t{20, 60, 20},\n\t\t\t{45, 10, 45},\n\t\t\t{10, 20, 70},\n\t\t\t{70, 20, 10},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc samePath(a, b *routeMeta) bool {\n\tif a == nil || b == nil || len(a.path) != len(b.path) {\n\t\treturn false\n\t}\n\tfor i := range a.path {\n\t\tif a.path[i].key != b.path[i].key {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc pathExtraCost(meta *routeMeta,\n\textra map[edgeKey]float64) float64 {\n\n\tvar total float64\n\tfor _, edge := range meta.path {\n\t\ttotal += extra[edge.key]\n\t}\n\treturn total\n}\n\ntype partialPlan struct {\n\troutes []*plannedRoute\n\treservations map[edgeKey]lnwire.MilliSatoshi\n\tfeeRemaining lnwire.MilliSatoshi\n\tscore float64\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\tfrontier := []*partialPlan{{\n\t\treservations: cloneReservations(r.reserved),\n\t\tfeeRemaining: feeBudget,\n\t}}\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tnextFrontier := make([]*partialPlan, 0,\n\t\t\tlen(frontier)*plannerAlternatives)\n\n\t\tfor _, partial := range frontier {\n\t\t\textra := make(map[edgeKey]float64)\n\t\t\talternatives := make([]*plannedRoute, 0,\n\t\t\t\tplannerAlternatives)\n\n\t\t\tfor tries := 0; tries < alternativeSearches &&\n\t\t\t\tlen(alternatives) < plannerAlternatives; tries++ {\n\n\t\t\t\tpr, err := r.findRoute(\n\t\t\t\t\tshard, partial.reservations,\n\t\t\t\t\tpartial.feeRemaining, extra,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tduplicate := false\n\t\t\t\tfor _, old := range alternatives {\n\t\t\t\t\tif samePath(old.meta, pr.meta) {\n\t\t\t\t\t\tduplicate = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\textraCost := pathExtraCost(pr.meta, extra)\n\t\t\t\tfor _, edge := range pr.meta.path {\n\t\t\t\t\textra[edge.key] += alternativeEdgeCost\n\t\t\t\t}\n\n\t\t\t\tif duplicate {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tpr.score -= extraCost\n\t\t\t\talternatives = append(alternatives, pr)\n\t\t\t}\n\n\t\t\tfor _, pr := range alternatives {\n\t\t\t\tif partial.feeRemaining !=\n\t\t\t\t\tlnwire.MaxMilliSatoshi &&\n\t\t\t\t\tpr.meta.fee > partial.feeRemaining {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tremaining := partial.feeRemaining\n\t\t\t\tif remaining != lnwire.MaxMilliSatoshi {\n\t\t\t\t\tremaining -= pr.meta.fee\n\t\t\t\t}\n\n\t\t\t\treservations := cloneReservations(\n\t\t\t\t\tpartial.reservations,\n\t\t\t\t)\n\t\t\t\taddReservationSet(reservations, pr.meta)\n\n\t\t\t\troutes := make([]*plannedRoute,\n\t\t\t\t\tlen(partial.routes), len(partial.routes)+1)\n\t\t\t\tcopy(routes, partial.routes)\n\t\t\t\troutes = append(routes, pr)\n\n\t\t\t\tnextFrontier = append(nextFrontier, &partialPlan{\n\t\t\t\t\troutes: routes,\n\t\t\t\t\treservations: reservations,\n\t\t\t\t\tfeeRemaining: remaining,\n\t\t\t\t\tscore: partial.score + pr.score,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(nextFrontier) == 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tsort.Slice(nextFrontier, func(i, j int) bool {\n\t\t\treturn nextFrontier[i].score < nextFrontier[j].score\n\t\t})\n\t\tif len(nextFrontier) > plannerBeamWidth {\n\t\t\tnextFrontier = nextFrontier[:plannerBeamWidth]\n\t\t}\n\t\tfrontier = nextFrontier\n\t}\n\n\tbest := frontier[0]\n\tbest.score += float64(len(best.routes)-1) * extraPartCostMsat\n\treturn best.routes, best.score, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tmaxParts := int(slots)\n\tif maxParts > maxPlannedParts {\n\t\tmaxParts = maxPlannedParts\n\t}\n\n\tpartCounts := make([]int, 0, maxParts)\n\tfor parts := 1; parts <= maxParts; parts++ {\n\t\tpartCounts = append(partCounts, parts)\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range partCounts {\n\t\tfor _, allocation := range r.allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor tries := 0; tries < 10 && shard > 0; tries++ {\n\t\t\tpr, err := r.findRoute(\n\t\t\t\tshard, r.reserved, feeBudget, nil,\n\t\t\t)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tnext := shard * 4 / 5\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) noteUnknownFailure(meta *routeMeta) {\n\tif meta == nil || len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := unknownRoutePenalty / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.addPenalty(edge.key, perEdge)\n\t}\n}\n\nfunc (r *candidateRouter) noteLiquidityFailure(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.30\n\t\tcost := temporaryAdjacentCost\n\t\tif i == failIdx {\n\t\t\tweight = 0.65\n\t\t\tcost = temporaryDirectCost\n\t\t}\n\n\t\tr.liquidityVotes[key] += weight\n\t\tload := meta.loads[i]\n\t\tif old := r.suspectLoad[key]; old == 0 || load < old {\n\t\t\tr.suspectLoad[key] = load\n\t\t}\n\t\tr.addPenalty(key, cost)\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.liquidityVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx < 0 || best < 1.15 || best-second < 0.25 {\n\t\treturn\n\t}\n\n\tkey := meta.path[bestIdx].key\n\tload := r.suspectLoad[key]\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupdateBelief(key, load, false, now)\n\tr.recordLocalFailure(key, load)\n\tr.addPenalty(\n\t\tkey, 350_000+float64(r.localFails[key])*175_000,\n\t)\n\n\tlo := bestIdx - 1\n\tif lo < 0 {\n\t\tlo = 0\n\t}\n\thi := bestIdx + 1\n\tif hi >= len(meta.path) {\n\t\thi = len(meta.path) - 1\n\t}\n\tfor i := lo; i <= hi; i++ {\n\t\tk := meta.path[i].key\n\t\tr.liquidityVotes[k] *= 0.15\n\t}\n}\n\nfunc (r *candidateRouter) notePolicyFailure(meta *routeMeta,\n\tfailIdx int) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.25\n\t\tcost := float64(650_000)\n\t\tif i == failIdx {\n\t\t\tweight = 0.75\n\t\t\tcost = 3_000_000\n\t\t}\n\n\t\tr.policyVotes[key] += weight\n\t\tr.addPenalty(key, cost)\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.policyVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx >= 0 && best >= 1.40 && best-second >= 0.45 {\n\t\tkey := meta.path[bestIdx].key\n\t\tr.blocked[key] = true\n\t\tr.addPenalty(key, 8_000_000)\n\t\tr.policyVotes[key] = 0\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.penalty[edge.key] *= 0.20\n\t\t\tr.liquidityVotes[edge.key] *= 0.20\n\t\t\tr.policyVotes[edge.key] *= 0.20\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\t\tr.noteLiquidityFailure(meta, failIdx, now)\n\t\t} else {\n\t\t\tr.noteUnknownFailure(meta)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\t\tr.notePolicyFailure(meta, failIdx)\n\t\t} else {\n\t\t\tr.noteUnknownFailure(meta)\n\t\t}\n\n\tdefault:\n\t\tr.noteUnknownFailure(meta)\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 18,
"parent": 0,
"score": 0.1257,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(8_000)\n\treusedEdgeCostMsat = float64(1_200_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 80\n\tmaxConsecutiveMisses = 52\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tsum += weight\n\t\t}\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 || sum == 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif remaining > minLater && part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 {\n\t\treturn nil\n\t}\n\tif parts == 1 {\n\t\treturn [][]lnwire.MilliSatoshi{{total}}\n\t}\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 2*parts+4)\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 10\n\t}\n\tpatterns = append(patterns, weightedAllocation(total, equal))\n\n\tif parts == 2 {\n\t\tfor first := int64(5); first <= 95; first += 5 {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, []int64{\n\t\t\t\t\tfirst, 100 - first,\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\t\treturn patterns\n\t}\n\n\tfor favored := 0; favored < parts; favored++ {\n\t\tmoderate := make([]int64, parts)\n\t\tstrong := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\tmoderate[i] = 10\n\t\t\tstrong[i] = 10\n\t\t}\n\t\tmoderate[favored] = 20\n\t\tstrong[favored] = 40\n\n\t\tpatterns = append(\n\t\t\tpatterns,\n\t\t\tweightedAllocation(total, moderate),\n\t\t\tweightedAllocation(total, strong),\n\t\t)\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t}\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, descending),\n\t)\n\n\treturn patterns\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, remainingBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// MaxParts limits concurrent shards, not the lifetime number of\n\t// non-atomic shards. Even with one available slot, search for a useful\n\t// smaller shard when the full remaining amount violates a public HTLC\n\t// ceiling or a learned liquidity bound.\n\tdivisor := lnwire.MilliSatoshi(slots)\n\tif divisor < 1 {\n\t\tdivisor = 1\n\t}\n\n\tshard := (total + divisor - 1) / divisor\n\tif shard >= total {\n\t\tshard = total * 3 / 4\n\t}\n\tif shard <= 0 {\n\t\tshard = total\n\t}\n\n\tfor shard > 0 {\n\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= 1_000 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 3 / 4\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := float64(300_000) / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\t// A source outside the route is explicitly unattributed. Do not turn it\n\t// into a false channel bound, even when its remaining code resembles a\n\t// liquidity failure.\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\t// A shifted attribution can name the destination, whose only\n\t\t// adjacent candidate is the final channel.\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\t// With a possible one-hop attribution shift, only the prefix ending two\n\t// edges before the reported failure is safe to treat as having passed.\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.20, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\n\t// Spread a small amount of route-choice pressure to adjacent candidates,\n\t// but only the repeatedly reported edge may create a bound.\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 70_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 70_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t375_000 + float64(count)*125_000\n\n\t\tif count >= 2 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.45, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + float64(count)*750_000\n\n\t\t// Policy errors are deterministic, but attribution is not. Require\n\t\t// corroboration before permanently removing the reported edge.\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 700_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 19,
"parent": 2,
"score": 0.1572,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(35_000)\n\n\thopCostMsat = float64(10_000)\n\treusedEdgeCostMsat = float64(1_500_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 60\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 64\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\theld []*routeMeta\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() &&\n\t\t(now.Before(b.updated) || now.Sub(b.updated) > 2*time.Hour) {\n\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) rebuildReservations() {\n\tr.reserved = make(map[edgeKey]lnwire.MilliSatoshi)\n\tfor _, meta := range r.held {\n\t\taddReservationSet(r.reserved, meta)\n\t}\n}\n\nfunc (r *candidateRouter) syncReservations(inFlight uint32) {\n\tif int(inFlight) >= len(r.held) {\n\t\treturn\n\t}\n\n\tremove := len(r.held) - int(inFlight)\n\tr.held = r.held[remove:]\n\tr.rebuildReservations()\n\tr.plan = nil\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(r.remainingFeeBudget())\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.85\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlearnedPenalty := float64(0)\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tif p > 0.018 {\n\t\t\t\t\tp = 0.018\n\t\t\t\t}\n\t\t\t\tlearnedPenalty += 1_200_000\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key] +\n\t\t\t\tlearnedPenalty\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tsum += weight\n\t\t}\n\t}\n\n\tif len(weights) == 0 || sum == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tfloat64(remaining) *\n\t\t\t\tfloat64(weight) / float64(remainingWeight),\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc equalWeights(parts int) []int64 {\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\treturn weights\n}\n\nfunc allocationCandidates(total lnwire.MilliSatoshi,\n\tslots uint32) [][]lnwire.MilliSatoshi {\n\n\tmaxParts := int(slots)\n\tif maxParts <= 0 {\n\t\treturn nil\n\t}\n\n\tresult := make([][]lnwire.MilliSatoshi, 0, 12)\n\tresult = append(result, []lnwire.MilliSatoshi{total})\n\n\tif maxParts >= 2 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{1, 1},\n\t\t\t{13, 7},\n\t\t\t{7, 13},\n\t\t} {\n\t\t\tif allocation := weightedAllocation(total, weights);\n\t\t\t\tallocation != nil {\n\n\t\t\t\tresult = append(result, allocation)\n\t\t\t}\n\t\t}\n\t}\n\n\tif maxParts >= 3 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{1, 1, 1},\n\t\t\t{2, 1, 1},\n\t\t\t{1, 1, 2},\n\t\t} {\n\t\t\tif allocation := weightedAllocation(total, weights);\n\t\t\t\tallocation != nil {\n\n\t\t\t\tresult = append(result, allocation)\n\t\t\t}\n\t\t}\n\t}\n\n\tif maxParts >= 4 {\n\t\tif allocation := weightedAllocation(\n\t\t\ttotal, equalWeights(4),\n\t\t); allocation != nil {\n\n\t\t\tresult = append(result, allocation)\n\t\t}\n\t}\n\n\tif maxParts >= 5 {\n\t\tif allocation := weightedAllocation(\n\t\t\ttotal, equalWeights(maxParts),\n\t\t); allocation != nil {\n\n\t\t\tresult = append(result, allocation)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc shardFeeCap(budget, shard, remaining lnwire.MilliSatoshi,\n\tpieces int) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi || pieces <= 1 {\n\t\treturn budget\n\t}\n\tif budget <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\n\tshare := float64(budget) * float64(shard) / float64(remaining)\n\tslack := float64(budget) / float64(2*pieces)\n\tcapFloat := share + slack\n\tif capFloat > float64(budget) {\n\t\tcapFloat = float64(budget)\n\t}\n\tif capFloat < 0 {\n\t\treturn 0\n\t}\n\treturn lnwire.MilliSatoshi(capFloat)\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\tfor _, shard := range allocation {\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor i, shard := range allocation {\n\t\tif shard <= 0 || remainingAmount < shard {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpieces := len(allocation) - i\n\t\tcap := shardFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount, pieces,\n\t\t)\n\n\t\tpr, err := r.findRoute(shard, reservations, cap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, allocation := range allocationCandidates(total, slots) {\n\t\tplan, score, ok := r.tryAllocation(allocation, feeBudget)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tr.syncReservations(inFlightHtlcs)\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := float64(120_000) / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.72, now,\n\t\t\t)\n\t\t\tr.edgePenalty[edge.key] *= 0.20\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.held = append(r.held, meta)\n\t\taddReservationSet(r.reserved, meta)\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.18, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 90_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 90_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t450_000 + float64(count)*150_000\n\n\t\tweight := 0.10\n\t\tif count >= 2 {\n\t\t\tweight = 0.38\n\t\t}\n\t\tupdateBelief(\n\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t)\n\n\t\tif count >= 2 {\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_700_000 + float64(count)*800_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 650_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 20,
"parent": 2,
"score": 0.0785,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(8_000)\n\treusedEdgeCostMsat = float64(1_200_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 30\n\tmaxLabelsPerNode = 6\n\tmaxPlanSearches = 16\n\tmaxPlannedParts = 12\n\tmaxRouterAttempts = 36\n\tmaxConsecutiveMisses = 24\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, parts)\n\tbase := total / lnwire.MilliSatoshi(parts)\n\tremainder := total % lnwire.MilliSatoshi(parts)\n\n\tfor i := range result {\n\t\tresult[i] = base\n\t\tif lnwire.MilliSatoshi(i) < remainder {\n\t\t\tresult[i]++\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc twoPartAllocation(total lnwire.MilliSatoshi,\n\tfirstPercent int64) []lnwire.MilliSatoshi {\n\n\tif total < 2 || firstPercent <= 0 || firstPercent >= 100 {\n\t\treturn nil\n\t}\n\n\tfirst := lnwire.MilliSatoshi(\n\t\tfloat64(total) * float64(firstPercent) / 100,\n\t)\n\tif first <= 0 {\n\t\tfirst = 1\n\t}\n\tif first >= total {\n\t\tfirst = total - 1\n\t}\n\n\treturn []lnwire.MilliSatoshi{first, total - first}\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc proportionalFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || shard <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\tif shard >= remaining {\n\t\treturn budget\n\t}\n\n\tcap := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif cap <= 0 && budget > 0 {\n\t\tcap = 1\n\t}\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\treturn cap\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc (r *candidateRouter) allocationAllowed(\n\tallocation []lnwire.MilliSatoshi) bool {\n\n\tif r.consecutiveMisses < 2 || r.nextShardCap <= 0 {\n\t\treturn true\n\t}\n\n\tfor _, shard := range allocation {\n\t\tif shard > r.nextShardCap {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches ||\n\t\t!r.allocationAllowed(allocation) {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tfeeCap := proportionalFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\t*searches++\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 && r.spec.FeeLimitMsat != lnwire.MaxMilliSatoshi {\n\t\tif total <= 0 {\n\t\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t\t}\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) bool {\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t\treturn ok\n\t}\n\n\tfullOK := consider([]lnwire.MilliSatoshi{total})\n\ttwoOK := false\n\n\tif partLimit >= 2 && searches < maxPlanSearches {\n\t\ttwoOK = consider(equalAllocation(total, 2))\n\t}\n\n\tif twoOK {\n\t\tif searches+2 <= maxPlanSearches {\n\t\t\tconsider(twoPartAllocation(total, 65))\n\t\t}\n\t\tif searches+2 <= maxPlanSearches {\n\t\t\tconsider(twoPartAllocation(total, 35))\n\t\t}\n\t\tif partLimit >= 3 && searches+3 <= maxPlanSearches {\n\t\t\tconsider(equalAllocation(total, 3))\n\t\t}\n\t} else {\n\t\tfor parts := 3; parts <= partLimit; parts++ {\n\t\t\tif searches+parts > maxPlanSearches {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif consider(equalAllocation(total, parts)) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif fullOK {\n\t\treturn best, nil\n\t}\n\n\tshard := total * 3 / 4\n\tif r.nextShardCap > 0 && shard > r.nextShardCap {\n\t\tshard = r.nextShardCap\n\t}\n\tif shard >= total {\n\t\tshard = total - 1\n\t}\n\tif shard <= 0 {\n\t\treturn nil, errors.New(\"no decreasing shard available\")\n\t}\n\n\tfor shard > 0 && searches < maxPlanSearches {\n\t\tsearches++\n\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= 1_000 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 3 / 4\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tif amount > lnwire.MaxMilliSatoshi-r.reserved[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(amt lnwire.MilliSatoshi) {\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tcap := amt * 3 / 4\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\tr.nextShardCap = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.20, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 60_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 60_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t350_000 + float64(count)*125_000\n\n\t\tif count >= 2 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.45, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + float64(count)*750_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.edgePenalty[edge.key] += 600_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 21,
"parent": 3,
"score": 0.2723,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(20_000)\n\n\thopCostMsat = float64(15_000)\n\textraPartCostMsat = float64(45_000)\n\tbaseReuseCostMsat = float64(260_000)\n\tloadReuseCostMsat = float64(700_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 8\n\tmaxPlanSearches = 112\n\tmaxPlannedParts = 12\n\tmaxRouterAttempts = 36\n\tmaxConsecutiveMisses = 28\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n\tunattributed int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tfailures := r.localFails[key]\n\tif upper <= 0 || failures <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 9 / 10\n\tif failures >= 2 {\n\t\tcutoff = upper * 3 / 4\n\t}\n\tif failures >= 4 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc reusePenalty(edge *candidateEdge,\n\treserved lnwire.MilliSatoshi) float64 {\n\n\tif reserved <= 0 || edge.capacity <= 0 {\n\t\treturn 0\n\t}\n\n\tloadRatio := float64(reserved) / float64(edge.capacity)\n\tif loadRatio > 1 {\n\t\tloadRatio = 1\n\t}\n\n\tbase := baseReuseCostMsat\n\tif edge.from == edge.key.from && edge.from == edge.key.from {\n\t\tbase = baseReuseCostMsat\n\t}\n\n\treturn base + loadReuseCostMsat*loadRatio\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localFails[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusePenalty(edge, reserved)\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.45\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, parts)\n\tbase := total / lnwire.MilliSatoshi(parts)\n\tremainder := total % lnwire.MilliSatoshi(parts)\n\n\tfor i := range result {\n\t\tresult[i] = base\n\t\tif lnwire.MilliSatoshi(i) < remainder {\n\t\t\tresult[i]++\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tparts := len(weights)\n\tif parts == 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, parts)\n\tfor i := range result {\n\t\tresult[i] = 1\n\t}\n\n\tremaining := total - lnwire.MilliSatoshi(parts)\n\tvar assigned lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tshare := lnwire.MilliSatoshi(\n\t\t\tfloat64(remaining) * float64(weight) /\n\t\t\t\tfloat64(weightTotal),\n\t\t)\n\t\tresult[i] += share\n\t\tassigned += share\n\t}\n\n\tleft := remaining - assigned\n\tfor i := 0; left > 0; i = (i + 1) % parts {\n\t\tresult[i]++\n\t\tleft--\n\t}\n\n\treturn result\n}\n\nfunc reverseAllocation(src []lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\tif len(src) == 0 {\n\t\treturn nil\n\t}\n\n\tdst := make([]lnwire.MilliSatoshi, len(src))\n\tfor i := range src {\n\t\tdst[len(src)-1-i] = src[i]\n\t}\n\treturn dst\n}\n\nfunc linearAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tif parts <= 0 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = int64(parts - i)\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc flexibleFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || shard <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\tif shard >= remaining {\n\t\treturn budget\n\t}\n\n\tproportional := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tslack := (budget - proportional) / 4\n\tcap := proportional + slack\n\tif cap <= 0 && budget > 0 {\n\t\tcap = 1\n\t}\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\n\treturn cap\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc (r *candidateRouter) allocationAllowed(\n\tallocation []lnwire.MilliSatoshi) bool {\n\n\tif r.consecutiveMisses < 2 || r.nextShardCap <= 0 {\n\t\treturn true\n\t}\n\n\tfor _, shard := range allocation {\n\t\tif shard > r.nextShardCap {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches ||\n\t\t!r.allocationAllowed(allocation) {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tfeeCap := flexibleFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\t*searches = *searches + 1\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc minimumParts(total,\n\tcap lnwire.MilliSatoshi) int {\n\n\tif cap <= 0 || total <= cap {\n\t\treturn 1\n\t}\n\n\tparts := int(total / cap)\n\tif total%cap != 0 {\n\t\tparts++\n\t}\n\tif parts < 1 {\n\t\tparts = 1\n\t}\n\n\treturn parts\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tminParts := 1\n\tif r.consecutiveMisses >= 2 && r.nextShardCap > 0 {\n\t\tminParts = minimumParts(total, r.nextShardCap)\n\t}\n\tif minParts > partLimit {\n\t\treturn nil, errors.New(\"remaining amount exceeds available parts\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tif len(allocation) == 0 ||\n\t\t\tsearches+len(allocation) > maxPlanSearches {\n\n\t\t\treturn\n\t\t}\n\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor parts := minParts; parts <= partLimit; parts++ {\n\t\tconsider(equalAllocation(total, parts))\n\n\t\tif parts >= 2 && parts <= 5 {\n\t\t\tunequal := linearAllocation(total, parts)\n\t\t\tconsider(unequal)\n\t\t\tconsider(reverseAllocation(unequal))\n\t\t}\n\n\t\tif searches >= maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif amount > lnwire.MaxMilliSatoshi-r.reserved[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(amt lnwire.MilliSatoshi) {\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tcap := amt * 3 / 4\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta,\n\tnow time.Time) {\n\n\tvar riskTotal float64\n\trisks := make([]float64, len(meta.path))\n\n\tfor i, edge := range meta.path {\n\t\tp := channelProbability(\n\t\t\tedge.key, meta.loads[i], edge.capacity, now,\n\t\t)\n\t\trisk := 1 - p\n\t\tif risk < 0.02 {\n\t\t\trisk = 0.02\n\t\t}\n\t\trisks[i] = risk\n\t\triskTotal += risk\n\t}\n\n\tif riskTotal <= 0 {\n\t\treturn\n\t}\n\n\tfor i, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t25_000 + 260_000*risks[i]/riskTotal\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.20\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\tr.unattributed = 0\n\t\tr.nextShardCap = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.unattributed++\n\t\tr.penalizeUnattributed(meta, now)\n\t\tif r.unattributed >= 2 {\n\t\t\tr.lowerFutureShard(meta.delivered)\n\t\t}\n\t\treturn nil\n\t}\n\tr.unattributed = 0\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tsafePrefix := failIdx - 1\n\tfor i := 0; i < safePrefix; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.16, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 140_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 140_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t650_000 + float64(count)*180_000\n\n\t\tif count >= 2 {\n\t\t\tweight := 0.32\n\t\t\tif count >= 4 {\n\t\t\t\tweight = 0.48\n\t\t\t}\n\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_200_000 + float64(count)*600_000\n\n\t\tif count >= 3 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.edgePenalty[edge.key] += 500_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 22,
"parent": 2,
"score": 0.1486,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(8_000)\n\treusedEdgeCostMsat = float64(1_150_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 45\n\tmaxLabelsPerNode = 4\n\tmaxSearchExpansions = 8_000\n\tmaxPlannedParts = 8\n\tmaxInitialSearches = 18\n\tmaxReplanSearches = 9\n\tmaxFallbackSearches = 6\n\tmaxRouterAttempts = 36\n\tmaxConsecutiveMisses = 20\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc proportionalFee(amt, rate lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn (amt/1_000_000)*rate +\n\t\t(amt%1_000_000)*rate/1_000_000\n}\n\nfunc signedProportionalFee(amt, rate int64) int64 {\n\treturn (amt/1_000_000)*rate +\n\t\t(amt%1_000_000)*rate/1_000_000\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + proportionalFee(amt, e.feeRatePPM)\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tif inboundBasis < int64(amt) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tsignedProportionalFee(inboundBasis, int64(in.inboundRate))\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total < outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tpenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tplansBuilt int\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tpenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\tlabels[s.node] = append(kept, other)\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tworst := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tremoved := kept[worst]\n\t\tremoved.active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\tif removed == s {\n\t\t\tlabels[s.node] = kept\n\t\t\treturn false\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\texpansions := 0\n\n\tfor pq.Len() > 0 && expansions < maxSearchExpansions {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\n\t\texpansions++\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.penalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 || total < lnwire.MilliSatoshi(len(weights)) {\n\t\treturn result\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tweightTotal += weight\n\t\t}\n\t}\n\tif weightTotal == 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\tremainingWeight := weightTotal\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || *searches <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\t*searches--\n\t\tpr, err := r.findRoute(shard, reservations, remainingBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc appendUniquePartCount(dst []int, value, limit int) []int {\n\tif value < 1 || value > limit {\n\t\treturn dst\n\t}\n\tfor _, existing := range dst {\n\t\tif existing == value {\n\t\t\treturn dst\n\t\t}\n\t}\n\treturn append(dst, value)\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif lnwire.MilliSatoshi(partLimit) > total {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tsearches := maxReplanSearches\n\tif r.plansBuilt == 0 {\n\t\tsearches = maxInitialSearches\n\t}\n\tr.plansBuilt++\n\n\tcounts := make([]int, 0, 5)\n\tcounts = appendUniquePartCount(counts, 1, partLimit)\n\tcounts = appendUniquePartCount(counts, 2, partLimit)\n\tcounts = appendUniquePartCount(counts, partLimit, partLimit)\n\tcounts = appendUniquePartCount(counts, 3, partLimit)\n\tcounts = appendUniquePartCount(counts, 4, partLimit)\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range counts {\n\t\tallocation := equalAllocation(total, parts)\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t\tif searches <= 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif partLimit >= 2 && searches >= 2 {\n\t\tfor _, weights := range [][]int64{{3, 2}, {2, 3}} {\n\t\t\tallocation := weightedAllocation(total, weights)\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget, &searches,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t\tif searches < 2 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif partLimit >= 3 && searches >= 3 {\n\t\tfor _, weights := range [][]int64{{5, 3, 2}, {2, 3, 5}} {\n\t\t\tallocation := weightedAllocation(total, weights)\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget, &searches,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t\tif searches < 3 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// Fall back to a bounded, strictly decreasing shard ladder. Starting\n\t// with the equal-share amount exploits public HTLC ceilings without\n\t// repeatedly probing the full payment.\n\tshard := (total + lnwire.MilliSatoshi(partLimit) - 1) /\n\t\tlnwire.MilliSatoshi(partLimit)\n\tif shard >= total {\n\t\tshard = total * 3 / 4\n\t}\n\tif shard <= 0 {\n\t\tshard = total\n\t}\n\n\tfor tries := 0; tries < maxFallbackSearches && shard > 0; tries++ {\n\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 2 / 3\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 {\n\t\tqueued := queuedAmount(r.plan)\n\n\t\t// Some runners report only the unassigned remainder, while others\n\t\t// temporarily include unresolved in-flight shards. A queued plan is\n\t\t// still safe in the latter case when it is smaller than amt.\n\t\tif queued > amt || (queued < amt && inFlightHtlcs == 0) {\n\t\t\tr.plan = nil\n\t\t}\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif r.reserved[key] > lnwire.MaxMilliSatoshi-amount {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := float64(280_000) / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.penalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.penalty[edge.key] *= 0.25\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\t// An off-route source explicitly carries no attribution. Preserve the\n\t// useful fact that the route failed, but do not poison a channel bound.\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\t// Attribution may shift by one hop. Only edges strictly before the\n\t// earliest possible failing edge are safe positive observations.\n\tsafePassed := failIdx - 1\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.20, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\tif failIdx > 0 {\n\t\tr.penalty[meta.path[failIdx-1].key] += 65_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.penalty[meta.path[failIdx+1].key] += 65_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\t\tr.penalty[edge.key] +=\n\t\t\t350_000 + float64(count)*125_000\n\n\t\t// A single report may be shifted. Corroboration creates a soft\n\t\t// ceiling, while the nonzero probability floor keeps transiently\n\t\t// reserved corridors eligible later.\n\t\tif count >= 2 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.45, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.penalty[edge.key] +=\n\t\t\t1_400_000 + float64(count)*700_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.penalty[edge.key] += 650_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 23,
"parent": 1,
"score": 0.2782,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(32_000)\n\treusedEdgeCostMsat = float64(1_350_000)\n\textraPartCostMsat = float64(70_000)\n\n\tmaxSearchHops = 32\n\tmaxLabelsPerNode = 10\n\tmaxRouterAttempts = 64\n\tmaxConsecutiveMisses = 56\n\tmaxPlannedParts = 12\n\tmaxEdgePenaltyMsat = float64(14_000_000)\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\ntype beliefSpace struct {\n\tview routing.SimNetworkView\n\tm map[edgeKey]liquidityBelief\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tspaces []*beliefSpace\n}{}\n\nfunc sameNetworkView(a, b routing.SimNetworkView) (same bool) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tsame = false\n\t\t}\n\t}()\n\n\treturn a == b\n}\n\nfunc beliefSpaceLocked(view routing.SimNetworkView) *beliefSpace {\n\tfor _, space := range sharedBeliefs.spaces {\n\t\tif sameNetworkView(space.view, view) {\n\t\t\treturn space\n\t\t}\n\t}\n\n\tspace := &beliefSpace{\n\t\tview: view,\n\t\tm: make(map[edgeKey]liquidityBelief),\n\t}\n\tsharedBeliefs.spaces = append(sharedBeliefs.spaces, space)\n\n\treturn space\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tinboundBasis := int64(amt) + outbound\n\tif inboundBasis < 0 {\n\t\treturn 0\n\t}\n\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityVotes map[edgeKey]float64\n\tsuspectLoad map[edgeKey]lnwire.MilliSatoshi\n\tpolicyVotes map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityVotes: make(map[edgeKey]float64),\n\t\tsuspectLoad: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpolicyVotes: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc (r *candidateRouter) channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tspace := beliefSpaceLocked(r.view)\n\tb, ok := space.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc (r *candidateRouter) updateBelief(key edgeKey,\n\tamt lnwire.MilliSatoshi, success bool, now time.Time) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tspace := beliefSpaceLocked(r.view)\n\tb := space.m[key]\n\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.55)\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t\tb.conf = math.Min(1, b.conf+0.38)\n\t}\n\n\tb.updated = now\n\tspace.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) activeReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tactive := cloneReservations(r.reserved)\n\tfor _, meta := range r.issued {\n\t\taddReservationSet(active, meta)\n\t}\n\n\treturn active\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := r.committedFees\n\tfor _, meta := range r.issued {\n\t\tif meta.fee > lnwire.MaxMilliSatoshi-used {\n\t\t\treturn 0\n\t\t}\n\t\tused += meta.fee\n\t}\n\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.85\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 3 / 4\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper / 2\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) addPenalty(key edgeKey, amount float64) {\n\tnext := r.edgePenalty[key] + amount\n\tif next > maxEdgePenaltyMsat {\n\t\tnext = maxEdgePenaltyMsat\n\t}\n\tr.edgePenalty[key] = next\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reservations[edge.key]\n\t\t\tif totalLoad < over || totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t}\n\n\t\t\tretryPenalty := float64(0)\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tswitch {\n\t\t\t\tcase r.localFails[edge.key] >= 3:\n\t\t\t\t\tp *= 0.10\n\t\t\t\t\tretryPenalty = 650_000\n\t\t\t\tcase r.localFails[edge.key] == 2:\n\t\t\t\t\tp *= 0.18\n\t\t\t\t\tretryPenalty = 400_000\n\t\t\t\tdefault:\n\t\t\t\t\tp *= 0.35\n\t\t\t\t\tretryPenalty = 200_000\n\t\t\t\t}\n\t\t\t}\n\t\t\tif p < 0.005 {\n\t\t\t\tp = 0.005\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key] +\n\t\t\t\tretryPenalty\n\n\t\t\tif reservations[edge.key] > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.45\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn result\n\t}\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 {\n\t\t\treturn result\n\t\t}\n\t\tsum += weight\n\t}\n\tif sum <= 0 {\n\t\treturn result\n\t}\n\n\tremaining := total\n\ttotal64 := int64(total)\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tq := total64 / sum\n\t\trem := total64 % sum\n\t\tpart64 := q*weight + rem*weight/sum\n\t\tpart := lnwire.MilliSatoshi(part64)\n\n\t\tminLeft := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLeft {\n\t\t\tpart = remaining - minLeft\n\t\t}\n\t\tif part <= 0 {\n\t\t\treturn make([]lnwire.MilliSatoshi, len(weights))\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\n\tpatterns := [][]lnwire.MilliSatoshi{\n\t\tweightedAllocation(total, equal),\n\t}\n\n\tswitch parts {\n\tcase 2:\n\t\tfor first := int64(2); first <= 98; first += 2 {\n\t\t\tif first == 50 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{first, 100 - first},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 20, 70}, {10, 30, 60},\n\t\t\t{10, 40, 50}, {20, 20, 60},\n\t\t\t{20, 30, 50}, {20, 40, 40},\n\t\t\t{30, 20, 50}, {30, 30, 40},\n\t\t\t{70, 20, 10}, {60, 30, 10},\n\t\t\t{50, 40, 10}, {60, 20, 20},\n\t\t\t{50, 30, 20}, {40, 40, 20},\n\t\t\t{50, 20, 30}, {40, 30, 30},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tcase 4:\n\t\tfor _, weights := range [][]int64{\n\t\t\t{10, 15, 25, 50},\n\t\t\t{50, 25, 15, 10},\n\t\t\t{10, 20, 30, 40},\n\t\t\t{40, 30, 20, 10},\n\t\t\t{15, 20, 25, 40},\n\t\t\t{40, 25, 20, 15},\n\t\t} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts > 4 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tguardBudget bool) ([]*plannedRoute, float64, bool) {\n\n\treservations := r.activeReservations()\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\tfor _, shard := range allocation {\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor i, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tshardBudget := remainingBudget\n\t\tif guardBudget &&\n\t\t\tremainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\ti < len(allocation)-1 &&\n\t\t\tremainingAmount > 0 {\n\n\t\t\tfraction := float64(shard) / float64(remainingAmount)\n\t\t\tguarded := lnwire.MilliSatoshi(\n\t\t\t\tfloat64(remainingBudget) * fraction * 1.5,\n\t\t\t)\n\t\t\tif guarded < shardBudget {\n\t\t\t\tshardBudget = guarded\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tmaxParts := int(slots)\n\tif maxParts > maxPlannedParts {\n\t\tmaxParts = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(maxParts) {\n\t\tmaxParts = int(total)\n\t}\n\tif maxParts <= 0 {\n\t\treturn nil, errors.New(\"invalid part count\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor parts := 1; parts <= maxParts; parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tguardModes := []bool{false}\n\t\t\tif feeBudget != lnwire.MaxMilliSatoshi && parts > 1 {\n\t\t\t\tguardModes = append(guardModes, true)\n\t\t\t}\n\n\t\t\tfor _, guarded := range guardModes {\n\t\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\t\tallocation, feeBudget, guarded,\n\t\t\t\t)\n\t\t\t\tif ok && score < bestScore {\n\t\t\t\t\tbest = plan\n\t\t\t\t\tbestScore = score\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif slots > 1 {\n\t\treservations := r.activeReservations()\n\t\tshard := (total + lnwire.MilliSatoshi(slots) - 1) /\n\t\t\tlnwire.MilliSatoshi(slots)\n\n\t\tfor tries := 0; tries < 14 && shard > 0; tries++ {\n\t\t\tpr, err := r.findRoute(shard, reservations, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\n\t\t\tnext := shard * 17 / 20\n\t\t\tif next >= shard {\n\t\t\t\tnext = shard - 1\n\t\t\t}\n\t\t\tshard = next\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) refreshObservedLoads(rt *route.Route,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tload := meta.amounts[i] + r.reserved[edge.key]\n\n\t\tfor otherRT, otherMeta := range r.issued {\n\t\t\tif otherRT == rt {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor j, otherEdge := range otherMeta.path {\n\t\t\t\tif otherEdge.key == edge.key {\n\t\t\t\t\tload += otherMeta.amounts[j]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tmeta.loads[i] = load\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes != result.FailureSource {\n\t\t\tcontinue\n\t\t}\n\n\t\tif i == len(rt.Hops)-1 {\n\t\t\treturn len(rt.Hops) - 1\n\t\t}\n\n\t\treturn i + 1\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) noteProvenPrefix(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tlimit := failIdx - 1\n\tif limit < 0 {\n\t\treturn\n\t}\n\n\tfor i := 0; i < limit; i++ {\n\t\tedge := meta.path[i]\n\t\tr.updateBelief(edge.key, meta.loads[i], true, now)\n\t\tr.edgePenalty[edge.key] *= 0.72\n\t}\n}\n\nfunc (r *candidateRouter) noteLiquidityFailure(meta *routeMeta,\n\tfailIdx int, now time.Time) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfor i, edge := range meta.path {\n\t\tr.addPenalty(edge.key, 105_000)\n\t\tif i <= failIdx {\n\t\t\tr.addPenalty(edge.key, 35_000)\n\t\t}\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.14\n\t\tpenalty := float64(190_000)\n\t\tif i == failIdx {\n\t\t\tweight = 1.0\n\t\t\tpenalty = 650_000\n\t\t}\n\n\t\tr.liquidityVotes[key] += weight\n\t\tload := meta.loads[i]\n\t\tif old := r.suspectLoad[key]; old == 0 || load < old {\n\t\t\tr.suspectLoad[key] = load\n\t\t}\n\t\tr.addPenalty(key, penalty)\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.liquidityVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx < 0 || best < 1.65 || best-second < 0.55 {\n\t\treturn\n\t}\n\n\tkey := meta.path[bestIdx].key\n\tload := r.suspectLoad[key]\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tr.updateBelief(key, load, false, now)\n\tr.recordLocalFailure(key, load)\n\tr.addPenalty(\n\t\tkey, 300_000+float64(r.localFails[key])*175_000,\n\t)\n\n\tlo := bestIdx - 1\n\tif lo < 0 {\n\t\tlo = 0\n\t}\n\thi := bestIdx + 1\n\tif hi >= len(meta.path) {\n\t\thi = len(meta.path) - 1\n\t}\n\tfor i := lo; i <= hi; i++ {\n\t\tk := meta.path[i].key\n\t\tr.liquidityVotes[k] *= 0.18\n\t}\n}\n\nfunc (r *candidateRouter) notePolicyFailure(meta *routeMeta,\n\tfailIdx int) {\n\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn\n\t}\n\n\tfor _, edge := range meta.path {\n\t\tr.addPenalty(edge.key, 100_000)\n\t}\n\n\tfirst := failIdx - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := failIdx + 1\n\tif last >= len(meta.path) {\n\t\tlast = len(meta.path) - 1\n\t}\n\n\tfor i := first; i <= last; i++ {\n\t\tkey := meta.path[i].key\n\t\tweight := 0.12\n\t\tpenalty := float64(500_000)\n\t\tif i == failIdx {\n\t\t\tweight = 1.0\n\t\t\tpenalty = 2_400_000\n\t\t}\n\n\t\tr.policyVotes[key] += weight\n\t\tr.addPenalty(key, penalty)\n\t}\n\n\tbestIdx := -1\n\tbest := float64(-1)\n\tsecond := float64(-1)\n\n\tfor i, edge := range meta.path {\n\t\tvote := r.policyVotes[edge.key]\n\t\tif vote > best {\n\t\t\tsecond = best\n\t\t\tbest = vote\n\t\t\tbestIdx = i\n\t\t} else if vote > second {\n\t\t\tsecond = vote\n\t\t}\n\t}\n\n\tif bestIdx >= 0 && best >= 1.75 && best-second >= 0.65 {\n\t\tkey := meta.path[bestIdx].key\n\t\tr.blocked[key] = true\n\t\tr.addPenalty(key, 8_000_000)\n\t\tr.policyVotes[key] = 0\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tif meta == nil {\n\t\tdelete(r.issued, rt)\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tr.refreshObservedLoads(rt, meta)\n\tdelete(r.issued, rt)\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tr.updateBelief(edge.key, meta.loads[i], true, now)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.22\n\t\t\tr.liquidityVotes[edge.key] *= 0.20\n\t\t\tr.policyVotes[edge.key] *= 0.20\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t}\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\tfailIdx := r.failureEdgeIndex(rt, result)\n\n\tif failIdx >= 0 && failIdx < len(meta.path) {\n\t\tr.noteProvenPrefix(meta, failIdx, now)\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.noteLiquidityFailure(meta, failIdx, now)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.notePolicyFailure(meta, failIdx)\n\n\t\tdefault:\n\t\t\tfor _, edge := range meta.path {\n\t\t\t\tr.addPenalty(edge.key, 160_000)\n\t\t\t}\n\t\t\tr.addPenalty(meta.path[failIdx].key, 900_000)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, edge := range meta.path {\n\t\tr.addPenalty(edge.key, 300_000)\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 24,
"parent": 3,
"score": 0.1976,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(40_000)\n\n\thopCostMsat = float64(24_000)\n\treusedEdgeCostMsat = float64(1_350_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 30\n\tmaxLabelsPerNode = 8\n\tmaxPlanSearches = 32\n\tmaxPlannedParts = 12\n\tmaxRouterAttempts = 36\n\tmaxConsecutiveMisses = 24\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tfee := e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n\tif fee < 0 {\n\t\treturn 0\n\t}\n\treturn fee\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]float64\n\tpolicySuspects map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]float64),\n\t\tpolicySuspects: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1200)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.018\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.018 + 0.977*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addAmount(dst *lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt < 0 || *dst > lnwire.MaxMilliSatoshi-amt {\n\t\t*dst = lnwire.MaxMilliSatoshi\n\t\treturn false\n\t}\n\t*dst += amt\n\treturn true\n}\n\nfunc (r *candidateRouter) pendingAmount() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, meta := range r.issued {\n\t\tif !addAmount(&total, meta.delivered) {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) pendingFees() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, meta := range r.issued {\n\t\tif !addAmount(&total, meta.fee) {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) currentReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\n\tfor _, meta := range r.issued {\n\t\tfor i, edge := range meta.path {\n\t\t\tcurrent := result[edge.key]\n\t\t\tif meta.amounts[i] > lnwire.MaxMilliSatoshi-current {\n\t\t\t\tresult[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult[edge.key] = current + meta.amounts[i]\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := r.committedFees\n\tpending := r.pendingFees()\n\tif pending > lnwire.MaxMilliSatoshi-used {\n\t\treturn 0\n\t}\n\tused += pending\n\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap)\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 11 / 20\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 5\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.997\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.72\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.60\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, parts)\n\tbase := total / lnwire.MilliSatoshi(parts)\n\tremainder := total % lnwire.MilliSatoshi(parts)\n\n\tfor i := range result {\n\t\tresult[i] = base\n\t\tif lnwire.MilliSatoshi(i) < remainder {\n\t\t\tresult[i]++\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights ...int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 || total < lnwire.MilliSatoshi(len(weights)) {\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\tif weightTotal <= 0 {\n\t\treturn nil\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tremaining := total\n\n\tfor i := 0; i < len(weights)-1; i++ {\n\t\tpartsLeft := len(weights) - i - 1\n\t\tshard := lnwire.MilliSatoshi(\n\t\t\tint64(total) * weights[i] / weightTotal,\n\t\t)\n\t\tif shard <= 0 {\n\t\t\tshard = 1\n\t\t}\n\n\t\tmaxShard := remaining - lnwire.MilliSatoshi(partsLeft)\n\t\tif shard > maxShard {\n\t\t\tshard = maxShard\n\t\t}\n\n\t\tresult[i] = shard\n\t\tremaining -= shard\n\t}\n\n\tresult[len(result)-1] = remaining\n\tif result[len(result)-1] <= 0 {\n\t\treturn nil\n\t}\n\n\treturn result\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tcurrent := dst[edge.key]\n\t\tif meta.amounts[i] > lnwire.MaxMilliSatoshi-current {\n\t\t\tdst[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\tcontinue\n\t\t}\n\t\tdst[edge.key] = current + meta.amounts[i]\n\t}\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := r.currentReservations()\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\t*searches++\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, remainingBudget,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc ceilParts(total, cap lnwire.MilliSatoshi) int {\n\tif total <= 0 || cap <= 0 {\n\t\treturn 0\n\t}\n\tparts := total / cap\n\tif total%cap != 0 {\n\t\tparts++\n\t}\n\tif parts > lnwire.MilliSatoshi(maxPlannedParts) {\n\t\treturn maxPlannedParts + 1\n\t}\n\treturn int(parts)\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tif len(allocation) == 0 || len(allocation) > partLimit {\n\t\t\treturn\n\t\t}\n\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif r.nextShardCap > 0 {\n\t\tparts := ceilParts(total, r.nextShardCap)\n\t\tif parts >= 2 && parts <= partLimit {\n\t\t\tconsider(equalAllocation(total, parts))\n\t\t}\n\t}\n\n\tconsider([]lnwire.MilliSatoshi{total})\n\n\tif partLimit >= 2 {\n\t\tconsider(equalAllocation(total, 2))\n\t\tconsider(weightedAllocation(total, 60, 40))\n\t\tconsider(weightedAllocation(total, 70, 30))\n\t}\n\n\tif partLimit >= 3 {\n\t\tconsider(equalAllocation(total, 3))\n\t\tconsider(weightedAllocation(total, 50, 30, 20))\n\t}\n\n\tif partLimit >= 4 {\n\t\tconsider(equalAllocation(total, 4))\n\t\tconsider(weightedAllocation(total, 40, 30, 20, 10))\n\t}\n\n\tfor parts := 5; parts <= partLimit; parts++ {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\t\tconsider(equalAllocation(total, parts))\n\t\tif len(best) > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tshard := total\n\tif r.nextShardCap > 0 && shard > r.nextShardCap {\n\t\tshard = r.nextShardCap\n\t} else if shard > 1 {\n\t\tshard = shard * 11 / 20\n\t}\n\n\tfor shard > 0 && searches < maxPlanSearches {\n\t\tsearches++\n\t\tpr, err := r.findRoute(\n\t\t\tshard, r.currentReservations(), feeBudget,\n\t\t)\n\t\tif err == nil {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\n\t\tif shard <= 1_000 {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := shard * 11 / 20\n\t\tif next >= shard {\n\t\t\tnext = shard - 1\n\t\t}\n\t\tshard = next\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tpending := r.pendingAmount()\n\n\tif len(r.plan) > 0 {\n\t\tqueued := queuedAmount(r.plan)\n\t\texpected := queued\n\n\t\tif pending <= lnwire.MaxMilliSatoshi-expected {\n\t\t\texpected += pending\n\t\t} else {\n\t\t\texpected = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tif expected != amt && queued != amt {\n\t\t\tr.plan = nil\n\t\t}\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tneeded := amt\n\t\tif pending > 0 {\n\t\t\tif pending >= amt {\n\t\t\t\treturn nil, errors.New(\"remaining amount already in flight\")\n\t\t\t}\n\t\t\tneeded = amt - pending\n\t\t}\n\n\t\tplan, err := r.makePlan(needed, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tif amount > lnwire.MaxMilliSatoshi-r.reserved[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(amt lnwire.MilliSatoshi) {\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tcap := amt * 11 / 20\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\ntype blamedEdge struct {\n\tindex int\n\tweight float64\n}\n\nfunc failureCandidates(reported, pathLen int) []blamedEdge {\n\tif pathLen <= 0 || reported < 0 {\n\t\treturn nil\n\t}\n\n\tif reported >= pathLen {\n\t\treturn []blamedEdge{{\n\t\t\tindex: pathLen - 1,\n\t\t\tweight: 0.35,\n\t\t}}\n\t}\n\n\tresult := []blamedEdge{{\n\t\tindex: reported,\n\t\tweight: 0.70,\n\t}}\n\n\tif reported > 0 {\n\t\tresult = append(result, blamedEdge{\n\t\t\tindex: reported - 1,\n\t\t\tweight: 0.15,\n\t\t})\n\t}\n\tif reported+1 < pathLen {\n\t\tresult = append(result, blamedEdge{\n\t\t\tindex: reported + 1,\n\t\t\tweight: 0.15,\n\t\t})\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) penalizeFailedRoute(meta *routeMeta,\n\tpenalty float64) {\n\n\tif len(meta.path) == 0 || penalty <= 0 {\n\t\treturn\n\t}\n\n\tperEdge := penalty / math.Sqrt(float64(len(meta.path)))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.55, now,\n\t\t\t)\n\n\t\t\tcurrent := r.reserved[edge.key]\n\t\t\tif meta.amounts[i] > lnwire.MaxMilliSatoshi-current {\n\t\t\t\tr.reserved[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\t} else {\n\t\t\t\tr.reserved[edge.key] = current + meta.amounts[i]\n\t\t\t}\n\n\t\t\tr.edgePenalty[edge.key] *= 0.20\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\tr.nextShardCap = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\tr.penalizeFailedRoute(meta, 180_000)\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.penalizeFailedRoute(meta, 360_000)\n\t\treturn nil\n\t}\n\n\tcandidates := failureCandidates(reportedIdx, len(meta.path))\n\tif len(candidates) == 0 {\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tsafePrefix := reportedIdx - 1\n\tif safePrefix > len(meta.path) {\n\t\tsafePrefix = len(meta.path)\n\t}\n\tfor i := 0; i < safePrefix; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.08, now,\n\t\t)\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\n\t\tfor _, candidate := range candidates {\n\t\t\tedge := meta.path[candidate.index]\n\t\t\tr.liquiditySuspects[edge.key] += candidate.weight\n\t\t\tsuspicion := r.liquiditySuspects[edge.key]\n\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\tcandidate.weight *\n\t\t\t\t\t(1_650_000 + suspicion*240_000)\n\n\t\t\tif suspicion >= 1.05 {\n\t\t\t\tweight := 0.10 + 0.16*suspicion\n\t\t\t\tif weight > 0.48 {\n\t\t\t\t\tweight = 0.48\n\t\t\t\t}\n\t\t\t\tupdateBelief(\n\t\t\t\t\tedge.key, meta.loads[candidate.index],\n\t\t\t\t\tfalse, weight, now,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif suspicion >= 1.45 {\n\t\t\t\tr.recordLocalFailure(\n\t\t\t\t\tedge.key, meta.loads[candidate.index],\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tfor _, candidate := range candidates {\n\t\t\tedge := meta.path[candidate.index]\n\t\t\tr.policySuspects[edge.key] += candidate.weight\n\t\t\tsuspicion := r.policySuspects[edge.key]\n\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\tcandidate.weight *\n\t\t\t\t\t(2_500_000 + suspicion*900_000)\n\n\t\t\tif suspicion >= 1.45 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tfor _, candidate := range candidates {\n\t\t\tedge := meta.path[candidate.index]\n\t\t\tr.edgePenalty[edge.key] +=\n\t\t\t\tcandidate.weight * 900_000\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 25,
"parent": 3,
"score": 0.077,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(20_000)\n\n\thopCostMsat = float64(30_000)\n\treusedEdgeCostMsat = float64(1_800_000)\n\textraPartCostMsat = float64(25_000)\n\n\tmaxSearchHops = 26\n\tmaxLabelsPerNode = 10\n\tmaxPlanSearches = 120\n\tmaxPlannedParts = 16\n\tmaxRouterAttempts = 36\n\tmaxConsecutiveMisses = 24\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tif int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpendingReserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpendingReserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief,\n\tnow time.Time) float64 {\n\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc addAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\told := dst[key]\n\tif amt > lnwire.MaxMilliSatoshi-old {\n\t\tdst[key] = lnwire.MaxMilliSatoshi\n\t\treturn\n\t}\n\n\tdst[key] = old + amt\n}\n\nfunc subtractAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\told := dst[key]\n\tif amt >= old {\n\t\tdelete(dst, key)\n\t\treturn\n\t}\n\n\tdst[key] = old - amt\n}\n\nfunc (r *candidateRouter) effectiveReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\tfor key, amt := range r.pendingReserved {\n\t\taddAmount(result, key, amt)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := r.committedFees\n\tif r.pendingFees > lnwire.MaxMilliSatoshi-used {\n\t\tused = lnwire.MaxMilliSatoshi\n\t} else {\n\t\tused += r.pendingFees\n\t}\n\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tvar cutoff lnwire.MilliSatoshi\n\tswitch {\n\tcase r.localFails[key] >= 3:\n\t\tcutoff = upper * 55 / 100\n\tcase r.localFails[key] == 2:\n\t\tcutoff = upper * 70 / 100\n\tdefault:\n\t\tcutoff = upper * 85 / 100\n\t}\n\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) dynamicPenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif upper > 0 && load < upper*3/4 {\n\t\tpenalty *= 0.12\n\t}\n\n\treturn penalty\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.dynamicPenalty(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tvar allocated lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tshard := total * lnwire.MilliSatoshi(weight) /\n\t\t\tlnwire.MilliSatoshi(weightTotal)\n\t\tif shard <= 0 {\n\t\t\tshard = 1\n\t\t}\n\n\t\tif i == len(weights)-1 {\n\t\t\tshard = total - allocated\n\t\t}\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-allocated {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tresult[i] = shard\n\t\tallocated += shard\n\t}\n\n\tif allocated != total {\n\t\treturn nil\n\t}\n\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tif parts <= 0 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc heavyAllocation(total lnwire.MilliSatoshi,\n\tparts int, heavyFirst bool) []lnwire.MilliSatoshi {\n\n\tif parts < 2 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\n\tif heavyFirst {\n\t\tweights[0] = 2\n\t} else {\n\t\tweights[len(weights)-1] = 2\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc rampAllocation(total lnwire.MilliSatoshi,\n\tparts int, descending bool) []lnwire.MilliSatoshi {\n\n\tif parts < 3 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tif descending {\n\t\t\tweights[i] = int64(parts - i)\n\t\t} else {\n\t\t\tweights[i] = int64(i + 1)\n\t\t}\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\taddAmount(dst, edge.key, meta.amounts[i])\n\t}\n}\n\nfunc planningFeeCap(budget, shard, remaining lnwire.MilliSatoshi,\n\tpartsLeft int) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || shard <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\tif partsLeft <= 1 || shard >= remaining {\n\t\treturn budget\n\t}\n\n\tshare := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif share <= 0 && budget > 0 {\n\t\tshare = 1\n\t}\n\n\tslack := (budget - share) / 4\n\tif slack > lnwire.MaxMilliSatoshi-share {\n\t\treturn budget\n\t}\n\n\tcap := share + slack\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\n\treturn cap\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\n\treturn total, true\n}\n\nfunc allocationAllowed(allocation []lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi) bool {\n\n\tif shardCap <= 0 {\n\t\treturn true\n\t}\n\n\tfor _, shard := range allocation {\n\t\tif shard > shardCap {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches ||\n\t\t!allocationAllowed(allocation, shardCap) {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor i, shard := range allocation {\n\t\tfeeCap := planningFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t\tlen(allocation)-i,\n\t\t)\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc appendPartCount(dst []int, seen map[int]bool,\n\tparts, minimum, maximum int) []int {\n\n\tif parts < minimum || parts < 1 || parts > maximum ||\n\t\tseen[parts] {\n\n\t\treturn dst\n\t}\n\n\tseen[parts] = true\n\treturn append(dst, parts)\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tbaseReservations := r.effectiveReservations()\n\n\tshardCap := r.nextShardCap\n\tminParts := 1\n\tif shardCap > 0 {\n\t\tminParts = int(total / shardCap)\n\t\tif total%shardCap != 0 {\n\t\t\tminParts++\n\t\t}\n\n\t\t// A transient failure must not make completion mathematically\n\t\t// impossible with the slots that remain. In that case, retain\n\t\t// diversification penalties but allow another full-sized route.\n\t\tif minParts > partLimit {\n\t\t\tminParts = 1\n\t\t\tshardCap = 0\n\t\t}\n\t}\n\n\tseenCounts := make(map[int]bool)\n\tpartCounts := make([]int, 0, 10)\n\n\tpartCounts = appendPartCount(\n\t\tpartCounts, seenCounts, minParts, minParts, partLimit,\n\t)\n\tpartCounts = appendPartCount(\n\t\tpartCounts, seenCounts, minParts+1, minParts, partLimit,\n\t)\n\tfor _, parts := range []int{1, 2, 3, 4, 5, 6, 8, 12} {\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, parts, minParts, partLimit,\n\t\t)\n\t}\n\tpartCounts = appendPartCount(\n\t\tpartCounts, seenCounts, partLimit, minParts, partLimit,\n\t)\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, baseReservations,\n\t\t\tshardCap, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor _, parts := range partCounts {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\n\t\tconsider(equalAllocation(total, parts))\n\n\t\tif parts >= 2 && parts <= 4 {\n\t\t\tconsider(heavyAllocation(total, parts, true))\n\t\t\tconsider(heavyAllocation(total, parts, false))\n\t\t}\n\t\tif parts >= 3 && parts <= 4 {\n\t\t\tconsider(rampAllocation(total, parts, true))\n\t\t\tconsider(rampAllocation(total, parts, false))\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\treturn nil, errors.New(\"no complete route set found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) reservePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\taddAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t}\n\n\tif meta.fee > lnwire.MaxMilliSatoshi-r.pendingFees {\n\t\tr.pendingFees = lnwire.MaxMilliSatoshi\n\t} else {\n\t\tr.pendingFees += meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) releasePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\tsubtractAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t}\n\n\tif meta.fee >= r.pendingFees {\n\t\tr.pendingFees = 0\n\t} else {\n\t\tr.pendingFees -= meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.reservePending(next.meta)\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.effectiveReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tif amount > lnwire.MaxMilliSatoshi-reservations[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(\n\tamt lnwire.MilliSatoshi) {\n\n\t// First diversify at the same amount. Splitting only after repeated\n\t// misses avoids consuming scarce atomic-MPP slots prematurely.\n\tif amt <= 1 || r.consecutiveMisses < 2 {\n\t\treturn\n\t}\n\n\tcap := amt * 3 / 4\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizeWholePath(\n\tmeta *routeMeta, penalty float64) {\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += penalty\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tr.releasePending(meta)\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.75, now,\n\t\t\t)\n\t\t\taddAmount(r.reserved, edge.key, meta.amounts[i])\n\t\t\tr.edgePenalty[edge.key] *= 0.15\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee > lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t} else {\n\t\t\tr.committedFees += meta.fee\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\tr.nextShardCap = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\t// An unreadable onion failure carries route-level evidence only.\n\t// Diversify the next route and adjust shard sizing, but never poison\n\t// a directed-channel belief from fabricated attribution.\n\tif reportedIdx < 0 {\n\t\tr.penalizeWholePath(meta, 160_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeWholePath(meta, 160_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\t// Attribution can shift by one hop. Only edges strictly before that\n\t// ambiguity window are safe to record as successful.\n\tsafePrefix := failIdx - 1\n\tfor i := 0; i < safePrefix; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.18, now,\n\t\t)\n\t}\n\n\tr.penalizeWholePath(meta, 55_000)\n\n\tedge := meta.path[failIdx]\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 450_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 450_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_350_000 + float64(count)*350_000\n\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\n\t\t// A first readable attribution is useful but not trustworthy\n\t\t// enough for a hard global conclusion. Repetition increases its\n\t\t// influence while retaining a non-zero recovery floor.\n\t\tweight := 0.10\n\t\tif count >= 2 {\n\t\t\tweight = 0.35\n\t\t}\n\t\tif count >= 4 {\n\t\t\tweight = 0.50\n\t\t}\n\t\tupdateBelief(\n\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t)\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_500_000 + float64(count)*900_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.edgePenalty[edge.key] += 750_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 26,
"parent": 2,
"score": 0.2793,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(30_000)\n\n\thopCostMsat = float64(8_000)\n\treusedEdgeCostMsat = float64(1_200_000)\n\textraPartCostMsat = float64(40_000)\n\n\tmaxSearchHops = 40\n\tmaxLabelsPerNode = 8\n\tmaxRouterAttempts = 80\n\tmaxConsecutiveMisses = 52\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpending map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpending: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) baseReservations(\n) map[edgeKey]lnwire.MilliSatoshi {\n\n\tresult := cloneReservations(r.reserved)\n\tfor key, amt := range r.pending {\n\t\tif amt > lnwire.MaxMilliSatoshi-result[key] {\n\t\t\tresult[key] = lnwire.MaxMilliSatoshi\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] += amt\n\t}\n\n\treturn result\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tamt := meta.amounts[i]\n\t\tif amt > lnwire.MaxMilliSatoshi-dst[edge.key] {\n\t\t\tdst[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\tcontinue\n\t\t}\n\t\tdst[edge.key] += amt\n\t}\n}\n\nfunc removeReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tkey := edge.key\n\t\tamt := meta.amounts[i]\n\t\tif dst[key] <= amt {\n\t\t\tdelete(dst, key)\n\t\t\tcontinue\n\t\t}\n\t\tdst[key] -= amt\n\t}\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := r.committedFees\n\tif r.pendingFees > lnwire.MaxMilliSatoshi-used {\n\t\treturn 0\n\t}\n\tused += r.pendingFees\n\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 2\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t}\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && totalLoad >= upper {\n\n\t\t\t\tfactor := 0.32 /\n\t\t\t\t\t(1 + 0.20*float64(r.localFails[edge.key]))\n\t\t\t\tp *= factor\n\t\t\t\tif p < 0.012 {\n\t\t\t\t\tp = 0.012\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tsum += weight\n\t\t}\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 || sum == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn result\n\t}\n\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\t\tif weight <= 0 || remainingWeight <= 0 {\n\t\t\treturn make([]lnwire.MilliSatoshi, len(weights))\n\t\t}\n\n\t\tq := int64(remaining) / remainingWeight\n\t\trem := int64(remaining) % remainingWeight\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tq*weight + rem*weight/remainingWeight,\n\t\t)\n\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tif part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\tif parts == 1 {\n\t\treturn [][]lnwire.MilliSatoshi{{total}}\n\t}\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 8)\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 1\n\t}\n\tpatterns = append(patterns, weightedAllocation(total, equal))\n\n\tswitch parts {\n\tcase 2:\n\t\tfor _, first := range []int64{60, 75, 90} {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{first, 100 - first},\n\t\t\t\t),\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{100 - first, first},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\tcase 3:\n\t\tfor favored := 0; favored < parts; favored++ {\n\t\t\tweights := []int64{1, 1, 1}\n\t\t\tweights[favored] = 2\n\t\t\tpatterns = append(\n\t\t\t\tpatterns, weightedAllocation(total, weights),\n\t\t\t)\n\t\t}\n\n\tdefault:\n\t\tif parts <= 8 {\n\t\t\tascending := make([]int64, parts)\n\t\t\tdescending := make([]int64, parts)\n\t\t\tfor i := 0; i < parts; i++ {\n\t\t\t\tascending[i] = int64(i + 1)\n\t\t\t\tdescending[i] = int64(parts - i)\n\t\t\t}\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(total, ascending),\n\t\t\t\tweightedAllocation(total, descending),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc appendPartCount(counts []int, value, limit int) []int {\n\tif value < 1 || value > limit {\n\t\treturn counts\n\t}\n\n\tfor _, existing := range counts {\n\t\tif existing == value {\n\t\t\treturn counts\n\t\t}\n\t}\n\n\treturn append(counts, value)\n}\n\nfunc (r *candidateRouter) targetPartFloor(\n\ttotal lnwire.MilliSatoshi) int {\n\n\tvar widest lnwire.MilliSatoshi\n\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\tlimit := edge.capacity\n\t\tif edge.maxHTLC > 0 && edge.maxHTLC < limit {\n\t\t\tlimit = edge.maxHTLC\n\t\t}\n\t\tif limit > widest {\n\t\t\twidest = limit\n\t\t}\n\t}\n\n\tif widest <= 0 {\n\t\treturn 1\n\t}\n\n\treturn int(1 + (total-1)/widest)\n}\n\nfunc (r *candidateRouter) candidatePartCounts(\n\ttotal lnwire.MilliSatoshi, slots uint32) []int {\n\n\tlimit := int(slots)\n\tcounts := make([]int, 0, 10)\n\n\tfor _, value := range []int{1, 2, 3, 4, 6, 8} {\n\t\tcounts = appendPartCount(counts, value, limit)\n\t}\n\n\tfloor := r.targetPartFloor(total)\n\tcounts = appendPartCount(counts, floor, limit)\n\tcounts = appendPartCount(counts, floor+1, limit)\n\n\tif limit > 8 {\n\t\tcounts = appendPartCount(counts, (limit+1)/2, limit)\n\t}\n\tcounts = appendPartCount(counts, limit, limit)\n\n\tsort.Ints(counts)\n\treturn counts\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := r.baseReservations()\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, remainingBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range r.candidatePartCounts(total, slots) {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\n\tr.issued[next.rt] = next.meta\n\taddReservationSet(r.pending, next.meta)\n\tif next.meta.fee <= lnwire.MaxMilliSatoshi-r.pendingFees {\n\t\tr.pendingFees += next.meta.fee\n\t} else {\n\t\tr.pendingFees = lnwire.MaxMilliSatoshi\n\t}\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tbase := r.baseReservations()\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tload := amount\n\t\tif base[key] <= lnwire.MaxMilliSatoshi-amount {\n\t\t\tload += base[key]\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, load)\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 280_000\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tif issuedMeta, ok := r.issued[rt]; ok {\n\t\tremoveReservationSet(r.pending, issuedMeta)\n\t\tif issuedMeta.fee >= r.pendingFees {\n\t\t\tr.pendingFees = 0\n\t\t} else {\n\t\t\tr.pendingFees -= issuedMeta.fee\n\t\t}\n\t}\n\tdelete(r.issued, rt)\n\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.reserved[edge.key] += meta.amounts[i]\n\t\t\tr.edgePenalty[edge.key] *= 0.25\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.18, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 110_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 110_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t450_000 + float64(count)*150_000\n\n\t\tweight := 0.12\n\t\tif count >= 2 {\n\t\t\tweight = 0.38\n\t\t}\n\t\tupdateBelief(\n\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t)\n\n\t\tif count >= 2 {\n\t\t\tr.recordLocalFailure(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + float64(count)*750_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 700_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 27,
"parent": 2,
"score": 0.0888,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(25_000)\n\n\thopCostMsat = float64(24_000)\n\treusedEdgeCostMsat = float64(900_000)\n\textraPartCostMsat = float64(25_000)\n\n\tmaxSearchHops = 48\n\tmaxLabelsPerNode = 12\n\tmaxRouterAttempts = 80\n\tmaxConsecutiveMisses = 76\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// Successful shards either consumed or still hold this liquidity.\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityVotes map[edgeKey]float64\n\tpolicyVotes map[edgeKey]float64\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tsplitPressure int\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityVotes: make(map[edgeKey]float64),\n\t\tpolicyVotes: make(map[edgeKey]float64),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\t// Failure evidence is deliberately soft. It can describe\n\t\t// concurrent reservations which disappear later.\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tminFeeIdx := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[minFeeIdx].fee {\n\t\t\t\tminFeeIdx = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == minFeeIdx {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\told := dst[edge.key]\n\t\tif meta.amounts[i] > lnwire.MaxMilliSatoshi-old {\n\t\t\tdst[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\tcontinue\n\t\t}\n\t\tdst[edge.key] = old + meta.amounts[i]\n\t}\n}\n\nfunc (r *candidateRouter) currentReservations(\n) map[edgeKey]lnwire.MilliSatoshi {\n\n\tresult := cloneReservations(r.reserved)\n\tfor _, meta := range r.issued {\n\t\taddReservationSet(result, meta)\n\t}\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.55\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) localLiquidityAdjustment(key edgeKey,\n\tload lnwire.MilliSatoshi) (float64, float64) {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 1, 0\n\t}\n\n\tratio := float64(load) / float64(upper)\n\tfailures := float64(r.localFails[key])\n\n\tswitch {\n\tcase ratio >= 1:\n\t\treturn 0.16, 350_000 + failures*110_000\n\n\tcase ratio >= 0.75:\n\t\treturn 0.48, 150_000 + failures*45_000\n\n\tdefault:\n\t\treturn 1, 0\n\t}\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif totalFee < cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tlocalFactor, localCost := r.localLiquidityAdjustment(\n\t\t\t\tedge.key, totalLoad,\n\t\t\t)\n\t\t\tp *= localFactor\n\n\t\t\tif edge.from == r.source {\n\t\t\t\t// The snapshot makes the first hop unusually reliable,\n\t\t\t\t// but concurrent sibling payments can still contend.\n\t\t\t\tp = 0.995 * localFactor\n\t\t\t}\n\t\t\tif p < 0.005 {\n\t\t\t\tp = 0.005\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key] +\n\t\t\t\tlocalCost\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.60\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\treserved := reservations[edge.key]\n\t\tif reserved > lnwire.MaxMilliSatoshi-amounts[i] {\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reserved\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar sum int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tsum += weight\n\t\t}\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif len(weights) == 0 || sum == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn result\n\t}\n\n\tremaining := total\n\tremainingWeight := sum\n\n\tfor i, weight := range weights {\n\t\tif weight <= 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\tif parts == 1 {\n\t\treturn [][]lnwire.MilliSatoshi{{total}}\n\t}\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 3*parts+20)\n\n\tequal := make([]int64, parts)\n\tfor i := range equal {\n\t\tequal[i] = 10\n\t}\n\tpatterns = append(patterns, weightedAllocation(total, equal))\n\n\tif parts == 2 {\n\t\tfor first := int64(5); first <= 95; first += 5 {\n\t\t\tpatterns = append(\n\t\t\t\tpatterns,\n\t\t\t\tweightedAllocation(\n\t\t\t\t\ttotal, []int64{first, 100 - first},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\treturn patterns\n\t}\n\n\tfor favored := 0; favored < parts; favored++ {\n\t\tmoderate := make([]int64, parts)\n\t\tstrong := make([]int64, parts)\n\t\tfor i := 0; i < parts; i++ {\n\t\t\tmoderate[i] = 10\n\t\t\tstrong[i] = 10\n\t\t}\n\t\tmoderate[favored] = 20\n\t\tstrong[favored] = 40\n\n\t\tpatterns = append(\n\t\t\tpatterns,\n\t\t\tweightedAllocation(total, moderate),\n\t\t\tweightedAllocation(total, strong),\n\t\t)\n\t}\n\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\talternating := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t\tif i%2 == 0 {\n\t\t\talternating[i] = 3\n\t\t} else {\n\t\t\talternating[i] = 1\n\t\t}\n\t}\n\n\tpatterns = append(\n\t\tpatterns,\n\t\tweightedAllocation(total, ascending),\n\t\tweightedAllocation(total, descending),\n\t\tweightedAllocation(total, alternating),\n\t)\n\n\treturn patterns\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := r.currentReservations()\n\tremainingBudget := feeBudget\n\tremainingAmount := lnwire.MilliSatoshi(0)\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-remainingAmount {\n\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tremainingAmount += shard\n\t}\n\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tshardBudget := remainingBudget\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi &&\n\t\t\tremainingAmount > 0 {\n\n\t\t\t// Preserve a proportional share for later shards. A small\n\t\t\t// allowance lets the present route absorb base fees.\n\t\t\tproportional := lnwire.MilliSatoshi(\n\t\t\t\tint64(remainingBudget) * int64(shard) /\n\t\t\t\t\tint64(remainingAmount),\n\t\t\t)\n\t\t\tallowance := remainingBudget / 20\n\t\t\tif proportional <= remainingBudget-allowance {\n\t\t\t\tshardBudget = proportional + allowance\n\t\t\t}\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, shardBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tminParts := 1\n\tif r.splitPressure >= 2 && slots >= 2 {\n\t\tminParts = 2\n\t}\n\tif r.splitPressure >= 5 && slots >= 3 {\n\t\tminParts = 3\n\t}\n\n\tfor parts := minParts; parts <= int(slots); parts++ {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif parts == 1 && r.splitPressure > 0 {\n\t\t\t\tscore += float64(r.splitPressure) * 2_000_000\n\t\t\t}\n\n\t\t\tif score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\t// If forced splitting was infeasible, a fresh full-sized path is still\n\t// better than terminally abandoning the payment.\n\tif len(best) == 0 && minParts > 1 {\n\t\tfor _, allocation := range allocationPatterns(total, 1) {\n\t\t\tplan, score, ok := r.tryAllocation(\n\t\t\t\tallocation, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 {\n\t\tqueued := queuedAmount(r.plan)\n\n\t\t// Some runners subtract an issued shard immediately, while atomic\n\t\t// runners can continue presenting the original unsettled amount.\n\t\t// Only discard a plan if it now exceeds the amount still requested.\n\t\tif queued > amt {\n\t\t\tr.plan = nil\n\t\t}\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.currentReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif reservations[key] > lnwire.MaxMilliSatoshi-amount {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) decayTransientState() {\n\tfor key, penalty := range r.edgePenalty {\n\t\tpenalty *= 0.90\n\t\tif penalty < 1_000 {\n\t\t\tdelete(r.edgePenalty, key)\n\t\t} else {\n\t\t\tr.edgePenalty[key] = penalty\n\t\t}\n\t}\n\n\tfor key, vote := range r.liquidityVotes {\n\t\tvote *= 0.97\n\t\tif vote < 0.05 {\n\t\t\tdelete(r.liquidityVotes, key)\n\t\t} else {\n\t\t\tr.liquidityVotes[key] = vote\n\t\t}\n\t}\n\n\tfor key, vote := range r.policyVotes {\n\t\tvote *= 0.98\n\t\tif vote < 0.05 {\n\t\t\tdelete(r.policyVotes, key)\n\t\t} else {\n\t\t\tr.policyVotes[key] = vote\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\t// This is route-choice pressure only, never liquidity evidence.\n\tperEdge := float64(240_000) / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc nearbyIndices(center, count int) []int {\n\tfirst := center - 1\n\tif first < 0 {\n\t\tfirst = 0\n\t}\n\tlast := center + 1\n\tif last >= count {\n\t\tlast = count - 1\n\t}\n\n\tresult := make([]int, 0, last-first+1)\n\tfor i := first; i <= last; i++ {\n\t\tresult = append(result, i)\n\t}\n\treturn result\n}\n\nfunc (r *candidateRouter) addLiquidityVotes(meta *routeMeta,\n\tfailIdx int) int {\n\n\tbestIdx := failIdx\n\tbestVote := float64(-1)\n\n\tfor _, idx := range nearbyIndices(failIdx, len(meta.path)) {\n\t\tweight := 0.55\n\t\tif idx == failIdx {\n\t\t\tweight = 1\n\t\t}\n\n\t\tkey := meta.path[idx].key\n\t\tr.liquidityVotes[key] += weight\n\n\t\tpressure := float64(170_000)\n\t\tif idx == failIdx {\n\t\t\tpressure = 480_000\n\t\t}\n\t\tr.edgePenalty[key] += pressure\n\n\t\tvote := r.liquidityVotes[key]\n\t\tif vote > bestVote ||\n\t\t\t(vote == bestVote && idx == failIdx) {\n\n\t\t\tbestVote = vote\n\t\t\tbestIdx = idx\n\t\t}\n\t}\n\n\treturn bestIdx\n}\n\nfunc (r *candidateRouter) addPolicyVotes(meta *routeMeta,\n\tfailIdx int) int {\n\n\tbestIdx := failIdx\n\tbestVote := float64(-1)\n\n\tfor _, idx := range nearbyIndices(failIdx, len(meta.path)) {\n\t\tweight := 0.50\n\t\tif idx == failIdx {\n\t\t\tweight = 1\n\t\t}\n\n\t\tkey := meta.path[idx].key\n\t\tr.policyVotes[key] += weight\n\n\t\tpressure := float64(500_000)\n\t\tif idx == failIdx {\n\t\t\tpressure = 1_350_000\n\t\t}\n\t\tr.edgePenalty[key] += pressure\n\n\t\tvote := r.policyVotes[key]\n\t\tif vote > bestVote ||\n\t\t\t(vote == bestVote && idx == failIdx) {\n\n\t\t\tbestVote = vote\n\t\t\tbestIdx = idx\n\t\t}\n\t}\n\n\treturn bestIdx\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tr.decayTransientState()\n\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.72, now,\n\t\t\t)\n\n\t\t\told := r.reserved[edge.key]\n\t\t\tif meta.amounts[i] > lnwire.MaxMilliSatoshi-old {\n\t\t\t\tr.reserved[edge.key] = lnwire.MaxMilliSatoshi\n\t\t\t} else {\n\t\t\t\tr.reserved[edge.key] = old + meta.amounts[i]\n\t\t\t}\n\n\t\t\tr.edgePenalty[edge.key] *= 0.18\n\t\t\tdelete(r.liquidityVotes, edge.key)\n\t\t\tdelete(r.policyVotes, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tif r.splitPressure > 0 {\n\t\t\tr.splitPressure--\n\t\t}\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\tif r.splitPressure < 6 {\n\t\tr.splitPressure++\n\t}\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\t// An off-route source explicitly carries no channel attribution.\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\t// With a possible one-hop attribution shift, edges strictly before\n\t// failIdx-1 are the safely traversed prefix.\n\tsafePassed := failIdx - 1\n\tif safePassed < 0 {\n\t\tsafePassed = 0\n\t}\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.16, now,\n\t\t)\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tbestIdx := r.addLiquidityVotes(meta, failIdx)\n\t\tbestEdge := meta.path[bestIdx]\n\t\tvote := r.liquidityVotes[bestEdge.key]\n\n\t\t// Repeated overlapping reports corroborate an edge even when an\n\t\t// occasional report is shifted to an adjacent node.\n\t\tif vote >= 2.20 {\n\t\t\tupdateBelief(\n\t\t\t\tbestEdge.key, meta.loads[bestIdx],\n\t\t\t\tfalse, 0.34, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(\n\t\t\t\tbestEdge.key, meta.loads[bestIdx],\n\t\t\t)\n\t\t\tr.liquidityVotes[bestEdge.key] *= 0.55\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tbestIdx := r.addPolicyVotes(meta, failIdx)\n\t\tbestEdge := meta.path[bestIdx]\n\t\tvote := r.policyVotes[bestEdge.key]\n\n\t\t// Policy errors are deterministic, but their reported location can\n\t\t// still be shifted. Demand stronger corroboration than liquidity.\n\t\tif vote >= 3.40 {\n\t\t\tr.blocked[bestEdge.key] = true\n\t\t}\n\n\tdefault:\n\t\t// An unknown code is not liquidity evidence. Apply only modest,\n\t\t// local, transient route pressure.\n\t\tfor _, idx := range nearbyIndices(failIdx, len(meta.path)) {\n\t\t\tpressure := float64(85_000)\n\t\t\tif idx == failIdx {\n\t\t\t\tpressure = 180_000\n\t\t\t}\n\t\t\tr.edgePenalty[meta.path[idx].key] += pressure\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 28,
"parent": 2,
"score": 0.0507,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(60_000)\n\n\thopCostMsat = float64(85_000)\n\treusedEdgeCostMsat = float64(1_600_000)\n\textraPartCostMsat = float64(55_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 5\n\tmaxRouterAttempts = 40\n\tmaxConsecutiveMisses = 32\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\theld []*routeMeta\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]int\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]int),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := make([]*searchState, 0, len(old)+1)\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\tlabels[s.node] = append(kept, other)\n\t\t\tfor _, tail := range old {\n\t\t\t\tif tail.active && tail != other {\n\t\t\t\t\tlabels[s.node] = append(labels[s.node], tail)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tprotected := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[protected].fee {\n\t\t\t\tprotected = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == protected {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.75\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 7 / 8\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 3\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.6\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tvar weightSum int64\n\tfor _, weight := range weights {\n\t\tif weight > 0 {\n\t\t\tweightSum += weight\n\t\t}\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tif total < lnwire.MilliSatoshi(len(weights)) ||\n\t\tlen(weights) == 0 || weightSum == 0 {\n\n\t\treturn nil\n\t}\n\n\tremaining := total\n\tremainingWeight := weightSum\n\n\tfor i, weight := range weights {\n\t\tif i == len(weights)-1 {\n\t\t\tresult[i] = remaining\n\t\t\tbreak\n\t\t}\n\n\t\tminLater := lnwire.MilliSatoshi(len(weights) - i - 1)\n\t\tpart := lnwire.MilliSatoshi(\n\t\t\tint64(remaining) * weight / remainingWeight,\n\t\t)\n\t\tif part <= 0 {\n\t\t\tpart = 1\n\t\t}\n\t\tif part > remaining-minLater {\n\t\t\tpart = remaining - minLater\n\t\t}\n\n\t\tresult[i] = part\n\t\tremaining -= part\n\t\tremainingWeight -= weight\n\t}\n\n\treturn result\n}\n\nfunc allocationPatterns(total lnwire.MilliSatoshi,\n\tparts int) [][]lnwire.MilliSatoshi {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil\n\t}\n\tif parts == 1 {\n\t\treturn [][]lnwire.MilliSatoshi{{total}}\n\t}\n\n\tpatterns := make([][]lnwire.MilliSatoshi, 0, 5)\n\n\tif parts == 2 {\n\t\tfor _, weights := range [][]int64{\n\t\t\t{1, 1},\n\t\t\t{1, 2},\n\t\t\t{2, 1},\n\t\t\t{1, 3},\n\t\t\t{3, 1},\n\t\t} {\n\t\t\tif allocation := weightedAllocation(total, weights);\n\t\t\t\tallocation != nil {\n\n\t\t\t\tpatterns = append(patterns, allocation)\n\t\t\t}\n\t\t}\n\t\treturn patterns\n\t}\n\n\tequal := make([]int64, parts)\n\tascending := make([]int64, parts)\n\tdescending := make([]int64, parts)\n\tfor i := 0; i < parts; i++ {\n\t\tequal[i] = 1\n\t\tascending[i] = int64(i + 1)\n\t\tdescending[i] = int64(parts - i)\n\t}\n\n\tfor _, weights := range [][]int64{\n\t\tequal, ascending, descending,\n\t} {\n\t\tif allocation := weightedAllocation(total, weights);\n\t\t\tallocation != nil {\n\n\t\t\tpatterns = append(patterns, allocation)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc candidatePartCounts(slots uint32) []int {\n\tif slots == 0 {\n\t\treturn nil\n\t}\n\n\tmaxParts := int(slots)\n\tseen := make(map[int]bool)\n\tresult := make([]int, 0, 6)\n\n\tadd := func(parts int) {\n\t\tif parts < 1 || parts > maxParts || seen[parts] {\n\t\t\treturn\n\t\t}\n\t\tseen[parts] = true\n\t\tresult = append(result, parts)\n\t}\n\n\tadd(1)\n\tadd(2)\n\tadd(3)\n\tadd(4)\n\tif maxParts > 4 {\n\t\tadd((maxParts + 4) / 2)\n\t\tadd(maxParts)\n\t}\n\n\treturn result\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc removeReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif dst[edge.key] <= amount {\n\t\t\tdelete(dst, edge.key)\n\t\t} else {\n\t\t\tdst[edge.key] -= amount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) syncHeld(inFlight uint32) bool {\n\twanted := int(inFlight)\n\tif wanted >= len(r.held) {\n\t\treturn false\n\t}\n\n\tdrop := len(r.held) - wanted\n\tfor i := 0; i < drop; i++ {\n\t\tremoveReservationSet(r.reserved, r.held[i])\n\t}\n\n\tr.held = append([]*routeMeta(nil), r.held[drop:]...)\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\treservations := cloneReservations(r.reserved)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tpr, err := r.findRoute(shard, reservations, remainingBudget)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32, allowPartial bool) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range candidatePartCounts(slots) {\n\t\tfor _, allocation := range allocationPatterns(total, parts) {\n\t\t\tplan, score, ok := r.tryAllocation(allocation, feeBudget)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\tif allowPartial {\n\t\tfor _, denominator := range []int64{3, 2, 3, 4} {\n\t\t\tvar shard lnwire.MilliSatoshi\n\t\t\tif denominator == 3 && shard == 0 {\n\t\t\t\tshard = total * 2 / 3\n\t\t\t} else {\n\t\t\t\tshard = total / lnwire.MilliSatoshi(denominator)\n\t\t\t}\n\n\t\t\tif shard <= 0 || shard >= total {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpr, err := r.findRoute(shard, r.reserved, feeBudget)\n\t\t\tif err == nil {\n\t\t\t\treturn []*plannedRoute{pr}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tif r.syncHeld(inFlightHtlcs) {\n\t\tr.plan = nil\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(\n\t\t\tamt, slots, inFlightHtlcs == 0,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) penalizeUnattributed(meta *routeMeta) {\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += 110_000\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.70, now,\n\t\t\t)\n\t\t\tr.edgePenalty[edge.key] *= 0.20\n\t\t\tdelete(r.liquiditySuspects, edge.key)\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\taddReservationSet(r.reserved, meta)\n\t\tr.held = append(r.held, meta)\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeUnattributed(meta)\n\t\treturn nil\n\t}\n\n\tsafePassed := failIdx - 1\n\tfor i := 0; i < safePassed; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.28, now,\n\t\t)\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tif failIdx > 0 {\n\t\tr.edgePenalty[meta.path[failIdx-1].key] += 180_000\n\t}\n\tif failIdx+1 < len(meta.path) {\n\t\tr.edgePenalty[meta.path[failIdx+1].key] += 180_000\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tcount := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_800_000 + float64(count)*450_000\n\n\t\tif count == 1 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.18, now,\n\t\t\t)\n\t\t}\n\t\tif count >= 2 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.48, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_500_000 + float64(count)*1_000_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 900_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 29,
"parent": 6,
"score": 0.0718,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(18_000)\n\n\thopCostMsat = float64(75_000)\n\treusedEdgeCostMsat = float64(2_200_000)\n\textraPartCostMsat = float64(45_000)\n\n\tmaxSearchHops = 26\n\tmaxLabelsPerNode = 20\n\tmaxPlanSearches = 260\n\tmaxPlannedParts = 20\n\tmaxRouterAttempts = 48\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\tif int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpendingReserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityVotes map[edgeKey]int\n\tpolicyVotes map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpendingReserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityVotes: make(map[edgeKey]int),\n\t\tpolicyVotes: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief,\n\tnow time.Time) float64 {\n\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/1800)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc sameContinuation(a, b *searchState) bool {\n\treturn a.nextEdge == b.nextEdge\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif sameContinuation(other, s) &&\n\t\t\tother.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\tlabels[s.node] = append(kept, other)\n\t\t\tfor _, tail := range old[len(kept)+1:] {\n\t\t\t\tif tail.active {\n\t\t\t\t\tlabels[s.node] = append(labels[s.node], tail)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\n\t\tif sameContinuation(other, s) &&\n\t\t\ts.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tbestScore := 0\n\t\tcheapestFee := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].score < kept[bestScore].score {\n\t\t\t\tbestScore = i\n\t\t\t}\n\t\t\tif kept[i].fee < kept[cheapestFee].fee {\n\t\t\t\tcheapestFee = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == bestScore || i == cheapestFee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst < 0 {\n\t\t\tworst = len(kept) - 1\n\t\t}\n\t\tremoved := kept[worst]\n\t\tremoved.active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\tif removed == s {\n\t\t\tlabels[s.node] = kept\n\t\t\treturn false\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc addAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\told := dst[key]\n\tif old >= lnwire.MaxMilliSatoshi-amt {\n\t\tdst[key] = lnwire.MaxMilliSatoshi\n\t\treturn\n\t}\n\n\tdst[key] = old + amt\n}\n\nfunc subtractAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\told := dst[key]\n\tif amt >= old {\n\t\tdelete(dst, key)\n\t\treturn\n\t}\n\n\tdst[key] = old - amt\n}\n\nfunc (r *candidateRouter) effectiveReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\tfor key, amt := range r.pendingReserved {\n\t\taddAmount(result, key, amt)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := r.committedFees\n\tif used >= lnwire.MaxMilliSatoshi-r.pendingFees {\n\t\tused = lnwire.MaxMilliSatoshi\n\t} else {\n\t\tused += r.pendingFees\n\t}\n\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) / 3\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 || r.localFails[key] < 1 {\n\t\treturn 0\n\t}\n\n\tvar cutoff lnwire.MilliSatoshi\n\tswitch {\n\tcase r.localFails[key] >= 3:\n\t\tcutoff = upper * 58 / 100\n\tcase r.localFails[key] == 2:\n\t\tcutoff = upper * 70 / 100\n\tdefault:\n\t\tcutoff = upper * 82 / 100\n\t}\n\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) dynamicPenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif upper > 0 && load < upper*2/3 {\n\t\tpenalty *= 0.10\n\t}\n\n\treturn penalty\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.dynamicPenalty(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tvar allocated lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tshard := total * lnwire.MilliSatoshi(weight) /\n\t\t\tlnwire.MilliSatoshi(weightTotal)\n\t\tif shard <= 0 {\n\t\t\tshard = 1\n\t\t}\n\n\t\tif i == len(weights)-1 {\n\t\t\tshard = total - allocated\n\t\t}\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-allocated {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tresult[i] = shard\n\t\tallocated += shard\n\t}\n\n\tif allocated != total {\n\t\treturn nil\n\t}\n\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc rampAllocation(total lnwire.MilliSatoshi,\n\tparts int, descending bool) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tif descending {\n\t\t\tweights[i] = int64(parts - i)\n\t\t} else {\n\t\t\tweights[i] = int64(i + 1)\n\t\t}\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc endHeavyAllocation(total lnwire.MilliSatoshi,\n\tparts int, first bool, weight int64) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\n\tif first {\n\t\tweights[0] = weight\n\t} else {\n\t\tweights[len(weights)-1] = weight\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\taddAmount(dst, edge.key, meta.amounts[i])\n\t}\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\n\treturn total, true\n}\n\nfunc shardFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi || remaining <= 0 ||\n\t\tshard >= remaining {\n\n\t\treturn budget\n\t}\n\n\tshare := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif share < 0 {\n\t\tshare = 0\n\t}\n\n\tslack := (budget - share) / 2\n\tif share >= budget-slack {\n\t\treturn budget\n\t}\n\n\treturn share + slack\n}\n\nfunc shardCapPenalty(shard,\n\tshardCap lnwire.MilliSatoshi) float64 {\n\n\tif shardCap <= 0 || shard <= shardCap {\n\t\treturn 0\n\t}\n\n\texcess := float64(shard-shardCap) / float64(shardCap)\n\treturn 2_000_000 * (1 + excess)\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\ttotalScore := float64(0)\n\n\tfor _, shard := range allocation {\n\t\tfeeCap := shardFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score + shardCapPenalty(shard, shardCap)\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) largestRoute(\n\tmaxAmt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tsearches *int) (*plannedRoute, bool) {\n\n\tif maxAmt <= 0 || *searches >= maxPlanSearches {\n\t\treturn nil, false\n\t}\n\n\ttry := func(amt lnwire.MilliSatoshi) *plannedRoute {\n\t\tif amt <= 0 || *searches >= maxPlanSearches {\n\t\t\treturn nil\n\t\t}\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(amt, reservations, feeBudget)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn pr\n\t}\n\n\tif pr := try(maxAmt); pr != nil {\n\t\treturn pr, true\n\t}\n\n\tfailedHigh := maxAmt\n\tprobe := maxAmt / 2\n\tvar best *plannedRoute\n\n\tfor probe > 0 && *searches < maxPlanSearches {\n\t\tpr := try(probe)\n\t\tif pr != nil {\n\t\t\tbest = pr\n\t\t\tbreak\n\t\t}\n\n\t\tfailedHigh = probe\n\t\tprobe /= 2\n\t}\n\n\tif best == nil {\n\t\treturn nil, false\n\t}\n\n\tlow := best.meta.delivered\n\thigh := failedHigh\n\tif high <= low {\n\t\thigh = maxAmt\n\t}\n\n\tfor i := 0; i < 7 && high-low > 1 &&\n\t\t*searches < maxPlanSearches; i++ {\n\n\t\tmid := low + (high-low)/2\n\t\tpr := try(mid)\n\t\tif pr != nil {\n\t\t\tbest = pr\n\t\t\tlow = mid\n\t\t} else {\n\t\t\thigh = mid\n\t\t}\n\t}\n\n\treturn best, true\n}\n\nfunc (r *candidateRouter) tryGreedy(\n\ttotal lnwire.MilliSatoshi, parts, mode int,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif parts < 2 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremaining := total\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, parts)\n\ttotalScore := float64(0)\n\n\tfor i := 0; i < parts; i++ {\n\t\tleft := parts - i\n\t\tvar pr *plannedRoute\n\n\t\tif left == 1 {\n\t\t\tif *searches >= maxPlanSearches {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\t(*searches)++\n\t\t\tcandidate, err := r.findRoute(\n\t\t\t\tremaining, reservations, remainingBudget,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tpr = candidate\n\t\t} else {\n\t\t\taverage := remaining / lnwire.MilliSatoshi(left)\n\t\t\tif remaining%lnwire.MilliSatoshi(left) != 0 {\n\t\t\t\taverage++\n\t\t\t}\n\n\t\t\tupper := average\n\t\t\tswitch mode {\n\t\t\tcase 0:\n\t\t\t\tupper = average * 5 / 4\n\t\t\tcase 1:\n\t\t\t\ttail := average / 3\n\t\t\t\tif tail < 1 {\n\t\t\t\t\ttail = 1\n\t\t\t\t}\n\t\t\t\treserveTail := tail *\n\t\t\t\t\tlnwire.MilliSatoshi(left-1)\n\t\t\t\tif reserveTail < remaining {\n\t\t\t\t\tupper = remaining - reserveTail\n\t\t\t\t}\n\t\t\tcase 2:\n\t\t\t\tupper = average\n\t\t\t}\n\n\t\t\tif upper >= remaining {\n\t\t\t\tupper = remaining - lnwire.MilliSatoshi(left-1)\n\t\t\t}\n\t\t\tif upper <= 0 {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\n\t\t\tfeeCap := shardFeeCap(\n\t\t\t\tremainingBudget, upper, remaining,\n\t\t\t)\n\t\t\tcandidate, ok := r.largestRoute(\n\t\t\t\tupper, reservations, feeCap, searches,\n\t\t\t)\n\t\t\tif !ok {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tpr = candidate\n\t\t}\n\n\t\tif pr.meta.delivered <= 0 ||\n\t\t\tpr.meta.delivered > remaining {\n\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score +\n\t\t\tshardCapPenalty(pr.meta.delivered, shardCap)\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremaining -= pr.meta.delivered\n\t}\n\n\tif remaining != 0 {\n\t\treturn nil, 0, false\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc appendPartCount(dst []int, seen map[int]bool,\n\tparts, maximum int) []int {\n\n\tif parts < 1 || parts > maximum || seen[parts] {\n\t\treturn dst\n\t}\n\n\tseen[parts] = true\n\treturn append(dst, parts)\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tbaseReservations := r.effectiveReservations()\n\tshardCap := r.nextShardCap\n\n\tseenCounts := make(map[int]bool)\n\tpartCounts := make([]int, 0, partLimit)\n\n\tif shardCap > 0 {\n\t\tsuggested := int(total / shardCap)\n\t\tif total%shardCap != 0 {\n\t\t\tsuggested++\n\t\t}\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, suggested, partLimit,\n\t\t)\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, suggested+1, partLimit,\n\t\t)\n\t}\n\n\tfor parts := 1; parts <= partLimit && parts <= 8; parts++ {\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, parts, partLimit,\n\t\t)\n\t}\n\tfor _, parts := range []int{10, 12, 16, partLimit} {\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, parts, partLimit,\n\t\t)\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, baseReservations,\n\t\t\tshardCap, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor _, parts := range partCounts {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\n\t\tconsider(equalAllocation(total, parts))\n\n\t\tif parts >= 2 && parts <= 6 {\n\t\t\tconsider(endHeavyAllocation(total, parts, true, 2))\n\t\t\tconsider(endHeavyAllocation(total, parts, false, 2))\n\t\t\tconsider(rampAllocation(total, parts, true))\n\t\t\tconsider(rampAllocation(total, parts, false))\n\t\t}\n\n\t\tif parts == 2 {\n\t\t\tconsider(endHeavyAllocation(total, parts, true, 3))\n\t\t\tconsider(endHeavyAllocation(total, parts, false, 3))\n\t\t\tconsider(endHeavyAllocation(total, parts, true, 4))\n\t\t\tconsider(endHeavyAllocation(total, parts, false, 4))\n\t\t}\n\t}\n\n\tfor _, parts := range partCounts {\n\t\tif parts < 2 || parts > 6 ||\n\t\t\tsearches >= maxPlanSearches {\n\n\t\t\tcontinue\n\t\t}\n\n\t\tfor mode := 0; mode < 3; mode++ {\n\t\t\tplan, score, ok := r.tryGreedy(\n\t\t\t\ttotal, parts, mode, feeBudget,\n\t\t\t\tbaseReservations, shardCap, &searches,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\n\t// If a complete route set cannot be found, avoid giving up without\n\t// sending anything. A bounded partial shard can reveal or consume a\n\t// viable corridor, after which the remaining amount is replanned.\n\tif partLimit > 1 && total > 1 && searches < maxPlanSearches {\n\t\tmaxShard := total * 3 / 4\n\t\taverage := total / lnwire.MilliSatoshi(partLimit)\n\t\tif total%lnwire.MilliSatoshi(partLimit) != 0 {\n\t\t\taverage++\n\t\t}\n\t\tif maxShard < average {\n\t\t\tmaxShard = average\n\t\t}\n\t\tif maxShard >= total {\n\t\t\tmaxShard = total - 1\n\t\t}\n\n\t\tfeeCap := shardFeeCap(feeBudget, maxShard, total)\n\t\tpr, ok := r.largestRoute(\n\t\t\tmaxShard, baseReservations, feeCap, &searches,\n\t\t)\n\t\tif ok {\n\t\t\treturn []*plannedRoute{pr}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) reservePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\taddAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t}\n\n\tif meta.fee >= lnwire.MaxMilliSatoshi-r.pendingFees {\n\t\tr.pendingFees = lnwire.MaxMilliSatoshi\n\t} else {\n\t\tr.pendingFees += meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) releasePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\tsubtractAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t}\n\n\tif meta.fee >= r.pendingFees {\n\t\tr.pendingFees = 0\n\t} else {\n\t\tr.pendingFees -= meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.reservePending(next.meta)\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.effectiveReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tif amount > lnwire.MaxMilliSatoshi-reservations[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(\n\tamt lnwire.MilliSatoshi, numerator int64) {\n\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tcap := amt * lnwire.MilliSatoshi(numerator) / 100\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\tcap = 1\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizeWholePath(\n\tmeta *routeMeta, penalty float64) {\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += penalty\n\t}\n}\n\nfunc ambiguityWindow(center, length int) []int {\n\tstart := center - 1\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := center + 1\n\tif end >= length {\n\t\tend = length - 1\n\t}\n\n\tresult := make([]int, 0, 3)\n\tfor i := start; i <= end; i++ {\n\t\tresult = append(result, i)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tr.releasePending(meta)\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.78, now,\n\t\t\t)\n\t\t\taddAmount(r.reserved, edge.key, meta.amounts[i])\n\t\t\tr.edgePenalty[edge.key] *= 0.10\n\t\t\tdelete(r.liquidityVotes, edge.key)\n\t\t\tdelete(r.policyVotes, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tif meta.fee >= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t} else {\n\t\t\tr.committedFees += meta.fee\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\tr.nextShardCap = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.penalizeWholePath(meta, 210_000)\n\t\tr.lowerFutureShard(meta.delivered, 75)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeWholePath(meta, 210_000)\n\t\tr.lowerFutureShard(meta.delivered, 75)\n\t\treturn nil\n\t}\n\n\t// Attribution can move one hop. Only the prefix before the entire\n\t// ambiguity window is safe positive evidence.\n\tfor i := 0; i < failIdx-1; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.20, now,\n\t\t)\n\t}\n\n\tr.penalizeWholePath(meta, 70_000)\n\twindow := ambiguityWindow(failIdx, len(meta.path))\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered, 65)\n\n\t\tfor _, idx := range window {\n\t\t\tedge := meta.path[idx]\n\t\t\tvote := 1\n\t\t\tpenalty := float64(320_000)\n\t\t\tif idx == failIdx {\n\t\t\t\tvote = 2\n\t\t\t\tpenalty = 1_050_000\n\t\t\t}\n\n\t\t\tr.liquidityVotes[edge.key] += vote\n\t\t\tr.edgePenalty[edge.key] += penalty\n\t\t}\n\n\t\tedge := meta.path[failIdx]\n\t\tvotes := r.liquidityVotes[edge.key]\n\n\t\t// A single readable report changes route preference, but does\n\t\t// not create a hard liquidity ceiling. Two corroborating center\n\t\t// reports are required before committing persistent evidence.\n\t\tif votes >= 4 {\n\t\t\tr.recordLocalFailure(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\n\t\t\tweight := 0.28\n\t\t\tif votes >= 7 {\n\t\t\t\tweight = 0.42\n\t\t\t}\n\t\t\tif votes >= 10 {\n\t\t\t\tweight = 0.55\n\t\t\t}\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t\tfalse, weight, now,\n\t\t\t)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tfor _, idx := range window {\n\t\t\tedge := meta.path[idx]\n\t\t\tvote := 1\n\t\t\tpenalty := float64(600_000)\n\t\t\tif idx == failIdx {\n\t\t\t\tvote = 2\n\t\t\t\tpenalty = 2_200_000\n\t\t\t}\n\n\t\t\tr.policyVotes[edge.key] += vote\n\t\t\tr.edgePenalty[edge.key] += penalty\n\t\t}\n\n\t\tedge := meta.path[failIdx]\n\t\tif r.policyVotes[edge.key] >= 6 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered, 72)\n\t\tfor _, idx := range window {\n\t\t\tpenalty := float64(280_000)\n\t\t\tif idx == failIdx {\n\t\t\t\tpenalty = 700_000\n\t\t\t}\n\t\t\tr.edgePenalty[meta.path[idx].key] += penalty\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 30,
"parent": 7,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "// reflection unavailable: codex exec timed out after 1200s (process group killed)"
}
},
{
"id": 31,
"parent": 6,
"score": 0.1489,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(15_000)\n\n\thopCostMsat = float64(45_000)\n\treusedEdgeCostMsat = float64(2_100_000)\n\textraPartCostMsat = float64(35_000)\n\n\tmaxSearchHops = 26\n\tmaxLabelsPerNode = 12\n\tmaxPlanSearches = 160\n\tmaxPlannedParts = 16\n\tmaxRouterAttempts = 40\n\tmaxConsecutiveMisses = 34\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedState = struct {\n\tsync.Mutex\n\tbeliefs map[edgeKey]liquidityBelief\n\tholds map[edgeKey]map[uint64]lnwire.MilliSatoshi\n\tnextOwner uint64\n}{\n\tbeliefs: make(map[edgeKey]liquidityBelief),\n\tholds: make(map[edgeKey]map[uint64]lnwire.MilliSatoshi),\n}\n\nfunc allocateOwner() uint64 {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tsharedState.nextOwner++\n\tif sharedState.nextOwner == 0 {\n\t\tsharedState.nextOwner++\n\t}\n\n\treturn sharedState.nextOwner\n}\n\nfunc addSaturated(a, b lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif b > lnwire.MaxMilliSatoshi-a {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\treturn a + b\n}\n\nfunc addSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\towners = make(map[uint64]lnwire.MilliSatoshi)\n\t\tsharedState.holds[key] = owners\n\t}\n\towners[owner] = addSaturated(owners[owner], amt)\n}\n\nfunc subtractSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\treturn\n\t}\n\n\told := owners[owner]\n\tif amt >= old {\n\t\tdelete(owners, owner)\n\t} else {\n\t\towners[owner] = old - amt\n\t}\n\tif len(owners) == 0 {\n\t\tdelete(sharedState.holds, key)\n\t}\n}\n\nfunc sharedHoldsExcept(owner uint64) map[edgeKey]lnwire.MilliSatoshi {\n\tresult := make(map[edgeKey]lnwire.MilliSatoshi)\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tvar total lnwire.MilliSatoshi\n\t\tfor other, amt := range owners {\n\t\t\tif other == owner {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotal = addSaturated(total, amt)\n\t\t}\n\t\tif total > 0 {\n\t\t\tresult[key] = total\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc releaseAllSharedHolds(owner uint64) {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tdelete(owners, owner)\n\t\tif len(owners) == 0 {\n\t\t\tdelete(sharedState.holds, key)\n\t\t}\n\t}\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc (e *candidateEdge) publicLimit() lnwire.MilliSatoshi {\n\tlimit := e.capacity\n\tif e.maxHTLC > 0 && e.maxHTLC < limit {\n\t\tlimit = e.maxHTLC\n\t}\n\treturn limit\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\tif int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\towner uint64\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpendingReserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityReports map[edgeKey]int\n\tpolicyReports map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tinitialAmount lnwire.MilliSatoshi\n\tsucceededAmount lnwire.MilliSatoshi\n\tattempts int\n\tmisses int\n\tcleaned bool\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\towner: allocateOwner(),\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpendingReserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityReports: make(map[edgeKey]int),\n\t\tpolicyReports: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedState.Lock()\n\tb, ok := sharedState.beliefs[key]\n\tsharedState.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.015\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.015 + 0.98*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tb := sharedState.beliefs[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedState.beliefs[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\tdst[key] = addSaturated(dst[key], amt)\n}\n\nfunc subtractAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\told := dst[key]\n\tif amt >= old {\n\t\tdelete(dst, key)\n\t\treturn\n\t}\n\tdst[key] = old - amt\n}\n\nfunc (r *candidateRouter) effectiveReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\tfor key, amt := range r.pendingReserved {\n\t\taddAmount(result, key, amt)\n\t}\n\tfor key, amt := range sharedHoldsExcept(r.owner) {\n\t\taddAmount(result, key, amt)\n\t}\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := addSaturated(r.committedFees, r.pendingFees)\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.65\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tif r.localFails[key] < 2 {\n\t\treturn 0\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tvar cutoff lnwire.MilliSatoshi\n\tswitch {\n\tcase r.localFails[key] >= 5:\n\t\tcutoff = upper * 55 / 100\n\tcase r.localFails[key] >= 3:\n\t\tcutoff = upper * 68 / 100\n\tdefault:\n\t\tcutoff = upper * 82 / 100\n\t}\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) dynamicPenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif upper > 0 && load < upper*3/4 {\n\t\tpenalty *= 0.15\n\t}\n\treturn penalty\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.997\n\t\t\t\tif r.localFails[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.dynamicPenalty(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.45\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tvar allocated lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tvar shard lnwire.MilliSatoshi\n\t\tif float64(total)*float64(weight) > float64(math.MaxInt64) {\n\t\t\tshard = lnwire.MilliSatoshi(\n\t\t\t\tfloat64(total) * float64(weight) /\n\t\t\t\t\tfloat64(weightTotal),\n\t\t\t)\n\t\t} else {\n\t\t\tshard = total * lnwire.MilliSatoshi(weight) /\n\t\t\t\tlnwire.MilliSatoshi(weightTotal)\n\t\t}\n\n\t\tif shard <= 0 {\n\t\t\tshard = 1\n\t\t}\n\t\tif i == len(weights)-1 {\n\t\t\tshard = total - allocated\n\t\t}\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-allocated {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tresult[i] = shard\n\t\tallocated += shard\n\t}\n\n\tif allocated != total {\n\t\treturn nil\n\t}\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc shapedAllocation(total lnwire.MilliSatoshi,\n\tparts int, mode int) []lnwire.MilliSatoshi {\n\n\tif parts < 2 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tswitch mode {\n\t\tcase 0:\n\t\t\tweights[i] = 1\n\t\t\tweights[0] = 2\n\t\tcase 1:\n\t\t\tweights[i] = 1\n\t\t\tweights[len(weights)-1] = 2\n\t\tcase 2:\n\t\t\tweights[i] = int64(parts - i)\n\t\tcase 3:\n\t\t\tweights[i] = int64(i + 1)\n\t\t}\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\taddAmount(dst, edge.key, meta.amounts[i])\n\t}\n}\n\nfunc planningFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\n\tshare := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif share <= 0 {\n\t\tshare = 1\n\t}\n\n\tcap := addSaturated(share, share*3/4)\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\treturn cap\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc allocationAllowed(allocation []lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi) bool {\n\n\tif shardCap <= 0 {\n\t\treturn true\n\t}\n\tfor _, shard := range allocation {\n\t\tif shard > shardCap {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches ||\n\t\t!allocationAllowed(allocation, shardCap) {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\tvar totalScore float64\n\n\tfor _, shard := range allocation {\n\t\tfeeCap := planningFeeCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc appendPartCount(dst []int, seen map[int]bool,\n\tparts, minimum, maximum int) []int {\n\n\tif parts < minimum || parts < 1 ||\n\t\tparts > maximum || seen[parts] {\n\n\t\treturn dst\n\t}\n\n\tseen[parts] = true\n\treturn append(dst, parts)\n}\n\nfunc (r *candidateRouter) splitHints(\n\ttotal lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\thints := make([]lnwire.MilliSatoshi, 0, 40)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\tadd := func(v lnwire.MilliSatoshi) {\n\t\tif v <= 0 || v >= total || seen[v] {\n\t\t\treturn\n\t\t}\n\t\tseen[v] = true\n\t\thints = append(hints, v)\n\t}\n\n\tfor ratio := int64(1); ratio <= 9; ratio++ {\n\t\tadd(total * lnwire.MilliSatoshi(ratio) / 10)\n\t}\n\n\taddLimit := func(limit lnwire.MilliSatoshi) {\n\t\tif limit <= 0 {\n\t\t\treturn\n\t\t}\n\t\tfor _, pct := range []int64{60, 72, 84, 93, 97} {\n\t\t\tadd(limit * lnwire.MilliSatoshi(pct) / 100)\n\t\t}\n\t}\n\n\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\taddLimit(edge.publicLimit())\n\t}\n\n\tfor _, edges := range r.incomingEdges {\n\t\tfor _, edge := range edges {\n\t\t\tif edge.from != r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlimit := edge.publicLimit()\n\t\t\tif balance, ok := r.localBalances[edge.chanID];\n\t\t\t\tok && balance < limit {\n\n\t\t\t\tlimit = balance\n\t\t\t}\n\t\t\taddLimit(limit)\n\t\t}\n\t}\n\n\tfor key, upper := range r.localUpper {\n\t\tif r.localFails[key] > 0 {\n\t\t\tadd(upper * 55 / 100)\n\t\t\tadd(upper * 72 / 100)\n\t\t\tadd(upper * 84 / 100)\n\t\t}\n\t}\n\n\treturn hints\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tbaseReservations := r.effectiveReservations()\n\n\tshardCap := r.nextShardCap\n\tminParts := 1\n\tif shardCap > 0 {\n\t\tminParts = int(total / shardCap)\n\t\tif total%shardCap != 0 {\n\t\t\tminParts++\n\t\t}\n\t\tif minParts > partLimit {\n\t\t\tminParts = 1\n\t\t\tshardCap = 0\n\t\t}\n\t}\n\n\tseenCounts := make(map[int]bool)\n\tpartCounts := make([]int, 0, 12)\n\tfor _, parts := range []int{\n\t\tminParts, minParts + 1, 1, 2, 3, 4, 5, 6, 8, 12,\n\t\tpartLimit,\n\t} {\n\t\tpartCounts = appendPartCount(\n\t\t\tpartCounts, seenCounts, parts, minParts, partLimit,\n\t\t)\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\tseenAllocations := make(map[string]bool)\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tif len(allocation) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tkey := \"\"\n\t\tfor _, amt := range allocation {\n\t\t\tkey += string(rune(len(key)+1)) + \":\" +\n\t\t\t\tamountKey(amt)\n\t\t}\n\t\tif seenAllocations[key] {\n\t\t\treturn\n\t\t}\n\t\tseenAllocations[key] = true\n\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, baseReservations,\n\t\t\tshardCap, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor _, parts := range partCounts {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\n\t\tconsider(equalAllocation(total, parts))\n\t\tif parts >= 2 && parts <= 6 {\n\t\t\tconsider(shapedAllocation(total, parts, 0))\n\t\t\tconsider(shapedAllocation(total, parts, 1))\n\t\t}\n\t\tif parts >= 3 && parts <= 5 {\n\t\t\tconsider(shapedAllocation(total, parts, 2))\n\t\t\tconsider(shapedAllocation(total, parts, 3))\n\t\t}\n\t}\n\n\tif partLimit >= 2 {\n\t\tfor _, first := range r.splitHints(total) {\n\t\t\tif searches+2 > maxPlanSearches {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsecond := total - first\n\t\t\tconsider([]lnwire.MilliSatoshi{first, second})\n\t\t\tconsider([]lnwire.MilliSatoshi{second, first})\n\t\t}\n\t}\n\n\tif len(best) > 0 {\n\t\treturn best, nil\n\t}\n\treturn nil, errors.New(\"no complete route set found\")\n}\n\nfunc amountKey(v lnwire.MilliSatoshi) string {\n\tif v == 0 {\n\t\treturn \"0\"\n\t}\n\n\tconst digits = \"0123456789\"\n\tvar buf [24]byte\n\ti := len(buf)\n\tn := int64(v)\n\tfor n > 0 {\n\t\ti--\n\t\tbuf[i] = digits[n%10]\n\t\tn /= 10\n\t}\n\treturn string(buf[i:])\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal = addSaturated(total, pr.meta.delivered)\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) reservePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\taddAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\taddSharedHold(r.owner, edge.key, meta.amounts[i])\n\t}\n\tr.pendingFees = addSaturated(r.pendingFees, meta.fee)\n}\n\nfunc (r *candidateRouter) removePending(meta *routeMeta,\n\treleaseShared bool) {\n\n\tfor i, edge := range meta.path {\n\t\tsubtractAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\tif releaseShared {\n\t\t\tsubtractSharedHold(\n\t\t\t\tr.owner, edge.key, meta.amounts[i],\n\t\t\t)\n\t\t}\n\t}\n\n\tif meta.fee >= r.pendingFees {\n\t\tr.pendingFees = 0\n\t} else {\n\t\tr.pendingFees -= meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) cleanup() {\n\tif r.cleaned {\n\t\treturn\n\t}\n\tr.cleaned = true\n\treleaseAllSharedHolds(r.owner)\n}\n\nfunc (r *candidateRouter) refreshPlanMeta(\n\tpr *plannedRoute) bool {\n\n\treservations := r.effectiveReservations()\n\tmeta := pr.meta\n\tif meta == nil || len(meta.path) != len(meta.amounts) {\n\t\treturn false\n\t}\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif !edge.usableHTLC(amount) ||\n\t\t\treservations[edge.key] >\n\t\t\t\tlnwire.MaxMilliSatoshi-amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tload := reservations[edge.key] + amount\n\t\tif load > edge.capacity {\n\t\t\treturn false\n\t\t}\n\n\t\tcutoff := r.retryCutoff(edge.key)\n\t\tif cutoff > 0 && load >= cutoff {\n\t\t\treturn false\n\t\t}\n\n\t\tif edge.from == r.source {\n\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\tif !ok || load > balance {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tmeta.loads[i] = load\n\t}\n\n\tbudget := r.remainingFeeBudget()\n\treturn budget == lnwire.MaxMilliSatoshi || meta.fee <= budget\n}\n\nfunc (r *candidateRouter) fallbackRoute(\n\ttotal lnwire.MilliSatoshi, slots uint32) (*plannedRoute, error) {\n\n\tif slots <= 1 {\n\t\treturn r.findRoute(\n\t\t\ttotal, r.effectiveReservations(),\n\t\t\tr.remainingFeeBudget(),\n\t\t)\n\t}\n\n\tcandidates := make([]lnwire.MilliSatoshi, 0, 32)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tadd := func(v lnwire.MilliSatoshi) {\n\t\tif v <= 0 || v >= total || seen[v] {\n\t\t\treturn\n\t\t}\n\t\tseen[v] = true\n\t\tcandidates = append(candidates, v)\n\t}\n\n\tadd(total * 7 / 8)\n\tadd(total * 4 / 5)\n\tadd(total * 3 / 4)\n\tadd(total * 2 / 3)\n\tadd(total / 2)\n\tadd(total / lnwire.MilliSatoshi(slots))\n\tfor _, hint := range r.splitHints(total) {\n\t\tadd(hint)\n\t}\n\n\tfor len(candidates) > 0 {\n\t\tlargest := 0\n\t\tfor i := 1; i < len(candidates); i++ {\n\t\t\tif candidates[i] > candidates[largest] {\n\t\t\t\tlargest = i\n\t\t\t}\n\t\t}\n\n\t\tshard := candidates[largest]\n\t\tcandidates = append(\n\t\t\tcandidates[:largest], candidates[largest+1:]...,\n\t\t)\n\n\t\tpr, err := r.findRoute(\n\t\t\tshard, r.effectiveReservations(),\n\t\t\tr.remainingFeeBudget(),\n\t\t)\n\t\tif err == nil {\n\t\t\treturn pr, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no feasible partial route\")\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.initialAmount == 0 {\n\t\tr.initialAmount = amt\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.misses >= maxConsecutiveMisses {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) > 0 && !r.refreshPlanMeta(r.plan[0]) {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err == nil {\n\t\t\tr.plan = plan\n\t\t} else {\n\t\t\tfallback, fallbackErr := r.fallbackRoute(amt, slots)\n\t\t\tif fallbackErr != nil {\n\t\t\t\tr.cleanup()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tr.plan = []*plannedRoute{fallback}\n\t\t}\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tif !r.refreshPlanMeta(next) {\n\t\tr.plan = nil\n\t\tfallback, err := r.fallbackRoute(amt, slots)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t\tnext = fallback\n\t}\n\n\tr.issued[next.rt] = next.meta\n\tr.reservePending(next.meta)\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.effectiveReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif amount > lnwire.MaxMilliSatoshi-reservations[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt <= 1 || r.misses < 2 {\n\t\treturn\n\t}\n\n\tvar numerator int64 = 4\n\tvar denominator int64 = 5\n\tif r.misses >= 5 {\n\t\tnumerator = 2\n\t\tdenominator = 3\n\t}\n\tif r.misses >= 9 {\n\t\tnumerator = 1\n\t\tdenominator = 2\n\t}\n\n\tcap := amt * lnwire.MilliSatoshi(numerator) /\n\t\tlnwire.MilliSatoshi(denominator)\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizeWholePath(\n\tmeta *routeMeta, penalty float64) {\n\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += penalty\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tr.removePending(meta, false)\n\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.72, now,\n\t\t\t)\n\t\t\taddAmount(r.reserved, edge.key, meta.amounts[i])\n\t\t\tr.edgePenalty[edge.key] *= 0.12\n\t\t\tdelete(r.liquidityReports, edge.key)\n\t\t\tdelete(r.policyReports, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees = addSaturated(r.committedFees, meta.fee)\n\t\tr.succeededAmount = addSaturated(\n\t\t\tr.succeededAmount, meta.delivered,\n\t\t)\n\t\tr.misses = 0\n\t\tr.nextShardCap = 0\n\n\t\tif r.initialAmount > 0 &&\n\t\t\tr.succeededAmount >= r.initialAmount {\n\n\t\t\tr.cleanup()\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.removePending(meta, true)\n\tr.plan = nil\n\tr.misses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.penalizeWholePath(meta, 120_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeWholePath(meta, 120_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tearliestPossible := failIdx - 1\n\tif earliestPossible < 0 {\n\t\tearliestPossible = 0\n\t}\n\tfor i := 0; i < earliestPossible; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.15, now,\n\t\t)\n\t}\n\n\tr.penalizeWholePath(meta, 40_000)\n\n\twindowStart := failIdx - 1\n\tif windowStart < 0 {\n\t\twindowStart = 0\n\t}\n\twindowEnd := failIdx + 1\n\tif windowEnd >= len(meta.path) {\n\t\twindowEnd = len(meta.path) - 1\n\t}\n\n\tfor i := windowStart; i <= windowEnd; i++ {\n\t\tpenalty := float64(260_000)\n\t\tif i == failIdx {\n\t\t\tpenalty = 620_000\n\t\t}\n\t\tr.edgePenalty[meta.path[i].key] += penalty\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.liquidityReports[edge.key]++\n\t\tcount := r.liquidityReports[edge.key]\n\n\t\tweight := 0.045\n\t\tif count >= 2 {\n\t\t\tweight = 0.22\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\t\tif count >= 3 {\n\t\t\tweight = 0.38\n\t\t}\n\t\tif count >= 5 {\n\t\t\tweight = 0.52\n\t\t}\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t550_000 + float64(count)*280_000\n\t\tupdateBelief(\n\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t)\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyReports[edge.key]++\n\t\tcount := r.policyReports[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_800_000 + float64(count)*700_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.edgePenalty[edge.key] += 500_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 32,
"parent": 8,
"score": 0.1922,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\triskPriceMsat = float64(420_000)\n\thopCostMsat = float64(240_000)\n\treusedEdgeCost = float64(3_200_000)\n\textraPartCostMsat = float64(80_000)\n\n\tmaxSearchHops = 18\n\tmaxLabelsPerNode = 14\n\tmaxPlanSearches = 220\n\tmaxPlannedParts = 24\n\tmaxRouterAttempts = 44\n\tmaxConsecutiveMisses = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedState = struct {\n\tsync.Mutex\n\tbeliefs map[edgeKey]liquidityBelief\n\tholds map[edgeKey]map[uint64]lnwire.MilliSatoshi\n\tnextOwner uint64\n}{\n\tbeliefs: make(map[edgeKey]liquidityBelief),\n\tholds: make(map[edgeKey]map[uint64]lnwire.MilliSatoshi),\n}\n\nfunc allocateOwner() uint64 {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tsharedState.nextOwner++\n\tif sharedState.nextOwner == 0 {\n\t\tsharedState.nextOwner++\n\t}\n\treturn sharedState.nextOwner\n}\n\nfunc addSaturated(a, b lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif b > lnwire.MaxMilliSatoshi-a {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\treturn a + b\n}\n\nfunc addSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\towners = make(map[uint64]lnwire.MilliSatoshi)\n\t\tsharedState.holds[key] = owners\n\t}\n\towners[owner] = addSaturated(owners[owner], amt)\n}\n\nfunc subtractSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\treturn\n\t}\n\n\told := owners[owner]\n\tif amt >= old {\n\t\tdelete(owners, owner)\n\t} else {\n\t\towners[owner] = old - amt\n\t}\n\tif len(owners) == 0 {\n\t\tdelete(sharedState.holds, key)\n\t}\n}\n\nfunc sharedHoldsExcept(owner uint64) map[edgeKey]lnwire.MilliSatoshi {\n\tresult := make(map[edgeKey]lnwire.MilliSatoshi)\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tvar total lnwire.MilliSatoshi\n\t\tfor other, amt := range owners {\n\t\t\tif other == owner {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotal = addSaturated(total, amt)\n\t\t}\n\t\tif total > 0 {\n\t\t\tresult[key] = total\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc releaseAllSharedHolds(owner uint64) {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tdelete(owners, owner)\n\t\tif len(owners) == 0 {\n\t\t\tdelete(sharedState.holds, key)\n\t\t}\n\t}\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc (e *candidateEdge) publicLimit() lnwire.MilliSatoshi {\n\tlimit := e.capacity\n\tif e.maxHTLC > 0 && e.maxHTLC < limit {\n\t\tlimit = e.maxHTLC\n\t}\n\treturn limit\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\tif int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\towner uint64\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpendingReserved map[edgeKey]lnwire.MilliSatoshi\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquidityReports map[edgeKey]int\n\tpolicyReports map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\tnextShardCap lnwire.MilliSatoshi\n\n\tinitialAmount lnwire.MilliSatoshi\n\tsucceededAmount lnwire.MilliSatoshi\n\tattempts int\n\tmisses int\n\tcleaned bool\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\towner: allocateOwner(),\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpendingReserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquidityReports: make(map[edgeKey]int),\n\t\tpolicyReports: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedState.Lock()\n\tb, ok := sharedState.beliefs[key]\n\tsharedState.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tb := sharedState.beliefs[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedState.beliefs[key] = b\n}\n\ntype flowShift struct {\n\tup lnwire.MilliSatoshi\n\tdown lnwire.MilliSatoshi\n\tcap lnwire.MilliSatoshi\n}\n\nfunc applySettledFlows(flows map[edgeKey]lnwire.MilliSatoshi,\n\tlookup map[edgeKey]*candidateEdge, now time.Time) {\n\n\tshifts := make(map[edgeKey]flowShift)\n\n\tfor key, amt := range flows {\n\t\tif amt <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tedge := lookup[key]\n\t\tif edge == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\ts := shifts[key]\n\t\ts.down = addSaturated(s.down, amt)\n\t\ts.cap = edge.capacity\n\t\tshifts[key] = s\n\n\t\treverse := edgeKey{\n\t\t\tchanID: key.chanID,\n\t\t\tfrom: key.to,\n\t\t\tto: key.from,\n\t\t}\n\t\treverseEdge := lookup[reverse]\n\t\tif reverseEdge == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\ts = shifts[reverse]\n\t\ts.up = addSaturated(s.up, amt)\n\t\ts.cap = reverseEdge.capacity\n\t\tshifts[reverse] = s\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, shift := range shifts {\n\t\tb, exists := sharedState.beliefs[key]\n\t\tif exists {\n\t\t\tb.conf = beliefConfidence(b, now)\n\t\t}\n\n\t\tif shift.up >= shift.down {\n\t\t\tdelta := shift.up - shift.down\n\t\t\tif delta > 0 {\n\t\t\t\tb.lowerOK = addSaturated(b.lowerOK, delta)\n\t\t\t\tif b.lowerOK > shift.cap {\n\t\t\t\t\tb.lowerOK = shift.cap\n\t\t\t\t}\n\t\t\t\tif b.upperBad > 0 {\n\t\t\t\t\tb.upperBad = addSaturated(b.upperBad, delta)\n\t\t\t\t\tif b.upperBad > shift.cap {\n\t\t\t\t\t\tb.upperBad = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !exists && delta > 0 {\n\t\t\t\t\tb.conf = 0.65\n\t\t\t\t} else if b.conf < 0.65 {\n\t\t\t\t\tb.conf = 0.65\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tdelta := shift.down - shift.up\n\t\t\tif b.lowerOK > delta {\n\t\t\t\tb.lowerOK -= delta\n\t\t\t} else {\n\t\t\t\tb.lowerOK = 0\n\t\t\t}\n\t\t\tif b.upperBad > delta {\n\t\t\t\tb.upperBad -= delta\n\t\t\t} else if b.upperBad > 0 {\n\t\t\t\tb.upperBad = 1\n\t\t\t}\n\t\t}\n\n\t\tif exists || shift.up > shift.down {\n\t\t\tb.updated = now\n\t\t\tsharedState.beliefs[key] = b\n\t\t}\n\t}\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc addAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > 0 {\n\t\tdst[key] = addSaturated(dst[key], amt)\n\t}\n}\n\nfunc subtractAmount(dst map[edgeKey]lnwire.MilliSatoshi,\n\tkey edgeKey, amt lnwire.MilliSatoshi) {\n\n\told := dst[key]\n\tif amt >= old {\n\t\tdelete(dst, key)\n\t\treturn\n\t}\n\tdst[key] = old - amt\n}\n\nfunc (r *candidateRouter) effectiveReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\n\tfor key, amt := range r.pendingReserved {\n\t\taddAmount(result, key, amt)\n\t}\n\tfor key, amt := range sharedHoldsExcept(r.owner) {\n\t\taddAmount(result, key, amt)\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := addSaturated(r.committedFees, r.pendingFees)\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tswitch {\n\tcase r.localFails[key] >= 6:\n\t\treturn upper * 48 / 100\n\tcase r.localFails[key] >= 4:\n\t\treturn upper * 60 / 100\n\tcase r.localFails[key] >= 3:\n\t\treturn upper * 70 / 100\n\tcase r.localFails[key] >= 2:\n\t\treturn upper * 84 / 100\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc (r *candidateRouter) dynamicPenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tupper := r.localUpper[key]\n\tif upper > 0 {\n\t\tif load < upper*2/3 {\n\t\t\tpenalty *= 0.08\n\t\t} else if load < upper*4/5 {\n\t\t\tpenalty *= 0.28\n\t\t}\n\t}\n\treturn penalty\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\tif feeCap == 0 && r.spec.FeeLimitMsat != lnwire.MaxMilliSatoshi {\n\t\tfeeCap = 0\n\t}\n\n\tnow := r.view.Now()\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tvar nodeCharge lnwire.MilliSatoshi\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.998\n\t\t\t\tif r.localFails[edge.key] > 0 {\n\t\t\t\t\tp = 0.82\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\triskPriceMsat*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.dynamicPenalty(edge.key, totalLoad)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCost\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.40\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tvar allocated lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tvar shard lnwire.MilliSatoshi\n\t\tif float64(total)*float64(weight) > float64(math.MaxInt64) {\n\t\t\tshard = lnwire.MilliSatoshi(\n\t\t\t\tfloat64(total) * float64(weight) /\n\t\t\t\t\tfloat64(weightTotal),\n\t\t\t)\n\t\t} else {\n\t\t\tshard = total * lnwire.MilliSatoshi(weight) /\n\t\t\t\tlnwire.MilliSatoshi(weightTotal)\n\t\t}\n\n\t\tif shard <= 0 {\n\t\t\tshard = 1\n\t\t}\n\t\tif i == len(weights)-1 {\n\t\t\tshard = total - allocated\n\t\t}\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-allocated {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tresult[i] = shard\n\t\tallocated += shard\n\t}\n\n\tif allocated != total {\n\t\treturn nil\n\t}\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc shapedAllocation(total lnwire.MilliSatoshi,\n\tparts, mode int) []lnwire.MilliSatoshi {\n\n\tif parts < 2 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tswitch mode {\n\t\tcase 0:\n\t\t\tweights[i] = 1\n\t\t\tweights[0] = 2\n\t\tcase 1:\n\t\t\tweights[i] = 1\n\t\t\tweights[len(weights)-1] = 2\n\t\tcase 2:\n\t\t\tweights[i] = int64(parts - i)\n\t\tcase 3:\n\t\t\tweights[i] = int64(i + 1)\n\t\tcase 4:\n\t\t\tweights[i] = int64((parts-i)*(parts-i))\n\t\tcase 5:\n\t\t\tweights[i] = int64((i + 1) * (i + 1))\n\t\t}\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\taddAmount(dst, edge.key, meta.amounts[i])\n\t}\n}\n\nfunc planningFeeCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\n\tshare := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif share <= 0 {\n\t\tshare = 1\n\t}\n\n\tcap := addSaturated(share, share)\n\tcap = addSaturated(cap, share/2)\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\treturn cap\n}\n\nfunc allocationAmount(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc allocationAllowed(allocation []lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi) bool {\n\n\tif shardCap <= 0 {\n\t\treturn true\n\t}\n\tfor _, shard := range allocation {\n\t\tif shard > shardCap {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches ||\n\t\t!allocationAllowed(allocation, shardCap) {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationAmount(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\tvar totalScore float64\n\n\tfor i, shard := range allocation {\n\t\tfeeCap := remainingBudget\n\t\tif i != len(allocation)-1 {\n\t\t\tfeeCap = planningFeeCap(\n\t\t\t\tremainingBudget, shard, remainingAmount,\n\t\t\t)\n\t\t}\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\ttotalScore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\ttotalScore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, totalScore, true\n}\n\nfunc (r *candidateRouter) splitHints(\n\ttotal lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\thints := make([]lnwire.MilliSatoshi, 0, 64)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\tadd := func(v lnwire.MilliSatoshi) {\n\t\tif v <= 0 || v >= total || seen[v] {\n\t\t\treturn\n\t\t}\n\t\tseen[v] = true\n\t\thints = append(hints, v)\n\t}\n\n\tfor ratio := int64(1); ratio <= 9; ratio++ {\n\t\tadd(total * lnwire.MilliSatoshi(ratio) / 10)\n\t}\n\tfor _, denominator := range []int64{3, 4, 5, 6, 8, 12, 16, 24} {\n\t\tadd(total / lnwire.MilliSatoshi(denominator))\n\t\tadd(total - total/lnwire.MilliSatoshi(denominator))\n\t}\n\n\taddLimit := func(limit lnwire.MilliSatoshi) {\n\t\tif limit <= 0 {\n\t\t\treturn\n\t\t}\n\t\tfor _, pct := range []int64{45, 58, 68, 77, 85, 92, 97} {\n\t\t\tadd(limit * lnwire.MilliSatoshi(pct) / 100)\n\t\t}\n\t}\n\n\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\taddLimit(edge.publicLimit())\n\t}\n\n\tfor _, edges := range r.incomingEdges {\n\t\tfor _, edge := range edges {\n\t\t\tif edge.from != r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlimit := edge.publicLimit()\n\t\t\tif balance, ok := r.localBalances[edge.chanID];\n\t\t\t\tok && balance < limit {\n\n\t\t\t\tlimit = balance\n\t\t\t}\n\t\t\taddLimit(limit)\n\t\t}\n\t}\n\n\tfor key, upper := range r.localUpper {\n\t\tif r.localFails[key] > 0 {\n\t\t\tadd(upper * 45 / 100)\n\t\t\tadd(upper * 58 / 100)\n\t\t\tadd(upper * 70 / 100)\n\t\t\tadd(upper * 82 / 100)\n\t\t}\n\t}\n\n\tif r.nextShardCap > 0 {\n\t\tadd(r.nextShardCap)\n\t\tadd(r.nextShardCap * 7 / 8)\n\t\tadd(r.nextShardCap * 3 / 4)\n\t\tadd(r.nextShardCap * 2 / 3)\n\t}\n\n\treturn hints\n}\n\nfunc amountKey(v lnwire.MilliSatoshi) string {\n\tif v == 0 {\n\t\treturn \"0\"\n\t}\n\n\tconst digits = \"0123456789\"\n\tvar buf [24]byte\n\ti := len(buf)\n\tn := int64(v)\n\tfor n > 0 {\n\t\ti--\n\t\tbuf[i] = digits[n%10]\n\t\tn /= 10\n\t}\n\treturn string(buf[i:])\n}\n\nfunc allocationKey(allocation []lnwire.MilliSatoshi) string {\n\tvar result string\n\tfor _, amt := range allocation {\n\t\tresult += amountKey(amt) + \"/\"\n\t}\n\treturn result\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tbaseReservations := r.effectiveReservations()\n\tshardCap := r.nextShardCap\n\n\tminParts := 1\n\tif shardCap > 0 {\n\t\tminParts = int(total / shardCap)\n\t\tif total%shardCap != 0 {\n\t\t\tminParts++\n\t\t}\n\t\tif minParts > partLimit {\n\t\t\treturn nil, errors.New(\"complete plan needs more parts\")\n\t\t}\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\tseenAllocations := make(map[string]bool)\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tif len(allocation) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tkey := allocationKey(allocation)\n\t\tif seenAllocations[key] {\n\t\t\treturn\n\t\t}\n\t\tseenAllocations[key] = true\n\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, baseReservations,\n\t\t\tshardCap, &searches,\n\t\t)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor parts := minParts; parts <= partLimit; parts++ {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\n\t\tconsider(equalAllocation(total, parts))\n\n\t\tif parts <= 8 {\n\t\t\tconsider(shapedAllocation(total, parts, 0))\n\t\t\tconsider(shapedAllocation(total, parts, 1))\n\t\t}\n\t\tif parts >= 3 && parts <= 6 {\n\t\t\tconsider(shapedAllocation(total, parts, 2))\n\t\t\tconsider(shapedAllocation(total, parts, 3))\n\t\t}\n\t\tif parts >= 3 && parts <= 5 {\n\t\t\tconsider(shapedAllocation(total, parts, 4))\n\t\t\tconsider(shapedAllocation(total, parts, 5))\n\t\t}\n\t}\n\n\tif partLimit >= 2 {\n\t\tfor _, first := range r.splitHints(total) {\n\t\t\tif searches+2 > maxPlanSearches {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsecond := total - first\n\t\t\tconsider([]lnwire.MilliSatoshi{first, second})\n\t\t\tconsider([]lnwire.MilliSatoshi{second, first})\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no complete route set found\")\n\t}\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal = addSaturated(total, pr.meta.delivered)\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) reservePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\taddAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\taddSharedHold(r.owner, edge.key, meta.amounts[i])\n\t}\n\tr.pendingFees = addSaturated(r.pendingFees, meta.fee)\n}\n\nfunc (r *candidateRouter) removePending(meta *routeMeta,\n\treleaseShared bool) {\n\n\tfor i, edge := range meta.path {\n\t\tsubtractAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\tif releaseShared {\n\t\t\tsubtractSharedHold(\n\t\t\t\tr.owner, edge.key, meta.amounts[i],\n\t\t\t)\n\t\t}\n\t}\n\n\tif meta.fee >= r.pendingFees {\n\t\tr.pendingFees = 0\n\t} else {\n\t\tr.pendingFees -= meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) cleanup() {\n\tif r.cleaned {\n\t\treturn\n\t}\n\tr.cleaned = true\n\treleaseAllSharedHolds(r.owner)\n}\n\nfunc (r *candidateRouter) refreshPlanMeta(\n\tpr *plannedRoute) bool {\n\n\treservations := r.effectiveReservations()\n\tmeta := pr.meta\n\tif meta == nil || len(meta.path) != len(meta.amounts) {\n\t\treturn false\n\t}\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif !edge.usableHTLC(amount) ||\n\t\t\treservations[edge.key] >\n\t\t\t\tlnwire.MaxMilliSatoshi-amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tload := reservations[edge.key] + amount\n\t\tif load > edge.capacity {\n\t\t\treturn false\n\t\t}\n\n\t\tcutoff := r.retryCutoff(edge.key)\n\t\tif cutoff > 0 && load >= cutoff {\n\t\t\treturn false\n\t\t}\n\n\t\tif edge.from == r.source {\n\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\tif !ok || load > balance {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tmeta.loads[i] = load\n\t}\n\n\tbudget := r.remainingFeeBudget()\n\treturn budget == lnwire.MaxMilliSatoshi || meta.fee <= budget\n}\n\nfunc (r *candidateRouter) fallbackRoute(\n\ttotal lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tcandidates := make([]lnwire.MilliSatoshi, 0, 96)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\tadd := func(v lnwire.MilliSatoshi) {\n\t\tif v <= 0 || v > total {\n\t\t\treturn\n\t\t}\n\t\tif r.nextShardCap > 0 && v > r.nextShardCap {\n\t\t\treturn\n\t\t}\n\t\tif seen[v] {\n\t\t\treturn\n\t\t}\n\t\tseen[v] = true\n\t\tcandidates = append(candidates, v)\n\t}\n\n\tadd(total)\n\tfor _, numerator := range []int64{\n\t\t19, 18, 17, 16, 15, 14, 13, 12, 11, 10,\n\t\t9, 8, 7, 6, 5, 4, 3, 2, 1,\n\t} {\n\t\tadd(total * lnwire.MilliSatoshi(numerator) / 20)\n\t}\n\tfor _, denominator := range []int64{\n\t\t3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 48,\n\t} {\n\t\tadd(total / lnwire.MilliSatoshi(denominator))\n\t}\n\tfor _, hint := range r.splitHints(total) {\n\t\tadd(hint)\n\t}\n\tif r.nextShardCap > 0 {\n\t\tadd(r.nextShardCap)\n\t\tadd(r.nextShardCap * 9 / 10)\n\t\tadd(r.nextShardCap * 4 / 5)\n\t\tadd(r.nextShardCap * 2 / 3)\n\t\tadd(r.nextShardCap / 2)\n\t}\n\n\treservations := r.effectiveReservations()\n\tfeeBudget := r.remainingFeeBudget()\n\n\tfor len(candidates) > 0 {\n\t\tlargest := 0\n\t\tfor i := 1; i < len(candidates); i++ {\n\t\t\tif candidates[i] > candidates[largest] {\n\t\t\t\tlargest = i\n\t\t\t}\n\t\t}\n\n\t\tshard := candidates[largest]\n\t\tcandidates = append(\n\t\t\tcandidates[:largest], candidates[largest+1:]...,\n\t\t)\n\n\t\tfeeCap := planningFeeCap(feeBudget, shard, total)\n\t\tpr, err := r.findRoute(shard, reservations, feeCap)\n\t\tif err == nil {\n\t\t\treturn pr, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no feasible partial route\")\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.initialAmount == 0 {\n\t\tr.initialAmount = amt\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.misses >= maxConsecutiveMisses {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\tif len(r.plan) > 0 && !r.refreshPlanMeta(r.plan[0]) {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err == nil {\n\t\t\tr.plan = plan\n\t\t} else {\n\t\t\tfallback, fallbackErr := r.fallbackRoute(amt)\n\t\t\tif fallbackErr != nil {\n\t\t\t\tr.cleanup()\n\t\t\t\treturn nil, fallbackErr\n\t\t\t}\n\t\t\tr.plan = []*plannedRoute{fallback}\n\t\t}\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\n\tif !r.refreshPlanMeta(next) {\n\t\tr.plan = nil\n\t\tfallback, err := r.fallbackRoute(amt)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t\tnext = fallback\n\t}\n\n\tr.issued[next.rt] = next.meta\n\tr.reservePending(next.meta)\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.effectiveReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif amount > lnwire.MaxMilliSatoshi-reservations[key] {\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerFutureShard(\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tvar numerator int64 = 9\n\tvar denominator int64 = 10\n\n\tswitch {\n\tcase r.misses >= 16:\n\t\tnumerator, denominator = 2, 5\n\tcase r.misses >= 10:\n\t\tnumerator, denominator = 1, 2\n\tcase r.misses >= 6:\n\t\tnumerator, denominator = 2, 3\n\tcase r.misses >= 3:\n\t\tnumerator, denominator = 4, 5\n\t}\n\n\tcap := amt * lnwire.MilliSatoshi(numerator) /\n\t\tlnwire.MilliSatoshi(denominator)\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizeWholePath(\n\tmeta *routeMeta, totalPenalty float64) {\n\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := totalPenalty / float64(len(meta.path))\n\tfor _, edge := range meta.path {\n\t\tr.edgePenalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tr.removePending(meta, false)\n\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.80, now,\n\t\t\t)\n\t\t\taddAmount(r.reserved, edge.key, meta.amounts[i])\n\t\t\tr.edgePenalty[edge.key] *= 0.08\n\t\t\tdelete(r.liquidityReports, edge.key)\n\t\t\tdelete(r.policyReports, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\tr.committedFees = addSaturated(r.committedFees, meta.fee)\n\t\tr.succeededAmount = addSaturated(\n\t\t\tr.succeededAmount, meta.delivered,\n\t\t)\n\t\tr.misses = 0\n\t\tr.nextShardCap = 0\n\n\t\tif r.initialAmount > 0 &&\n\t\t\tr.succeededAmount >= r.initialAmount {\n\n\t\t\tapplySettledFlows(r.reserved, r.edgeLookup, now)\n\t\t\tr.cleanup()\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.removePending(meta, true)\n\tr.plan = nil\n\tr.misses++\n\n\tcode := result.Failure.Code()\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\n\tif reportedIdx < 0 {\n\t\tr.penalizeWholePath(meta, 480_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\tr.penalizeWholePath(meta, 480_000)\n\t\tr.lowerFutureShard(meta.delivered)\n\t\treturn nil\n\t}\n\n\t/*\n\t\tA shifted attribution can move by one hop. Therefore every edge at\n\t\tleast two positions before the report definitely carried the HTLC.\n\t*/\n\tconfirmedPrefix := failIdx - 1\n\tfor i := 0; i < confirmedPrefix; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i], true, 0.55, now,\n\t\t)\n\t\tr.edgePenalty[meta.path[i].key] *= 0.45\n\t}\n\n\tr.penalizeWholePath(meta, 260_000)\n\n\twindowStart := failIdx - 1\n\tif windowStart < 0 {\n\t\twindowStart = 0\n\t}\n\twindowEnd := failIdx + 1\n\tif windowEnd >= len(meta.path) {\n\t\twindowEnd = len(meta.path) - 1\n\t}\n\n\tfor i := windowStart; i <= windowEnd; i++ {\n\t\tpenalty := float64(380_000)\n\t\tif i == failIdx {\n\t\t\tpenalty = 900_000\n\t\t}\n\t\tr.edgePenalty[meta.path[i].key] += penalty\n\t}\n\n\tedge := meta.path[failIdx]\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.liquidityReports[edge.key]++\n\t\tcount := r.liquidityReports[edge.key]\n\n\t\tweight := 0.035\n\t\tif count >= 2 {\n\t\t\tweight = 0.20\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\t\tif count >= 3 {\n\t\t\tweight = 0.36\n\t\t}\n\t\tif count >= 5 {\n\t\t\tweight = 0.50\n\t\t}\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t650_000 + float64(count)*360_000\n\t\tupdateBelief(\n\t\t\tedge.key, meta.loads[failIdx], false, weight, now,\n\t\t)\n\n\t\t/*\n\t\t\tAdjacent edges are plausible under shifted attribution. Give\n\t\t\tthem only a soft exploration penalty; never write a hard bound\n\t\t\twithout repeated direct corroboration.\n\t\t*/\n\t\tfor _, adjacent := range []int{failIdx - 1, failIdx + 1} {\n\t\t\tif adjacent < 0 || adjacent >= len(meta.path) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.edgePenalty[meta.path[adjacent].key] += 180_000\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyReports[edge.key]++\n\t\tcount := r.policyReports[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_200_000 + float64(count)*900_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.lowerFutureShard(meta.delivered)\n\t\tr.edgePenalty[edge.key] += 650_000\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 33,
"parent": 7,
"score": 0.2965,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(45_000)\n\n\thopCostMsat = float64(75_000)\n\treusedEdgeCostMsat = float64(1_900_000)\n\textraPartCostMsat = float64(65_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 7\n\tmaxPlanBeam = 14\n\tmaxRouterAttempts = 32\n\tmaxConsecutiveMisses = 24\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\theld []*routeMeta\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]float64\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]float64),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := make([]*searchState, 0, len(old)+1)\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.80\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 7 / 8\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 3\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc removeReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif dst[edge.key] <= amount {\n\t\t\tdelete(dst, edge.key)\n\t\t} else {\n\t\t\tdst[edge.key] -= amount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) syncHeld(inFlight uint32) bool {\n\twanted := int(inFlight)\n\tif wanted >= len(r.held) {\n\t\treturn false\n\t}\n\n\tdrop := len(r.held) - wanted\n\tfor i := 0; i < drop; i++ {\n\t\tremoveReservationSet(r.reserved, r.held[i])\n\t}\n\n\tr.held = append([]*routeMeta(nil), r.held[drop:]...)\n\treturn true\n}\n\nfunc candidatePartCounts(slots uint32) []int {\n\tif slots == 0 {\n\t\treturn nil\n\t}\n\n\tmaxParts := int(slots)\n\tseen := make(map[int]bool)\n\tresult := make([]int, 0, 8)\n\n\tadd := func(parts int) {\n\t\tif parts < 1 || parts > maxParts || seen[parts] {\n\t\t\treturn\n\t\t}\n\t\tseen[parts] = true\n\t\tresult = append(result, parts)\n\t}\n\n\tadd(1)\n\tadd(2)\n\tadd(3)\n\tadd(4)\n\tadd(6)\n\tadd(8)\n\tif maxParts > 8 {\n\t\tadd((maxParts + 1) / 2)\n\t}\n\tadd(maxParts)\n\n\treturn result\n}\n\nfunc shardChoices(remaining lnwire.MilliSatoshi,\n\tpartsLeft int) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{remaining}\n\t}\n\n\tfair := (remaining + lnwire.MilliSatoshi(partsLeft) - 1) /\n\t\tlnwire.MilliSatoshi(partsLeft)\n\tminLater := lnwire.MilliSatoshi(partsLeft - 1)\n\n\traw := []lnwire.MilliSatoshi{\n\t\tfair,\n\t\tfair * 3 / 4,\n\t\tfair * 5 / 4,\n\t\tfair / 2,\n\t\tfair * 3 / 2,\n\t\tfair / 3,\n\t\tfair * 2,\n\t}\n\n\tif partsLeft == 2 {\n\t\traw = append(raw, remaining/3, remaining*2/3)\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tresult := make([]lnwire.MilliSatoshi, 0, len(raw))\n\n\tfor _, shard := range raw {\n\t\tif shard <= 0 || shard >= remaining {\n\t\t\tcontinue\n\t\t}\n\t\tif remaining-shard < minLater || seen[shard] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[shard] = true\n\t\tresult = append(result, shard)\n\t}\n\n\treturn result\n}\n\ntype planState struct {\n\tplan []*plannedRoute\n\treservations map[edgeKey]lnwire.MilliSatoshi\n\tfeeLeft lnwire.MilliSatoshi\n\tremaining lnwire.MilliSatoshi\n\tscore float64\n}\n\nfunc projectedPlanScore(state *planState,\n\ttotal lnwire.MilliSatoshi) float64 {\n\n\tdelivered := total - state.remaining\n\tif delivered <= 0 {\n\t\treturn math.Inf(1)\n\t}\n\n\treturn state.score * float64(total) / float64(delivered)\n}\n\nfunc (r *candidateRouter) beamPlan(total lnwire.MilliSatoshi,\n\tparts int, feeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil, 0, false\n\t}\n\n\tstates := []*planState{{\n\t\treservations: cloneReservations(r.reserved),\n\t\tfeeLeft: feeBudget,\n\t\tremaining: total,\n\t}}\n\n\tfor index := 0; index < parts; index++ {\n\t\tpartsLeft := parts - index\n\t\tnextStates := make([]*planState, 0, len(states)*5)\n\n\t\tfor _, state := range states {\n\t\t\tfor _, shard := range shardChoices(\n\t\t\t\tstate.remaining, partsLeft,\n\t\t\t) {\n\t\t\t\tpr, err := r.findRoute(\n\t\t\t\t\tshard, state.reservations, state.feeLeft,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfeeLeft := state.feeLeft\n\t\t\t\tif feeLeft != lnwire.MaxMilliSatoshi {\n\t\t\t\t\tif pr.meta.fee > feeLeft {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfeeLeft -= pr.meta.fee\n\t\t\t\t}\n\n\t\t\t\treservations := cloneReservations(\n\t\t\t\t\tstate.reservations,\n\t\t\t\t)\n\t\t\t\taddReservationSet(reservations, pr.meta)\n\n\t\t\t\tplan := append(\n\t\t\t\t\tappend([]*plannedRoute(nil), state.plan...), pr,\n\t\t\t\t)\n\t\t\t\tscore := state.score + pr.score\n\t\t\t\tif len(state.plan) > 0 {\n\t\t\t\t\tscore += extraPartCostMsat\n\t\t\t\t}\n\n\t\t\t\tnextStates = append(nextStates, &planState{\n\t\t\t\t\tplan: plan,\n\t\t\t\t\treservations: reservations,\n\t\t\t\t\tfeeLeft: feeLeft,\n\t\t\t\t\tremaining: state.remaining - shard,\n\t\t\t\t\tscore: score,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(nextStates) == 0 {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tsort.Slice(nextStates, func(i, j int) bool {\n\t\t\treturn projectedPlanScore(nextStates[i], total) <\n\t\t\t\tprojectedPlanScore(nextStates[j], total)\n\t\t})\n\t\tif len(nextStates) > maxPlanBeam {\n\t\t\tnextStates = nextStates[:maxPlanBeam]\n\t\t}\n\t\tstates = nextStates\n\t}\n\n\tvar best *planState\n\tfor _, state := range states {\n\t\tif state.remaining != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif best == nil || state.score < best.score {\n\t\t\tbest = state\n\t\t}\n\t}\n\tif best == nil {\n\t\treturn nil, 0, false\n\t}\n\n\treturn best.plan, best.score, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range candidatePartCounts(slots) {\n\t\tplan, score, ok := r.beamPlan(total, parts, feeBudget)\n\t\tif ok && score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tif r.syncHeld(inFlightHtlcs) {\n\t\tr.plan = nil\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.78, now,\n\t\t\t)\n\t\t\tr.edgePenalty[edge.key] *= 0.15\n\t\t\tr.liquiditySuspects[edge.key] *= 0.20\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\taddReservationSet(r.reserved, meta)\n\t\tr.held = append(r.held, meta)\n\t\tr.committedFees += meta.fee\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\tcode := result.Failure.Code()\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key] += 1\n\t\tsuspicion := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + suspicion*400_000\n\n\t\tif failIdx > 0 {\n\t\t\tneighbor := meta.path[failIdx-1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\t\tif failIdx+1 < len(meta.path) {\n\t\t\tneighbor := meta.path[failIdx+1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\n\t\tif suspicion >= 1.8 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.42, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(edge.key, meta.loads[failIdx])\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_700_000 + float64(count)*1_100_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 700_000\n\t}\n\n\treturn nil\n}"
},
"role": "best"
},
{
"id": 34,
"parent": 8,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(380_000)\n\tminRiskPriceMsat = float64(12_000)\n\thopCostMsat = float64(105_000)\n\treuseCostMsat = float64(850_000)\n\textraPartCostMsat = float64(28_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 14\n\tmaxPlanSearches = 260\n\tmaxPlannedParts = 16\n\tmaxRouterAttempts = 40\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedState = struct {\n\tsync.Mutex\n\tbeliefs map[edgeKey]liquidityBelief\n\tholds map[edgeKey]map[uint64]lnwire.MilliSatoshi\n\tnextOwner uint64\n}{\n\tbeliefs: make(map[edgeKey]liquidityBelief),\n\tholds: make(map[edgeKey]map[uint64]lnwire.MilliSatoshi),\n}\n\nfunc allocateOwner() uint64 {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tsharedState.nextOwner++\n\tif sharedState.nextOwner == 0 {\n\t\tsharedState.nextOwner++\n\t}\n\treturn sharedState.nextOwner\n}\n\nfunc addSaturated(a, b lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif b > lnwire.MaxMilliSatoshi-a {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\treturn a + b\n}\n\nfunc addAmount(m map[edgeKey]lnwire.MilliSatoshi, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt > 0 {\n\t\tm[key] = addSaturated(m[key], amt)\n\t}\n}\n\nfunc subtractAmount(m map[edgeKey]lnwire.MilliSatoshi, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\told := m[key]\n\tif amt >= old {\n\t\tdelete(m, key)\n\t\treturn\n\t}\n\tm[key] = old - amt\n}\n\nfunc addSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\towners = make(map[uint64]lnwire.MilliSatoshi)\n\t\tsharedState.holds[key] = owners\n\t}\n\towners[owner] = addSaturated(owners[owner], amt)\n}\n\nfunc subtractSharedHold(owner uint64, key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\towners := sharedState.holds[key]\n\tif owners == nil {\n\t\treturn\n\t}\n\n\tif amt >= owners[owner] {\n\t\tdelete(owners, owner)\n\t} else {\n\t\towners[owner] -= amt\n\t}\n\tif len(owners) == 0 {\n\t\tdelete(sharedState.holds, key)\n\t}\n}\n\nfunc sharedHoldsExcept(owner uint64) map[edgeKey]lnwire.MilliSatoshi {\n\tresult := make(map[edgeKey]lnwire.MilliSatoshi)\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tvar total lnwire.MilliSatoshi\n\t\tfor other, amt := range owners {\n\t\t\tif other != owner {\n\t\t\t\ttotal = addSaturated(total, amt)\n\t\t\t}\n\t\t}\n\t\tif total > 0 {\n\t\t\tresult[key] = total\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc releaseSharedHolds(owner uint64) {\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tfor key, owners := range sharedState.holds {\n\t\tdelete(owners, owner)\n\t\tif len(owners) == 0 {\n\t\t\tdelete(sharedState.holds, key)\n\t\t}\n\t}\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) publicLimit() lnwire.MilliSatoshi {\n\tlimit := e.capacity\n\tif e.maxHTLC > 0 && e.maxHTLC < limit {\n\t\tlimit = e.maxHTLC\n\t}\n\treturn limit\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc forwardingFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\tif int64(amt) > math.MaxInt64-outbound {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tbasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tbasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\tif total > int64(lnwire.MaxMilliSatoshi) {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\towner uint64\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\tpendingReserved map[edgeKey]lnwire.MilliSatoshi\n\n\tsuspicion map[edgeKey]float64\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tpenalty map[edgeKey]float64\n\tpolicyBlame map[edgeKey]float64\n\tblocked map[edgeKey]bool\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\tnextShardCap lnwire.MilliSatoshi\n\n\tcommittedFees lnwire.MilliSatoshi\n\tpendingFees lnwire.MilliSatoshi\n\n\tinitialAmount lnwire.MilliSatoshi\n\tsucceededAmount lnwire.MilliSatoshi\n\tattempts int\n\tmisses int\n\tcleaned bool\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\towner: allocateOwner(),\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpendingReserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tsuspicion: make(map[edgeKey]float64),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tpenalty: make(map[edgeKey]float64),\n\t\tpolicyBlame: make(map[edgeKey]float64),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.004\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlow := 0.49 * math.Exp(-x/0.045)\n\thigh := 0.49 / (1 + math.Exp((x-0.965)/0.026))\n\tp := 0.004 + low + high\n\n\tif p < 0.004 {\n\t\treturn 0.004\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\treturn p\n}\n\nfunc decayedConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\treturn b.conf * math.Exp(-age.Seconds()/2700)\n}\n\nfunc channelProbability(key edgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedState.Lock()\n\tb, ok := sharedState.beliefs[key]\n\tsharedState.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := decayedConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.996\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.018\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\tposition := float64(b.upperBad-amt) /\n\t\t\tfloat64(b.upperBad-b.lowerOK)\n\t\tif position < 0 {\n\t\t\tposition = 0\n\t\t}\n\t\tif position > 1 {\n\t\t\tposition = 1\n\t\t}\n\t\tevidence = 0.018 + 0.978*position\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tif p < 0.006 {\n\t\treturn 0.006\n\t}\n\tif p > 0.996 {\n\t\treturn 0.996\n\t}\n\treturn p\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi,\n\tsuccess bool, weight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedState.Lock()\n\tdefer sharedState.Unlock()\n\n\tb := sharedState.beliefs[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = decayedConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad > 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedState.beliefs[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int { return len(q) }\nfunc (q searchQueue) Less(i, j int) bool { return q[i].score < q[j].score }\nfunc (q searchQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\treturn item\n}\n\nfunc containsNode(state *searchState, node route.Vertex) bool {\n\tfor current := state; current != nil; current = current.child {\n\t\tif current.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\tstate *searchState) bool {\n\n\told := labels[state.node]\n\tkept := old[:0]\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= state.score &&\n\t\t\tother.fee <= state.fee &&\n\t\t\tother.amount <= state.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif state.score <= other.score &&\n\t\t\tstate.fee <= other.fee &&\n\t\t\tstate.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\tstate.active = true\n\tkept = append(kept, state)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == state {\n\t\t\t\tlabels[state.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[state.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) effectiveReservations() map[edgeKey]lnwire.MilliSatoshi {\n\tresult := cloneReservations(r.reserved)\n\tfor key, amt := range r.pendingReserved {\n\t\taddAmount(result, key, amt)\n\t}\n\tfor key, amt := range sharedHoldsExcept(r.owner) {\n\t\taddAmount(result, key, amt)\n\t}\n\treturn result\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\n\tused := addSaturated(r.committedFees, r.pendingFees)\n\tif used >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\treturn r.spec.FeeLimitMsat - used\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.72\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(\n\tkey edgeKey) lnwire.MilliSatoshi {\n\n\ts := r.suspicion[key]\n\tupper := r.localUpper[key]\n\tif s < 1.75 || upper <= 0 {\n\t\treturn 0\n\t}\n\n\tfactor := int64(88)\n\tswitch {\n\tcase s >= 5:\n\t\tfactor = 58\n\tcase s >= 3.5:\n\t\tfactor = 69\n\tcase s >= 2.5:\n\t\tfactor = 79\n\t}\n\n\tcutoff := upper * lnwire.MilliSatoshi(factor) / 100\n\tif cutoff <= 0 {\n\t\treturn 1\n\t}\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) edgePenalty(key edgeKey,\n\tload lnwire.MilliSatoshi) float64 {\n\n\tp := r.penalty[key]\n\tupper := r.localUpper[key]\n\tif upper > 0 && load < upper*2/3 {\n\t\tp *= 0.18\n\t}\n\treturn p\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\n\tfor pq.Len() > 0 {\n\t\tcurrent := heap.Pop(pq).(*searchState)\n\t\tif !current.active {\n\t\t\tcontinue\n\t\t}\n\t\tif current.node == r.source {\n\t\t\tresult = current\n\t\t\tbreak\n\t\t}\n\t\tif current.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[current.node] {\n\t\t\tif r.blocked[edge.key] ||\n\t\t\t\tcontainsNode(current, edge.from) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamountOverEdge := current.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif current.node != r.spec.Target {\n\t\t\t\tif current.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = forwardingFee(\n\t\t\t\t\tedge, current.nextEdge, current.amount,\n\t\t\t\t)\n\t\t\t\tif nodeCharge >\n\t\t\t\t\tlnwire.MaxMilliSatoshi-current.amount {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tamountOverEdge += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usable(amountOverEdge) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved >\n\t\t\t\tlnwire.MaxMilliSatoshi-amountOverEdge {\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tload := amountOverEdge + reserved\n\t\t\tif load > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && load >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || load > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-current.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttotalFee := current.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := channelProbability(\n\t\t\t\tedge.key, load, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source &&\n\t\t\t\tr.suspicion[edge.key] < 0.8 {\n\n\t\t\t\tif probability < 0.94 {\n\t\t\t\t\tprobability = 0.94\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := current.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(probability)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty(edge.key, load)\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reuseCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.55\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: amountOverEdge,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: current.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: current,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor current := result; current.node != r.spec.Target;\n\t\tcurrent = current.child {\n\n\t\tif current.nextEdge == nil || current.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, current.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tif reservations[edge.key] >\n\t\t\tlnwire.MaxMilliSatoshi-amounts[i] {\n\n\t\t\treturn nil, errors.New(\"route load overflow\")\n\t\t}\n\t\tloads[i] = reservations[edge.key] + amounts[i]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route,\n\t[]lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tcount := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, count)\n\texpiries := make([]uint32, count)\n\n\tamounts[count-1] = amt\n\texpiries[count-1] = finalCltvDelta\n\n\tfor i := count - 2; i >= 0; i-- {\n\t\tcharge := forwardingFee(\n\t\t\tpath[i], path[i+1], amounts[i+1],\n\t\t)\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, count)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\t\tif i < count-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc weightedAllocation(total lnwire.MilliSatoshi,\n\tweights []int64) []lnwire.MilliSatoshi {\n\n\tif len(weights) == 0 ||\n\t\ttotal < lnwire.MilliSatoshi(len(weights)) {\n\n\t\treturn nil\n\t}\n\n\tvar weightTotal int64\n\tfor _, weight := range weights {\n\t\tif weight <= 0 || weightTotal > math.MaxInt64-weight {\n\t\t\treturn nil\n\t\t}\n\t\tweightTotal += weight\n\t}\n\n\tresult := make([]lnwire.MilliSatoshi, len(weights))\n\tvar allocated lnwire.MilliSatoshi\n\n\tfor i, weight := range weights {\n\t\tvar shard lnwire.MilliSatoshi\n\t\tif i == len(weights)-1 {\n\t\t\tshard = total - allocated\n\t\t} else {\n\t\t\tshard = lnwire.MilliSatoshi(\n\t\t\t\tfloat64(total) * float64(weight) /\n\t\t\t\t\tfloat64(weightTotal),\n\t\t\t)\n\t\t}\n\n\t\tif shard <= 0 ||\n\t\t\tshard > lnwire.MaxMilliSatoshi-allocated {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tresult[i] = shard\n\t\tallocated += shard\n\t}\n\n\tif allocated != total {\n\t\treturn nil\n\t}\n\treturn result\n}\n\nfunc equalAllocation(total lnwire.MilliSatoshi,\n\tparts int) []lnwire.MilliSatoshi {\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tweights[i] = 1\n\t}\n\treturn weightedAllocation(total, weights)\n}\n\nfunc shapedAllocation(total lnwire.MilliSatoshi,\n\tparts, mode int) []lnwire.MilliSatoshi {\n\n\tif parts < 2 {\n\t\treturn nil\n\t}\n\n\tweights := make([]int64, parts)\n\tfor i := range weights {\n\t\tswitch mode {\n\t\tcase 0:\n\t\t\tweights[i] = int64(parts - i)\n\t\tcase 1:\n\t\t\tweights[i] = int64(i + 1)\n\t\tcase 2:\n\t\t\tweights[i] = 2\n\t\t\tif i == 0 {\n\t\t\t\tweights[i] = 3\n\t\t\t}\n\t\tcase 3:\n\t\t\tweights[i] = 2\n\t\t\tif i == parts-1 {\n\t\t\t\tweights[i] = 3\n\t\t\t}\n\t\t}\n\t}\n\n\treturn weightedAllocation(total, weights)\n}\n\nfunc allocationTotal(allocation []lnwire.MilliSatoshi) (\n\tlnwire.MilliSatoshi, bool) {\n\n\tvar total lnwire.MilliSatoshi\n\tfor _, shard := range allocation {\n\t\tif shard <= 0 || shard > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn 0, false\n\t\t}\n\t\ttotal += shard\n\t}\n\treturn total, true\n}\n\nfunc addReservationSet(reservations map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\taddAmount(reservations, edge.key, meta.amounts[i])\n\t}\n}\n\nfunc feeShareCap(budget, shard,\n\tremaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif budget == lnwire.MaxMilliSatoshi {\n\t\treturn budget\n\t}\n\tif budget <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\n\tshare := lnwire.MilliSatoshi(\n\t\tfloat64(budget) * float64(shard) / float64(remaining),\n\t)\n\tif share <= 0 {\n\t\tshare = 1\n\t}\n\n\tcap := addSaturated(share, share/3)\n\tif cap > budget {\n\t\tcap = budget\n\t}\n\treturn cap\n}\n\nfunc (r *candidateRouter) tryAllocation(\n\tallocation []lnwire.MilliSatoshi,\n\tfeeBudget lnwire.MilliSatoshi,\n\tbaseReservations map[edgeKey]lnwire.MilliSatoshi,\n\tshardCap lnwire.MilliSatoshi,\n\tsearches *int) ([]*plannedRoute, float64, bool) {\n\n\tif len(allocation) == 0 ||\n\t\t*searches+len(allocation) > maxPlanSearches {\n\n\t\treturn nil, 0, false\n\t}\n\n\tremainingAmount, ok := allocationTotal(allocation)\n\tif !ok {\n\t\treturn nil, 0, false\n\t}\n\n\tfor _, shard := range allocation {\n\t\tif shardCap > 0 && shard > shardCap {\n\t\t\treturn nil, 0, false\n\t\t}\n\t}\n\n\treservations := cloneReservations(baseReservations)\n\tremainingBudget := feeBudget\n\tplan := make([]*plannedRoute, 0, len(allocation))\n\tvar score float64\n\n\tfor _, shard := range allocation {\n\t\tcap := feeShareCap(\n\t\t\tremainingBudget, shard, remainingAmount,\n\t\t)\n\n\t\t(*searches)++\n\t\tpr, err := r.findRoute(shard, reservations, cap)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif remainingBudget != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > remainingBudget {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tremainingBudget -= pr.meta.fee\n\t\t}\n\n\t\tplan = append(plan, pr)\n\t\tscore += pr.score\n\t\taddReservationSet(reservations, pr.meta)\n\t\tremainingAmount -= shard\n\t}\n\n\tscore += float64(len(plan)-1) * extraPartCostMsat\n\treturn plan, score, true\n}\n\nfunc (r *candidateRouter) widestAvailable(\n\treservations map[edgeKey]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\twidth := map[route.Vertex]lnwire.MilliSatoshi{\n\t\tr.spec.Target: lnwire.MaxMilliSatoshi,\n\t}\n\tqueue := []route.Vertex{r.spec.Target}\n\tqueued := map[route.Vertex]bool{r.spec.Target: true}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\t\tqueued[node] = false\n\n\t\tdownstream := width[node]\n\t\tfor _, edge := range r.incomingEdges[node] {\n\t\t\tlimit := edge.publicLimit()\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved >= limit {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlimit -= reserved\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || reserved >= balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavailable := balance - reserved\n\t\t\t\tif available < limit {\n\t\t\t\t\tlimit = available\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif downstream < limit {\n\t\t\t\tlimit = downstream\n\t\t\t}\n\t\t\tif limit <= width[edge.from] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\twidth[edge.from] = limit\n\t\t\tif !queued[edge.from] {\n\t\t\t\tqueued[edge.from] = true\n\t\t\t\tqueue = append(queue, edge.from)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn width[r.source]\n}\n\nfunc ceilParts(total, limit lnwire.MilliSatoshi) int {\n\tif total <= 0 || limit <= 0 {\n\t\treturn 1\n\t}\n\n\tparts := int(total / limit)\n\tif total%limit != 0 {\n\t\tparts++\n\t}\n\tif parts < 1 {\n\t\tparts = 1\n\t}\n\treturn parts\n}\n\nfunc allocationKey(allocation []lnwire.MilliSatoshi) string {\n\tkey := \"\"\n\tfor _, amt := range allocation {\n\t\tkey += amountKey(amt)\n\t\tkey += \"/\"\n\t}\n\treturn key\n}\n\nfunc amountKey(v lnwire.MilliSatoshi) string {\n\tif v == 0 {\n\t\treturn \"0\"\n\t}\n\n\tconst digits = \"0123456789\"\n\tvar buf [24]byte\n\tindex := len(buf)\n\tn := int64(v)\n\tfor n > 0 {\n\t\tindex--\n\t\tbuf[index] = digits[n%10]\n\t\tn /= 10\n\t}\n\treturn string(buf[index:])\n}\n\nfunc reverseAllocation(src []lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\tdst := make([]lnwire.MilliSatoshi, len(src))\n\tfor i := range src {\n\t\tdst[len(src)-1-i] = src[i]\n\t}\n\treturn dst\n}\n\nfunc (r *candidateRouter) splitHints(\n\ttotal lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\thints := make([]lnwire.MilliSatoshi, 0, 48)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\tadd := func(amt lnwire.MilliSatoshi) {\n\t\tif amt <= 0 || amt >= total || seen[amt] {\n\t\t\treturn\n\t\t}\n\t\tseen[amt] = true\n\t\thints = append(hints, amt)\n\t}\n\n\tfor ratio := int64(1); ratio <= 9; ratio++ {\n\t\tadd(total * lnwire.MilliSatoshi(ratio) / 10)\n\t}\n\n\taddLimit := func(limit lnwire.MilliSatoshi) {\n\t\tfor _, pct := range []int64{45, 58, 70, 80, 88, 94} {\n\t\t\tadd(limit * lnwire.MilliSatoshi(pct) / 100)\n\t\t}\n\t}\n\n\tfor _, edge := range r.incomingEdges[r.spec.Target] {\n\t\taddLimit(edge.publicLimit())\n\t}\n\n\tfor _, edges := range r.incomingEdges {\n\t\tfor _, edge := range edges {\n\t\t\tif edge.from != r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlimit := edge.publicLimit()\n\t\t\tif balance, ok := r.localBalances[edge.chanID];\n\t\t\t\tok && balance < limit {\n\n\t\t\t\tlimit = balance\n\t\t\t}\n\t\t\taddLimit(limit)\n\t\t}\n\t}\n\n\tfor key, upper := range r.localUpper {\n\t\tif r.suspicion[key] < 1 {\n\t\t\tcontinue\n\t\t}\n\t\tadd(upper * 55 / 100)\n\t\tadd(upper * 68 / 100)\n\t\tadd(upper * 80 / 100)\n\t}\n\n\treturn hints\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tpartLimit := int(slots)\n\tif partLimit > maxPlannedParts {\n\t\tpartLimit = maxPlannedParts\n\t}\n\tif total < lnwire.MilliSatoshi(partLimit) {\n\t\tpartLimit = int(total)\n\t}\n\tif partLimit < 1 {\n\t\treturn nil, errors.New(\"amount too small\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tbaseReservations := r.effectiveReservations()\n\twidth := r.widestAvailable(baseReservations)\n\n\trequiredParts := 1\n\tif width > 0 {\n\t\trequiredParts = ceilParts(total, width)\n\t}\n\tif requiredParts > partLimit {\n\t\trequiredParts = 1\n\t}\n\n\tdesiredParts := requiredParts\n\tif width > 0 {\n\t\tcomfortable := width * 2 / 5\n\t\texploratory := ceilParts(total, comfortable)\n\t\tif exploratory > desiredParts {\n\t\t\tdesiredParts = exploratory\n\t\t}\n\t}\n\tif desiredParts > 4 {\n\t\tdesiredParts = 4\n\t}\n\tif desiredParts > partLimit {\n\t\tdesiredParts = partLimit\n\t}\n\n\tshardCap := r.nextShardCap\n\tif shardCap > 0 && ceilParts(total, shardCap) > partLimit {\n\t\tshardCap = 0\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\tsearches := 0\n\tseen := make(map[string]bool)\n\n\tconsider := func(allocation []lnwire.MilliSatoshi) {\n\t\tif len(allocation) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tkey := allocationKey(allocation)\n\t\tif seen[key] {\n\t\t\treturn\n\t\t}\n\t\tseen[key] = true\n\n\t\tplan, score, ok := r.tryAllocation(\n\t\t\tallocation, feeBudget, baseReservations,\n\t\t\tshardCap, &searches,\n\t\t)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\n\t\tif len(plan) < desiredParts {\n\t\t\tscore += float64(desiredParts-len(plan)) * 720_000\n\t\t}\n\t\tif score < bestScore {\n\t\t\tbest = plan\n\t\t\tbestScore = score\n\t\t}\n\t}\n\n\tfor parts := requiredParts; parts <= partLimit; parts++ {\n\t\tif searches+parts > maxPlanSearches {\n\t\t\tbreak\n\t\t}\n\n\t\tconsider(equalAllocation(total, parts))\n\t\tif parts >= 2 && parts <= 6 {\n\t\t\tfor mode := 0; mode < 4; mode++ {\n\t\t\t\tallocation := shapedAllocation(\n\t\t\t\t\ttotal, parts, mode,\n\t\t\t\t)\n\t\t\t\tconsider(allocation)\n\t\t\t\tconsider(reverseAllocation(allocation))\n\t\t\t}\n\t\t}\n\t}\n\n\tif partLimit >= 2 {\n\t\tfor _, first := range r.splitHints(total) {\n\t\t\tif searches+2 > maxPlanSearches {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsecond := total - first\n\t\t\tconsider([]lnwire.MilliSatoshi{first, second})\n\t\t\tconsider([]lnwire.MilliSatoshi{second, first})\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no complete route set found\")\n\t}\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pr := range plan {\n\t\ttotal = addSaturated(total, pr.meta.delivered)\n\t}\n\treturn total\n}\n\nfunc (r *candidateRouter) reservePending(meta *routeMeta) {\n\tfor i, edge := range meta.path {\n\t\taddAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\taddSharedHold(r.owner, edge.key, meta.amounts[i])\n\t}\n\tr.pendingFees = addSaturated(r.pendingFees, meta.fee)\n}\n\nfunc (r *candidateRouter) removePending(meta *routeMeta,\n\treleaseShared bool) {\n\n\tfor i, edge := range meta.path {\n\t\tsubtractAmount(r.pendingReserved, edge.key, meta.amounts[i])\n\t\tif releaseShared {\n\t\t\tsubtractSharedHold(\n\t\t\t\tr.owner, edge.key, meta.amounts[i],\n\t\t\t)\n\t\t}\n\t}\n\n\tif meta.fee >= r.pendingFees {\n\t\tr.pendingFees = 0\n\t} else {\n\t\tr.pendingFees -= meta.fee\n\t}\n}\n\nfunc (r *candidateRouter) cleanup() {\n\tif r.cleaned {\n\t\treturn\n\t}\n\tr.cleaned = true\n\treleaseSharedHolds(r.owner)\n}\n\nfunc (r *candidateRouter) refreshPlanMeta(pr *plannedRoute) bool {\n\tif pr == nil || pr.meta == nil ||\n\t\tlen(pr.meta.path) != len(pr.meta.amounts) {\n\n\t\treturn false\n\t}\n\n\treservations := r.effectiveReservations()\n\tfor i, edge := range pr.meta.path {\n\t\tamount := pr.meta.amounts[i]\n\t\tif !edge.usable(amount) ||\n\t\t\treservations[edge.key] >\n\t\t\t\tlnwire.MaxMilliSatoshi-amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tload := reservations[edge.key] + amount\n\t\tif load > edge.capacity {\n\t\t\treturn false\n\t\t}\n\n\t\tcutoff := r.retryCutoff(edge.key)\n\t\tif cutoff > 0 && load >= cutoff {\n\t\t\treturn false\n\t\t}\n\n\t\tif edge.from == r.source {\n\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\tif !ok || load > balance {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tpr.meta.loads[i] = load\n\t}\n\n\tbudget := r.remainingFeeBudget()\n\treturn budget == lnwire.MaxMilliSatoshi ||\n\t\tpr.meta.fee <= budget\n}\n\nfunc (r *candidateRouter) fallbackRoute(total lnwire.MilliSatoshi,\n\tslots uint32) (*plannedRoute, error) {\n\n\treservations := r.effectiveReservations()\n\tbudget := r.remainingFeeBudget()\n\n\tif slots <= 1 {\n\t\treturn r.findRoute(total, reservations, budget)\n\t}\n\n\tcandidates := make([]lnwire.MilliSatoshi, 0, 48)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\tadd := func(amt lnwire.MilliSatoshi) {\n\t\tif amt <= 0 || amt > total || seen[amt] {\n\t\t\treturn\n\t\t}\n\t\tif r.nextShardCap > 0 && amt > r.nextShardCap {\n\t\t\treturn\n\t\t}\n\t\tseen[amt] = true\n\t\tcandidates = append(candidates, amt)\n\t}\n\n\tadd(total)\n\tadd(total * 7 / 8)\n\tadd(total * 4 / 5)\n\tadd(total * 3 / 4)\n\tadd(total * 2 / 3)\n\tadd(total / 2)\n\tadd(total / lnwire.MilliSatoshi(slots))\n\n\twidth := r.widestAvailable(reservations)\n\tif width > 0 {\n\t\tadd(width * 9 / 10)\n\t\tadd(width * 3 / 4)\n\t\tadd(width / 2)\n\t\tadd(width / 3)\n\t}\n\n\tfor _, hint := range r.splitHints(total) {\n\t\tadd(hint)\n\t}\n\n\tfor len(candidates) > 0 {\n\t\tlargest := 0\n\t\tfor i := 1; i < len(candidates); i++ {\n\t\t\tif candidates[i] > candidates[largest] {\n\t\t\t\tlargest = i\n\t\t\t}\n\t\t}\n\n\t\tshard := candidates[largest]\n\t\tcandidates = append(\n\t\t\tcandidates[:largest], candidates[largest+1:]...,\n\t\t)\n\n\t\tcap := feeShareCap(budget, shard, total)\n\t\tpr, err := r.findRoute(shard, reservations, cap)\n\t\tif err == nil {\n\t\t\treturn pr, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no feasible partial route\")\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.initialAmount == 0 {\n\t\tr.initialAmount = amt\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\tr.cleanup()\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\tslots := maxParts - inFlightHtlcs\n\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\tif len(r.plan) > 0 && !r.refreshPlanMeta(r.plan[0]) {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err == nil {\n\t\t\tr.plan = plan\n\t\t} else {\n\t\t\tfallback, fallbackErr := r.fallbackRoute(amt, slots)\n\t\t\tif fallbackErr != nil {\n\t\t\t\tr.cleanup()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tr.plan = []*plannedRoute{fallback}\n\t\t}\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\n\tif !r.refreshPlanMeta(next) {\n\t\tr.plan = nil\n\t\tfallback, err := r.fallbackRoute(amt, slots)\n\t\tif err != nil {\n\t\t\tr.cleanup()\n\t\t\treturn nil, err\n\t\t}\n\t\tnext = fallback\n\t}\n\n\tr.issued[next.rt] = next.meta\n\tr.reservePending(next.meta)\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\treservations := r.effectiveReservations()\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tif reservations[key] >\n\t\t\tlnwire.MaxMilliSatoshi-amount {\n\n\t\t\treturn nil\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+reservations[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) reduceShardCap(amt lnwire.MilliSatoshi) {\n\tif amt <= 1 {\n\t\treturn\n\t}\n\n\tfactor := int64(84)\n\tswitch {\n\tcase r.misses >= 8:\n\t\tfactor = 52\n\tcase r.misses >= 5:\n\t\tfactor = 65\n\tcase r.misses >= 3:\n\t\tfactor = 74\n\t}\n\n\tcap := amt * lnwire.MilliSatoshi(factor) / 100\n\tif cap >= amt {\n\t\tcap = amt - 1\n\t}\n\tif cap <= 0 {\n\t\treturn\n\t}\n\n\tif r.nextShardCap == 0 || cap < r.nextShardCap {\n\t\tr.nextShardCap = cap\n\t}\n}\n\nfunc (r *candidateRouter) penalizePath(meta *routeMeta,\n\tamount float64) {\n\n\tif len(meta.path) == 0 {\n\t\treturn\n\t}\n\n\tperEdge := amount / math.Sqrt(float64(len(meta.path)))\n\tfor _, edge := range meta.path {\n\t\tr.penalty[edge.key] += perEdge\n\t}\n}\n\nfunc (r *candidateRouter) noteLiquidityCandidate(\n\tmeta *routeMeta, index int, strength float64, now time.Time) {\n\n\tif index < 0 || index >= len(meta.path) {\n\t\treturn\n\t}\n\n\tedge := meta.path[index]\n\tr.suspicion[edge.key] += strength\n\tload := meta.loads[index]\n\n\tif old := r.localUpper[edge.key]; old == 0 || load < old {\n\t\tr.localUpper[edge.key] = load\n\t}\n\n\ts := r.suspicion[edge.key]\n\tr.penalty[edge.key] +=\n\t\tstrength * (360_000 + 150_000*math.Min(s, 5))\n\n\tif s >= 2.1 {\n\t\tweight := 0.10\n\t\tif s >= 3.2 {\n\t\t\tweight = 0.22\n\t\t}\n\t\tif s >= 4.8 {\n\t\t\tweight = 0.35\n\t\t}\n\t\tupdateBelief(edge.key, load, false, weight, now)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64,\n\trt *route.Route, result routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tr.removePending(meta, false)\n\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.76, now,\n\t\t\t)\n\t\t\taddAmount(r.reserved, edge.key, meta.amounts[i])\n\t\t\tr.penalty[edge.key] *= 0.08\n\t\t\tr.suspicion[edge.key] *= 0.18\n\t\t\tr.policyBlame[edge.key] *= 0.1\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t}\n\t\t\tdelete(r.blocked, edge.key)\n\t\t}\n\n\t\tr.committedFees = addSaturated(\n\t\t\tr.committedFees, meta.fee,\n\t\t)\n\t\tr.succeededAmount = addSaturated(\n\t\t\tr.succeededAmount, meta.delivered,\n\t\t)\n\t\tr.misses = 0\n\t\tr.nextShardCap = 0\n\n\t\tif r.initialAmount > 0 &&\n\t\t\tr.succeededAmount >= r.initialAmount {\n\n\t\t\tr.cleanup()\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.removePending(meta, true)\n\tr.plan = nil\n\tr.misses++\n\n\treported := r.failureEdgeIndex(rt, result)\n\tif reported < 0 {\n\t\tr.penalizePath(meta, 150_000)\n\t\tr.reduceShardCap(meta.delivered)\n\t\treturn nil\n\t}\n\n\tfailIndex := reported\n\tif failIndex >= len(meta.path) {\n\t\tfailIndex = len(meta.path) - 1\n\t}\n\tif failIndex < 0 || failIndex >= len(meta.path) {\n\t\tr.penalizePath(meta, 150_000)\n\t\tr.reduceShardCap(meta.delivered)\n\t\treturn nil\n\t}\n\n\tearliestCertain := failIndex - 1\n\tif earliestCertain < 0 {\n\t\tearliestCertain = 0\n\t}\n\tfor i := 0; i < earliestCertain; i++ {\n\t\tupdateBelief(\n\t\t\tmeta.path[i].key, meta.loads[i],\n\t\t\ttrue, 0.12, now,\n\t\t)\n\t\tr.penalty[meta.path[i].key] *= 0.7\n\t}\n\n\tcode := result.Failure.Code()\n\tr.penalizePath(meta, 65_000)\n\n\twindowStart := failIndex - 1\n\tif windowStart < 0 {\n\t\twindowStart = 0\n\t}\n\twindowEnd := failIndex + 1\n\tif windowEnd >= len(meta.path) {\n\t\twindowEnd = len(meta.path) - 1\n\t}\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.reduceShardCap(meta.delivered)\n\n\t\tfor i := windowStart; i <= windowEnd; i++ {\n\t\t\tstrength := 0.58\n\t\t\tif i == failIndex {\n\t\t\t\tstrength = 1\n\t\t\t}\n\t\t\tr.noteLiquidityCandidate(\n\t\t\t\tmeta, i, strength, now,\n\t\t\t)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tfor i := windowStart; i <= windowEnd; i++ {\n\t\t\tstrength := 0.55\n\t\t\tif i == failIndex {\n\t\t\t\tstrength = 1\n\t\t\t}\n\n\t\t\tedge := meta.path[i]\n\t\t\tr.policyBlame[edge.key] += strength\n\t\t\tr.penalty[edge.key] += strength * 1_350_000\n\n\t\t\tif r.policyBlame[edge.key] >= 2.6 {\n\t\t\t\tr.blocked[edge.key] = true\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tr.reduceShardCap(meta.delivered)\n\t\tfor i := windowStart; i <= windowEnd; i++ {\n\t\t\tstrength := 0.25\n\t\t\tif i == failIndex {\n\t\t\t\tstrength = 0.45\n\t\t\t}\n\t\t\tr.penalty[meta.path[i].key] +=\n\t\t\t\tstrength * 500_000\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 35,
"parent": 9,
"score": 0.0091,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tbaseRiskPriceMsat = float64(420_000)\n\tminRiskPriceMsat = float64(35_000)\n\n\thopCostMsat = float64(75_000)\n\treusedEdgeCostMsat = float64(1_900_000)\n\textraPartCostMsat = float64(65_000)\n\n\tmaxSearchHops = 24\n\tmaxLabelsPerNode = 4\n\tmaxSearchPops = 4000\n\tmaxPlannedParts = 16\n\tmaxRouterAttempts = 20\n\tmaxConsecutiveMisses = 14\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tm map[edgeKey]liquidityBelief\n}{\n\tm: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n\n\tinboundBase int32\n\tinboundRate int32\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usableHTLC(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\nfunc nodeFee(in, out *candidateEdge,\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\toutbound := int64(out.fee(amt))\n\tif outbound < 0 {\n\t\toutbound = 0\n\t}\n\n\tinboundBasis := int64(amt) + outbound\n\tinbound := int64(in.inboundBase) +\n\t\tinboundBasis*int64(in.inboundRate)/1_000_000\n\n\ttotal := outbound + inbound\n\tif total <= 0 {\n\t\treturn 0\n\t}\n\n\treturn lnwire.MilliSatoshi(total)\n}\n\ntype routeMeta struct {\n\tpath []*candidateEdge\n\tamounts []lnwire.MilliSatoshi\n\tloads []lnwire.MilliSatoshi\n\tdelivered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\ntype plannedRoute struct {\n\trt *route.Route\n\tmeta *routeMeta\n\tscore float64\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\treserved map[edgeKey]lnwire.MilliSatoshi\n\theld []*routeMeta\n\n\tlocalUpper map[edgeKey]lnwire.MilliSatoshi\n\tlocalFails map[edgeKey]int\n\tblocked map[edgeKey]bool\n\tedgePenalty map[edgeKey]float64\n\n\tliquiditySuspects map[edgeKey]float64\n\tpolicySuspects map[edgeKey]int\n\n\tissued map[*route.Route]*routeMeta\n\tplan []*plannedRoute\n\n\tcommittedFees lnwire.MilliSatoshi\n\n\tattempts int\n\tconsecutiveMisses int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalUpper: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tlocalFails: make(map[edgeKey]int),\n\t\tblocked: make(map[edgeKey]bool),\n\t\tedgePenalty: make(map[edgeKey]float64),\n\t\tliquiditySuspects: make(map[edgeKey]float64),\n\t\tpolicySuspects: make(map[edgeKey]int),\n\t\tissued: make(map[*route.Route]*routeMeta),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(ch.Capacity),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t\tinboundBase: ch.InboundFee.BaseFee,\n\t\t\t\t\tinboundRate: ch.InboundFee.FeeRate,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.025)\n\thighMode := 0.49 / (1 + math.Exp((x-0.97)/0.025))\n\tp := 0.005 + lowMode + highMode\n\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.985:\n\t\treturn 0.985\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.updated.IsZero() || now.Before(b.updated) {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.conf\n\t}\n\n\treturn b.conf * math.Exp(-age.Seconds()/2400)\n}\n\nfunc channelProbability(key edgeKey, amt, capacity lnwire.MilliSatoshi,\n\tnow time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tsharedBeliefs.Lock()\n\tb, ok := sharedBeliefs.m[key]\n\tsharedBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconf := beliefConfidence(b, now)\n\tif conf < 0.01 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\thasEvidence := false\n\n\tswitch {\n\tcase b.lowerOK > 0 && amt <= b.lowerOK:\n\t\tevidence = 0.995\n\t\thasEvidence = true\n\n\tcase b.upperBad > 0 && amt >= b.upperBad:\n\t\tevidence = 0.012\n\t\thasEvidence = true\n\n\tcase b.lowerOK > 0 && b.upperBad > b.lowerOK:\n\t\twidth := float64(b.upperBad - b.lowerOK)\n\t\tpos := float64(b.upperBad-amt) / width\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t\tif pos > 1 {\n\t\t\tpos = 1\n\t\t}\n\n\t\tevidence = 0.012 + 0.983*pos\n\t\thasEvidence = true\n\t}\n\n\tif !hasEvidence {\n\t\treturn prior\n\t}\n\n\tp := prior*(1-conf) + evidence*conf\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc updateBelief(key edgeKey, amt lnwire.MilliSatoshi, success bool,\n\tweight float64, now time.Time) {\n\n\tif amt <= 0 || weight <= 0 {\n\t\treturn\n\t}\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tb := sharedBeliefs.m[key]\n\tif !b.updated.IsZero() && now.Before(b.updated) {\n\t\tb = liquidityBelief{}\n\t} else {\n\t\tb.conf = beliefConfidence(b, now)\n\t}\n\n\tif success {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperBad != 0 && amt >= b.upperBad {\n\t\t\tb.upperBad = 0\n\t\t}\n\t} else {\n\t\tif b.upperBad == 0 || amt < b.upperBad {\n\t\t\tb.upperBad = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t}\n\n\tb.conf = math.Min(1, b.conf+weight)\n\tb.updated = now\n\tsharedBeliefs.m[key] = b\n}\n\ntype searchState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n\tscore float64\n\thops int\n\tnextEdge *candidateEdge\n\tchild *searchState\n\tactive bool\n}\n\ntype searchQueue []*searchState\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(x any) {\n\t*q = append(*q, x.(*searchState))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\nfunc containsNode(s *searchState, node route.Vertex) bool {\n\tfor cur := s; cur != nil; cur = cur.child {\n\t\tif cur.node == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchState,\n\ts *searchState) bool {\n\n\told := labels[s.node]\n\tkept := make([]*searchState, 0, len(old)+1)\n\n\tfor _, other := range old {\n\t\tif !other.active {\n\t\t\tcontinue\n\t\t}\n\n\t\tif other.score <= s.score &&\n\t\t\tother.fee <= s.fee &&\n\t\t\tother.amount <= s.amount {\n\n\t\t\treturn false\n\t\t}\n\n\t\tif s.score <= other.score &&\n\t\t\ts.fee <= other.fee &&\n\t\t\ts.amount <= other.amount {\n\n\t\t\tother.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, other)\n\t}\n\n\ts.active = true\n\tkept = append(kept, s)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tcheapest := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].fee < kept[cheapest].fee {\n\t\t\t\tcheapest = i\n\t\t\t}\n\t\t}\n\n\t\tworst := -1\n\t\tfor i := range kept {\n\t\t\tif i == cheapest {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif worst < 0 || kept[i].score > kept[worst].score {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif worst >= 0 {\n\t\t\tremoved := kept[worst]\n\t\t\tremoved.active = false\n\t\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\t\tif removed == s {\n\t\t\t\tlabels[s.node] = kept\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tlabels[s.node] = kept\n\treturn true\n}\n\nfunc cloneReservations(\n\tsrc map[edgeKey]lnwire.MilliSatoshi) map[edgeKey]lnwire.MilliSatoshi {\n\n\tdst := make(map[edgeKey]lnwire.MilliSatoshi, len(src))\n\tfor key, amt := range src {\n\t\tdst[key] = amt\n\t}\n\n\treturn dst\n}\n\nfunc (r *candidateRouter) remainingFeeBudget() lnwire.MilliSatoshi {\n\tif r.spec.FeeLimitMsat == lnwire.MaxMilliSatoshi {\n\t\treturn lnwire.MaxMilliSatoshi\n\t}\n\tif r.committedFees >= r.spec.FeeLimitMsat {\n\t\treturn 0\n\t}\n\n\treturn r.spec.FeeLimitMsat - r.committedFees\n}\n\nfunc riskPrice(feeCap lnwire.MilliSatoshi) float64 {\n\tif feeCap == lnwire.MaxMilliSatoshi {\n\t\treturn baseRiskPriceMsat\n\t}\n\n\tprice := float64(feeCap) * 0.80\n\tif price < minRiskPriceMsat {\n\t\tprice = minRiskPriceMsat\n\t}\n\tif price > baseRiskPriceMsat {\n\t\tprice = baseRiskPriceMsat\n\t}\n\n\treturn price\n}\n\nfunc (r *candidateRouter) retryCutoff(key edgeKey) lnwire.MilliSatoshi {\n\tupper := r.localUpper[key]\n\tif upper <= 0 {\n\t\treturn 0\n\t}\n\n\tcutoff := upper * 7 / 8\n\tif r.localFails[key] >= 3 {\n\t\tcutoff = upper * 2 / 3\n\t}\n\tif cutoff <= 0 {\n\t\tcutoff = 1\n\t}\n\n\treturn cutoff\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi,\n\treservations map[edgeKey]lnwire.MilliSatoshi,\n\tfeeCap lnwire.MilliSatoshi) (*plannedRoute, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is target\")\n\t}\n\n\tnow := r.view.Now()\n\tprice := riskPrice(feeCap)\n\tlabels := make(map[route.Vertex][]*searchState)\n\tpq := &searchQueue{}\n\n\tstart := &searchState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\tlabels[start.node] = []*searchState{start}\n\theap.Push(pq, start)\n\n\tvar result *searchState\n\tpops := 0\n\n\tfor pq.Len() > 0 && pops < maxSearchPops {\n\t\tcur := heap.Pop(pq).(*searchState)\n\t\tpops++\n\n\t\tif !cur.active {\n\t\t\tcontinue\n\t\t}\n\t\tif cur.node == r.source {\n\t\t\tresult = cur\n\t\t\tbreak\n\t\t}\n\t\tif cur.hops >= maxSearchHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[cur.node] {\n\t\t\tif r.blocked[edge.key] || containsNode(cur, edge.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tover := cur.amount\n\t\t\tnodeCharge := lnwire.MilliSatoshi(0)\n\n\t\t\tif cur.node != r.spec.Target {\n\t\t\t\tif cur.nextEdge == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tnodeCharge = nodeFee(edge, cur.nextEdge, cur.amount)\n\t\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.amount {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tover += nodeCharge\n\t\t\t}\n\n\t\t\tif !edge.usableHTLC(over) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := reservations[edge.key]\n\t\t\tif reserved > lnwire.MaxMilliSatoshi-over {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalLoad := over + reserved\n\t\t\tif totalLoad > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcutoff := r.retryCutoff(edge.key)\n\t\t\tif cutoff > 0 && totalLoad >= cutoff {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance, ok := r.localBalances[edge.chanID]\n\t\t\t\tif !ok || totalLoad > balance {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif nodeCharge > lnwire.MaxMilliSatoshi-cur.fee {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttotalFee := cur.fee + nodeCharge\n\t\t\tif feeCap != lnwire.MaxMilliSatoshi &&\n\t\t\t\ttotalFee > feeCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := channelProbability(\n\t\t\t\tedge.key, totalLoad, edge.capacity, now,\n\t\t\t)\n\t\t\tif edge.from == r.source {\n\t\t\t\tp = 0.995\n\t\t\t\tif r.localUpper[edge.key] > 0 {\n\t\t\t\t\tp = 0.80\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tscore := cur.score +\n\t\t\t\tfloat64(nodeCharge) +\n\t\t\t\tprice*(-math.Log(p)) +\n\t\t\t\thopCostMsat +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\tif reserved > 0 {\n\t\t\t\treuse := reusedEdgeCostMsat\n\t\t\t\tif edge.from == r.source {\n\t\t\t\t\treuse *= 0.65\n\t\t\t\t}\n\t\t\t\tscore += reuse\n\t\t\t}\n\n\t\t\tnext := &searchState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: over,\n\t\t\t\tfee: totalFee,\n\t\t\t\tscore: score,\n\t\t\t\thops: cur.hops + 1,\n\t\t\t\tnextEdge: edge,\n\t\t\t\tchild: cur,\n\t\t\t}\n\t\t\tif addSearchLabel(labels, next) {\n\t\t\t\theap.Push(pq, next)\n\t\t\t}\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tpath := make([]*candidateEdge, 0, result.hops)\n\tfor cur := result; cur.node != r.spec.Target; cur = cur.child {\n\t\tif cur.nextEdge == nil || cur.child == nil {\n\t\t\treturn nil, errors.New(\"broken route state\")\n\t\t}\n\t\tpath = append(path, cur.nextEdge)\n\t}\n\n\trt, amounts, err := r.buildRoute(amt, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tloads := make([]lnwire.MilliSatoshi, len(path))\n\tfor i, edge := range path {\n\t\tloads[i] = amounts[i] + reservations[edge.key]\n\t}\n\n\tmeta := &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: amt,\n\t\tfee: rt.TotalAmount - amt,\n\t}\n\tif feeCap != lnwire.MaxMilliSatoshi && meta.fee > feeCap {\n\t\treturn nil, errors.New(\"route exceeds fee budget\")\n\t}\n\n\treturn &plannedRoute{\n\t\trt: rt,\n\t\tmeta: meta,\n\t\tscore: result.score,\n\t}, nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, []lnwire.MilliSatoshi, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamounts := make([]lnwire.MilliSatoshi, n)\n\texpiries := make([]uint32, n)\n\n\tamounts[n-1] = amt\n\texpiries[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tcharge := nodeFee(path[i], path[i+1], amounts[i+1])\n\t\tif charge > lnwire.MaxMilliSatoshi-amounts[i+1] {\n\t\t\treturn nil, nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + charge\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(path[i+1].timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, edge := range path {\n\t\tforward := amt\n\t\texpiry := finalCltvDelta\n\n\t\tif i < n-1 {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, amounts, nil\n}\n\nfunc addReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tdst[edge.key] += meta.amounts[i]\n\t}\n}\n\nfunc removeReservationSet(dst map[edgeKey]lnwire.MilliSatoshi,\n\tmeta *routeMeta) {\n\n\tfor i, edge := range meta.path {\n\t\tamount := meta.amounts[i]\n\t\tif dst[edge.key] <= amount {\n\t\t\tdelete(dst, edge.key)\n\t\t} else {\n\t\t\tdst[edge.key] -= amount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) syncHeld(inFlight uint32) bool {\n\twanted := int(inFlight)\n\tif wanted >= len(r.held) {\n\t\treturn false\n\t}\n\n\tdrop := len(r.held) - wanted\n\tfor i := 0; i < drop; i++ {\n\t\tremoveReservationSet(r.reserved, r.held[i])\n\t}\n\n\tr.held = append([]*routeMeta(nil), r.held[drop:]...)\n\treturn true\n}\n\nfunc candidatePartCounts(slots uint32) []int {\n\tif slots == 0 {\n\t\treturn nil\n\t}\n\n\tlimit := int(slots)\n\tif limit > maxPlannedParts {\n\t\tlimit = maxPlannedParts\n\t}\n\n\tseen := make(map[int]bool)\n\tresult := make([]int, 0, 5)\n\n\tadd := func(parts int) {\n\t\tif parts < 1 || parts > limit || seen[parts] {\n\t\t\treturn\n\t\t}\n\n\t\tseen[parts] = true\n\t\tresult = append(result, parts)\n\t}\n\n\tadd(1)\n\tadd(2)\n\tadd(4)\n\tadd(8)\n\tadd(limit)\n\n\treturn result\n}\n\nfunc plannedShard(remaining lnwire.MilliSatoshi, partsLeft,\n\tmode int) lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn remaining\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(partsLeft)\n\tfair := (remaining + divisor - 1) / divisor\n\tshard := fair\n\n\tswitch mode {\n\tcase 1:\n\t\tshard = fair * 3 / 2\n\n\tcase 2:\n\t\tshard = fair * 3 / 4\n\t}\n\n\tmaxShard := remaining - lnwire.MilliSatoshi(partsLeft-1)\n\tif shard > maxShard {\n\t\tshard = maxShard\n\t}\n\tif shard <= 0 {\n\t\tshard = 1\n\t}\n\n\treturn shard\n}\n\nfunc proportionalFeeCap(feeLeft, shard,\n\tremaining lnwire.MilliSatoshi, partsLeft int) lnwire.MilliSatoshi {\n\n\tif feeLeft == lnwire.MaxMilliSatoshi || partsLeft <= 1 {\n\t\treturn feeLeft\n\t}\n\tif feeLeft <= 0 || shard <= 0 || remaining <= 0 {\n\t\treturn 0\n\t}\n\n\tproportional := lnwire.MilliSatoshi(\n\t\tfloat64(feeLeft) * float64(shard) / float64(remaining),\n\t)\n\tallowance := feeLeft / lnwire.MilliSatoshi(partsLeft*5)\n\n\tif proportional > feeLeft-allowance {\n\t\treturn feeLeft\n\t}\n\n\treturn proportional + allowance\n}\n\nfunc (r *candidateRouter) greedyPlan(total lnwire.MilliSatoshi,\n\tparts, mode int,\n\tfeeBudget lnwire.MilliSatoshi) ([]*plannedRoute, float64, bool) {\n\n\tif parts <= 0 || total < lnwire.MilliSatoshi(parts) {\n\t\treturn nil, 0, false\n\t}\n\n\treservations := cloneReservations(r.reserved)\n\tfeeLeft := feeBudget\n\tremaining := total\n\tplan := make([]*plannedRoute, 0, parts)\n\tscore := float64(0)\n\n\tfor index := 0; index < parts; index++ {\n\t\tpartsLeft := parts - index\n\t\tshard := plannedShard(remaining, partsLeft, mode)\n\t\tif shard <= 0 || shard > remaining {\n\t\t\treturn nil, 0, false\n\t\t}\n\t\tif remaining-shard <\n\t\t\tlnwire.MilliSatoshi(partsLeft-1) {\n\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\trouteFeeCap := proportionalFeeCap(\n\t\t\tfeeLeft, shard, remaining, partsLeft,\n\t\t)\n\t\tpr, err := r.findRoute(\n\t\t\tshard, reservations, routeFeeCap,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, 0, false\n\t\t}\n\n\t\tif feeLeft != lnwire.MaxMilliSatoshi {\n\t\t\tif pr.meta.fee > feeLeft {\n\t\t\t\treturn nil, 0, false\n\t\t\t}\n\t\t\tfeeLeft -= pr.meta.fee\n\t\t}\n\n\t\taddReservationSet(reservations, pr.meta)\n\t\tplan = append(plan, pr)\n\t\tscore += pr.score\n\t\tif index > 0 {\n\t\t\tscore += extraPartCostMsat\n\t\t}\n\t\tremaining -= shard\n\t}\n\n\tif remaining != 0 {\n\t\treturn nil, 0, false\n\t}\n\n\treturn plan, score, true\n}\n\nfunc (r *candidateRouter) makePlan(total lnwire.MilliSatoshi,\n\tslots uint32) ([]*plannedRoute, error) {\n\n\tif slots == 0 {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tfeeBudget := r.remainingFeeBudget()\n\tif feeBudget == 0 && r.spec.FeeLimitMsat != lnwire.MaxMilliSatoshi {\n\t\treturn nil, errors.New(\"fee budget exhausted\")\n\t}\n\n\tvar best []*plannedRoute\n\tbestScore := math.Inf(1)\n\n\tfor _, parts := range candidatePartCounts(slots) {\n\t\tfor mode := 0; mode < 3; mode++ {\n\t\t\tif parts == 1 && mode > 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tplan, score, ok := r.greedyPlan(\n\t\t\t\ttotal, parts, mode, feeBudget,\n\t\t\t)\n\t\t\tif ok && score < bestScore {\n\t\t\t\tbest = plan\n\t\t\t\tbestScore = score\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(best) == 0 {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn best, nil\n}\n\nfunc queuedAmount(plan []*plannedRoute) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\n\tfor _, pr := range plan {\n\t\tif pr.meta.delivered > lnwire.MaxMilliSatoshi-total {\n\t\t\treturn lnwire.MaxMilliSatoshi\n\t\t}\n\t\ttotal += pr.meta.delivered\n\t}\n\n\treturn total\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid remaining amount\")\n\t}\n\tif r.attempts >= maxRouterAttempts {\n\t\treturn nil, errors.New(\"attempt limit reached\")\n\t}\n\tif r.consecutiveMisses >= maxConsecutiveMisses {\n\t\treturn nil, errors.New(\"no progress after repeated failures\")\n\t}\n\n\tif r.syncHeld(inFlightHtlcs) {\n\t\tr.plan = nil\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum parts exhausted\")\n\t}\n\n\tslots := maxParts - inFlightHtlcs\n\tif len(r.plan) > 0 && queuedAmount(r.plan) != amt {\n\t\tr.plan = nil\n\t}\n\n\tif len(r.plan) == 0 {\n\t\tplan, err := r.makePlan(amt, slots)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.plan = plan\n\t}\n\n\tif len(r.plan) == 0 {\n\t\treturn nil, errors.New(\"empty route plan\")\n\t}\n\n\tnext := r.plan[0]\n\tr.plan = r.plan[1:]\n\tif next == nil || next.rt == nil || next.meta == nil ||\n\t\tnext.meta.delivered <= 0 {\n\n\t\tr.plan = nil\n\t\treturn nil, errors.New(\"invalid route plan\")\n\t}\n\n\tr.issued[next.rt] = next.meta\n\tr.attempts++\n\n\treturn next.rt, nil\n}\n\nfunc (r *candidateRouter) metaForRoute(rt *route.Route) *routeMeta {\n\tif meta, ok := r.issued[rt]; ok {\n\t\treturn meta\n\t}\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\tpath := make([]*candidateEdge, 0, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tloads := make([]lnwire.MilliSatoshi, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkey := edgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tedge := r.edgeLookup[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tamount := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamount = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tamounts = append(amounts, amount)\n\t\tloads = append(loads, amount+r.reserved[key])\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\treturn &routeMeta{\n\t\tpath: path,\n\t\tamounts: amounts,\n\t\tloads: loads,\n\t\tdelivered: delivered,\n\t\tfee: rt.TotalAmount - delivered,\n\t}\n}\n\nfunc (r *candidateRouter) recordLocalFailure(key edgeKey,\n\tload lnwire.MilliSatoshi) {\n\n\tif load <= 0 {\n\t\treturn\n\t}\n\n\tupper := r.localUpper[key]\n\tif upper == 0 || load < upper {\n\t\tr.localUpper[key] = load\n\t}\n\tr.localFails[key]++\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(rt *route.Route,\n\tresult routing.SimHtlcResult) int {\n\n\tif rt == nil {\n\t\treturn -1\n\t}\n\tif result.FailureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\n\tmeta := r.metaForRoute(rt)\n\tdelete(r.issued, rt)\n\tif meta == nil {\n\t\tr.plan = nil\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, edge := range meta.path {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[i], true, 0.78, now,\n\t\t\t)\n\t\t\tr.edgePenalty[edge.key] *= 0.15\n\t\t\tr.liquiditySuspects[edge.key] *= 0.20\n\t\t\tdelete(r.policySuspects, edge.key)\n\n\t\t\tif upper := r.localUpper[edge.key];\n\t\t\t\tupper > 0 && meta.loads[i] >= upper {\n\n\t\t\t\tdelete(r.localUpper, edge.key)\n\t\t\t\tdelete(r.localFails, edge.key)\n\t\t\t}\n\t\t}\n\n\t\taddReservationSet(r.reserved, meta)\n\t\tr.held = append(r.held, meta)\n\n\t\tif meta.fee <= lnwire.MaxMilliSatoshi-r.committedFees {\n\t\t\tr.committedFees += meta.fee\n\t\t} else {\n\t\t\tr.committedFees = lnwire.MaxMilliSatoshi\n\t\t}\n\n\t\tr.consecutiveMisses = 0\n\t\treturn nil\n\t}\n\n\tr.plan = nil\n\tr.consecutiveMisses++\n\n\treportedIdx := r.failureEdgeIndex(rt, result)\n\tif reportedIdx < 0 {\n\t\treturn nil\n\t}\n\n\tfailIdx := reportedIdx\n\tif failIdx >= len(meta.path) {\n\t\tfailIdx = len(meta.path) - 1\n\t}\n\tif failIdx < 0 || failIdx >= len(meta.path) {\n\t\treturn nil\n\t}\n\n\tedge := meta.path[failIdx]\n\tcode := result.Failure.Code()\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.liquiditySuspects[edge.key]++\n\t\tsuspicion := r.liquiditySuspects[edge.key]\n\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t1_500_000 + suspicion*400_000\n\n\t\tif failIdx > 0 {\n\t\t\tneighbor := meta.path[failIdx-1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\t\tif failIdx+1 < len(meta.path) {\n\t\t\tneighbor := meta.path[failIdx+1]\n\t\t\tr.liquiditySuspects[neighbor.key] += 0.15\n\t\t\tr.edgePenalty[neighbor.key] += 100_000\n\t\t}\n\n\t\tif suspicion >= 1.8 {\n\t\t\tupdateBelief(\n\t\t\t\tedge.key, meta.loads[failIdx], false, 0.42, now,\n\t\t\t)\n\t\t\tr.recordLocalFailure(\n\t\t\t\tedge.key, meta.loads[failIdx],\n\t\t\t)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policySuspects[edge.key]++\n\t\tcount := r.policySuspects[edge.key]\n\t\tr.edgePenalty[edge.key] +=\n\t\t\t2_700_000 + float64(count)*1_100_000\n\n\t\tif count >= 2 {\n\t\t\tr.blocked[edge.key] = true\n\t\t}\n\n\tdefault:\n\t\tr.edgePenalty[edge.key] += 700_000\n\t}\n\n\treturn nil\n}"
}
}
]
}