lnd/simulation/command-center/data/run.json

568 lines
No EOL
1.4 MiB

{
"run_id": "code_split_opus1",
"reflection_lm": "codex:gpt-5.6-sol",
"mode": "generalization",
"status": "complete",
"seed_score": 0.7746,
"best_score": 0.9432,
"iterations": [
{
"i": 0,
"candidate_score": 0.7746,
"best_score": 0.7746,
"note": "seed"
},
{
"i": 1,
"candidate_score": 0.0,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 2,
"candidate_score": 0.0,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 3,
"candidate_score": 0.5057,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 4,
"candidate_score": 0.6843,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 5,
"candidate_score": 0.6124,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 6,
"candidate_score": 0.507,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 7,
"candidate_score": 0.4918,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 8,
"candidate_score": 0.5158,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 9,
"candidate_score": 0.7396,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 10,
"candidate_score": 0.4224,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 11,
"candidate_score": 0.6867,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 12,
"candidate_score": 0.9059,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 13,
"candidate_score": 0.7642,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 14,
"candidate_score": 0.5048,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 15,
"candidate_score": 0.6911,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 16,
"candidate_score": 0.6468,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 17,
"candidate_score": 0.5024,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 18,
"candidate_score": 0.7734,
"best_score": 0.7746,
"note": "accepted"
},
{
"i": 19,
"candidate_score": 0.0,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 20,
"candidate_score": 0.7723,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 21,
"candidate_score": 0.7206,
"best_score": 0.7746,
"note": "rejected"
},
{
"i": 22,
"candidate_score": 0.8052,
"best_score": 0.8052,
"note": "accepted"
},
{
"i": 23,
"candidate_score": 0.7505,
"best_score": 0.8052,
"note": "accepted"
},
{
"i": 24,
"candidate_score": 0.5412,
"best_score": 0.8052,
"note": "rejected"
},
{
"i": 25,
"candidate_score": 0.6335,
"best_score": 0.8052,
"note": "rejected"
},
{
"i": 26,
"candidate_score": 0.6662,
"best_score": 0.8052,
"note": "rejected"
},
{
"i": 27,
"candidate_score": 0.744,
"best_score": 0.8052,
"note": "rejected"
},
{
"i": 28,
"candidate_score": 0.8717,
"best_score": 0.8717,
"note": "accepted"
},
{
"i": 29,
"candidate_score": 0.6601,
"best_score": 0.8717,
"note": "rejected"
},
{
"i": 30,
"candidate_score": 0.0,
"best_score": 0.8717,
"note": "accepted"
},
{
"i": 31,
"candidate_score": 0.0,
"best_score": 0.8717,
"note": "accepted"
},
{
"i": 32,
"candidate_score": 0.7868,
"best_score": 0.8717,
"note": "rejected"
},
{
"i": 33,
"candidate_score": 0.7223,
"best_score": 0.8717,
"note": "rejected"
}
],
"seed_params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds and\n// no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): the planner always\n// builds a full COVER of the remaining amount over several disjoint\n// corridors, each shard sized to that corridor's believed bottleneck,\n// and only accepts it when the cover is complete enough to be worth\n// starting. This is the main change over the previous generation, which\n// preferred a single big shard and only fell back to a split after that\n// shard had already failed. The eval data showed exactly that pathology:\n// large payments burning 15-20 attempts on descending single corridors\n// and then terminating with \"no route found\".\n// - AGGRESSIVE PART USE for large payments: when the remaining amount is\n// large relative to what any corridor can bear, we go straight to many\n// small-ish shards instead of probing halves.\n// - Depth-aware lower retries instead of blacklisting.\n// - Duplicate-attempt suppression on (path, amount).\n// - Non-liquidity failures (fee, cltv, min, disabled) are repaired in the\n// local policy view rather than treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\tmaxRouteHops = 7\n\n\tmaxAttempts = 80\n\tmaxFailStreak = 26\n\n\tprobeBudget = 12\n\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear when SIZING a shard.\n\tpriorSafeNum = 33\n\tpriorSafeDen = 100\n\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\tqueueMinProb = 0.15\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\tokAmt lnwire.MilliSatoshi\n\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\tsucc bool\n\tdrained lnwire.MilliSatoshi\n\n\tinFlight lnwire.MilliSatoshi\n\n\tmisses int\n\tdead bool\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\tlow := math.Exp(-x * 3.2)\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tinEdges map[route.Vertex][]*edge\n\tbyKey map[edgeKey]*edge\n\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan.\n\tqueued []*plan\n\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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 (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push over\n// an edge. Zero means unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can bear with decent\n// probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every hop's\n// policy and belief bounds.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the delivered amount a corridor could bear if every hop\n// were as full as our HARD bounds allow. Used to decide whether a corridor is\n// worth probing at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with a\n// bonus for finishing the payment outright, a light fee penalty, and a cost\n// for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// bestOnPath prices a corridor at descending amounts starting from hi and\n// returns the best novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount. It returns the corridor\n// and the largest ladder amount it was found for.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// flowPlan is a full joint route-set: a set of disjoint corridors with shard\n// amounts, plus how much of the requested amount they cover in total.\ntype flowPlan struct {\n\tshards []*plan\n\tcovered lnwire.MilliSatoshi\n}\n\n// planFlow decomposes the remaining amount over up to partsLeft DISJOINT\n// corridors, sizing each shard to what that corridor's weakest hop is\n// believed able to bear. This is the min-cost-flow style joint plan: unequal\n// parallel corridors each get a shard that fits.\n//\n// cap0 optionally limits the FIRST shard, which lets the caller ask for a\n// plan whose shards are all \"small\" (many-parts mode for large payments).\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool, shardCap lnwire.MilliSatoshi) *flowPlan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tfp := &flowPlan{}\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif shardCap > 0 && shardCap < want {\n\t\t\twant = shardCap\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor exists but is believed too thin; try to\n\t\t\t// price it at its hard bound instead of dropping it.\n\t\t\tif hb := r.hardBottleneck(path); hb >= minShard {\n\t\t\t\tamtS = hb\n\t\t\t\tif amtS > want {\n\t\t\t\t\tamtS = want\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfp.shards = append(fp.shards, pl)\n\t\tfp.covered += pl.amt\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\tif len(fp.shards) == 0 {\n\t\treturn nil\n\t}\n\tif fp.covered > remaining {\n\t\tfp.covered = remaining\n\t}\n\treturn fp\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next still-valid shard from a joint plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// planSingle searches jointly over shard amount and corridor for the single\n// best next attempt.\nfunc (r *router) planSingle(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard.\n//\n// Strategy: serve any queued shard from an existing joint plan first. Then\n// compute the best single attempt AND a full joint route-set cover of the\n// remaining amount. If the cover reaches materially further than the single\n// attempt, commit to the cover: its first shard goes out now and the rest are\n// queued. When even the greedy cover falls short, try a many-parts cover with\n// a smaller per-shard cap, which is what actually rescues large payments over\n// a network of unequal corridors.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\tbest := r.planSingle(amt, partsLeft, busy)\n\n\t// Joint route-set planning. Always consider it when we may split, not\n\t// only when the single best plan falls short: an unequal cover over\n\t// several corridors is frequently better than one large risky shard.\n\tif partsLeft > 1 {\n\t\tvar cands []*flowPlan\n\n\t\tif fp := r.planFlow(amt, partsLeft, busy, 0); fp != nil {\n\t\t\tcands = append(cands, fp)\n\t\t}\n\n\t\t// Many-parts covers: cap each shard so the greedy walk spreads\n\t\t// over more corridors rather than betting the bulk of the\n\t\t// payment on the first (widest-looking) one. This is what\n\t\t// large payments over unequal corridors need.\n\t\tneed := amt\n\t\tif best != nil && best.amt >= amt {\n\t\t\tneed = 0\n\t\t}\n\t\tif need > 0 {\n\t\t\tfor _, div := range []uint32{2, 3, 5, 8} {\n\t\t\t\tif div > partsLeft {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcapS := amt / lnwire.MilliSatoshi(div)\n\t\t\t\tif capS < minShard {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfp := r.planFlow(\n\t\t\t\t\tamt, partsLeft, busy, capS,\n\t\t\t\t)\n\t\t\t\tif fp != nil {\n\t\t\t\t\tcands = append(cands, fp)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar pick *flowPlan\n\t\tfor _, fp := range cands {\n\t\t\tif pick == nil {\n\t\t\t\tpick = fp\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Prefer the cover that reaches furthest; break ties\n\t\t\t// on fewer shards and better first-shard score.\n\t\t\tswitch {\n\t\t\tcase fp.covered > pick.covered+pick.covered/64:\n\t\t\t\tpick = fp\n\t\t\tcase pick.covered > fp.covered+fp.covered/64:\n\t\t\tcase len(fp.shards) < len(pick.shards):\n\t\t\t\tpick = fp\n\t\t\tcase len(fp.shards) == len(pick.shards) &&\n\t\t\t\tfp.shards[0].score > pick.shards[0].score:\n\t\t\t\tpick = fp\n\t\t\t}\n\t\t}\n\n\t\tif pick != nil {\n\t\t\tfirst := pick.shards[0]\n\t\t\ttake := false\n\t\t\tswitch {\n\t\t\tcase best == nil:\n\t\t\t\ttake = true\n\t\t\tcase best.amt >= amt:\n\t\t\t\t// Single shard already covers everything: only\n\t\t\t\t// switch if it is genuinely unlikely and the\n\t\t\t\t// cover is confident.\n\t\t\t\ttake = best.prob < 0.35 &&\n\t\t\t\t\tpick.covered >= amt &&\n\t\t\t\t\tfirst.prob > best.prob*1.4\n\t\t\tdefault:\n\t\t\t\t// Prefer the plan that covers more of the\n\t\t\t\t// remaining amount; a slightly worse first\n\t\t\t\t// shard is fine if the plan as a whole covers\n\t\t\t\t// materially more.\n\t\t\t\ttake = pick.covered >\n\t\t\t\t\tbest.amt+best.amt/16 ||\n\t\t\t\t\t(pick.covered >= best.amt &&\n\t\t\t\t\t\tfirst.score >\n\t\t\t\t\t\t\tbest.score*0.9)\n\t\t\t}\n\n\t\t\tif take {\n\t\t\t\tr.queued = pick.shards[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded but\n// whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote hops\n\t// so we stop re-picking this corridor without destroying hard bounds.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\t// A policy repair does not mean the corridor is bad, so do not\n\t\t// let it count against the progress budget.\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\tb.succ = false\n\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
},
"best_candidate": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - COMPLEMENTARY REASONING across the two sides of a channel: liquidity\n// proven to sit on the reverse side cannot also sit on this side, and a\n// reverse-direction failure at a small amount means the reverse side is\n// empty, so under bimodality nearly the whole channel sits on THIS side.\n// - JOINT ROUTE-SET PLANNING with a real flow budget: for a large payment\n// we decompose the remainder over DISJOINT corridors up front, each\n// shard sized to its own corridor's bottleneck, and queue the rest.\n//\n// What changed relative to the previous generation (the observed failure was\n// a huge payment burning all 64 attempts and then reporting \"attempt budget\n// exhausted\", i.e. the retry budget drained on a payment whose split plan\n// never converged):\n//\n// 1. FLOW FEASIBILITY GATE. Before spending attempts we compute the total\n// believed deliverable flow over disjoint corridors. If that aggregate\n// cannot cover the remainder AND we have already confirmed dryness by\n// failing, we give up immediately instead of grinding out 60 doomed\n// probes. Giving up early on an impossible payment costs nothing in\n// success rate and saves the whole retry penalty.\n// 2. COMMITTED PLANS SURVIVE A FAILURE. The old code cleared the entire\n// queued plan on every failure, so a 6-shard decomposition was thrown\n// away the moment one shard missed and was then rediscovered from\n// scratch, one expensive Dijkstra ladder at a time. Now a failure only\n// invalidates queued shards that actually touch the failing direction;\n// the rest are re-validated lazily when served.\n// 3. PER-DIRECTION ATTEMPT LEDGER. Every liquidity failure is charged to a\n// direction, and a direction that has failed maxDryProbes times is\n// excluded from path finding for the remainder of the payment rather\n// than being re-probed at ever smaller amounts. This is the single\n// biggest source of wasted attempts.\n// 4. GLOBAL PROBE BUDGET. Dijkstra runs are accounted against a whole-\n// payment budget, not just a per-call one, so late calls degrade to\n// cheap single-probe behaviour instead of re-running full ladders.\n// 5. SHARD FLOOR TIED TO THE REMAINDER. Shards below remainder/maxParts\n// cannot finish the payment, so planning them just burns parts. The\n// ladder now refuses to go below that floor while parts remain scarce,\n// which both cuts attempts and raises the chance the split closes.\n// 6. POLICY REPAIRS ARE NOT FAILURES. A FeeInsufficient or cltv failure is\n// a stale-gossip repair, not evidence of dryness, so it no longer\n// counts against the give-up streak.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 48\n\tmaxFailStreak = 12\n\n\t// probeBudget caps Dijkstra runs inside one RequestRoute call, and\n\t// totalProbeBudget caps them across the whole payment so that a long\n\t// payment cannot spend unbounded search effort.\n\tprobeBudget = 10\n\ttotalProbeBudget = 220\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we bank on.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before it is excluded for the rest of the payment.\n\tmaxDryProbes = 2\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 14\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting belief-derived budgets for a give-up decision.\n\thopelessStreak = 3\n\n\t// feasStreak is how many consecutive failures we require before the\n\t// aggregate-flow feasibility gate is allowed to abandon a payment.\n\tfeasStreak = 5\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// exhausted reports whether this direction has come up dry so often that\n// probing it again is a waste of an attempt.\nfunc (b *belief) exhausted() bool {\n\treturn b.dead || (b.hasFail && b.fails >= maxDryProbes)\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\tprobes 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After maxDryProbes dry probes we stop\n// entirely: grinding the same direction down by halves is the main source of\n// wasted retries.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds. It\n// combines a demonstrated forward (less what we have since drained) with the\n// complementary inference that a reverse-direction failure at a small amount\n// means the funds are on THIS side.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif b != nil && b.exhausted() {\n\t\t\t// This direction has come up dry repeatedly. Probing\n\t\t\t// it again is what drains the attempt budget, so it is\n\t\t\t// retired for the rest of the payment.\n\t\t\tif b.okAmt == 0 {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn b.okAmt\n\t\t}\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// A direction retired after repeated dry probes only clears amounts we\n\t// have actually proven.\n\tif b.exhausted() && eff > b.okAmt {\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.probes >= totalProbeBudget {\n\t\treturn nil, errors.New(\"probe budget exhausted\")\n\t}\n\tr.probes++\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance, which bounds one\n// shard: a shard leaves through exactly one first hop.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// shardFloor is the smallest shard worth planning: anything far below\n// remaining/partsLeft cannot close the payment with the parts we have left,\n// so planning it just burns a part and an attempt.\nfunc (r *router) shardFloor(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\treturn remaining\n\t}\n\tf := remaining / lnwire.MilliSatoshi(partsLeft)\n\n\t// Leave headroom: a shard under the even split can still be\n\t// compensated by a larger sibling.\n\tf = f / 3\n\tif f < minShard {\n\t\tf = minShard\n\t}\n\treturn f\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent, exact\n// local channel balances, and evidence-derived sizes just below proven\n// failure points. Nothing below the floor is offered.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tfloor := r.shardFloor(remaining, partsLeft)\n\tif floor > hi {\n\t\tfloor = minShard\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < floor || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent down to the floor.\n\tcur := hi\n\tfor i := 0; i < 14; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= floor {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= floor {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail && b.fails < maxDryProbes {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= floor {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// touches reports whether a plan's corridor uses the given channel.\nfunc (p *plan) touches(chanID uint64) bool {\n\tfor _, e := range p.path {\n\t\tif e.chanID == chanID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 5 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It keeps going until either the whole remainder is covered or no further\n// disjoint corridor exists, and it caps each shard at the largest single\n// local channel so a shard is never planned larger than one first hop can\n// carry.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free right now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push. Our balances are exact, so this catches payments\n// that no amount of retrying could ever complete, which is where the retry\n// budget used to drain away.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// dropStalePlans discards queued shards whose corridor contains the channel\n// that just failed. The rest of a joint plan is still the best decomposition\n// we know of, so throwing it away wholesale (as an earlier generation did)\n// only forces us to rediscover it one expensive ladder at a time.\nfunc (r *router) dropStalePlans(chanID uint64) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif pl.touches(chanID) {\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// serveQueued hands out the next queued shard that still holds up against\n// current beliefs, re-pricing it at the amount actually remaining.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, split bool,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when the payment cannot be split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tif rt := r.serveQueued(amt, split, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. We\n\t// compare the aggregate believed flow of the plan set against the best\n\t// single shard: covering more of the payment beats a slightly nicer\n\t// first hop, because every uncovered millisat is a failed payment.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\n\t\t\t// FEASIBILITY GATE. Once we have failed enough times to\n\t\t\t// trust our beliefs, a decomposition that still cannot\n\t\t\t// cover the remainder means the payment is not going to\n\t\t\t// complete. Abandoning it here saves dozens of doomed\n\t\t\t// attempts, which is pure retry-efficiency gain with no\n\t\t\t// success-rate cost.\n\t\t\tif r.failStreak >= feasStreak && total < amt &&\n\t\t\t\tinFlightHtlcs == 0 {\n\n\t\t\t\treturn nil, errors.New(\n\t\t\t\t\t\"believed flow cannot cover remainder\",\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t} else if best == nil && inFlightHtlcs == 0 &&\n\t\t\tr.failStreak >= feasStreak {\n\n\t\t\treturn nil, errors.New(\"no corridor left\")\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a descending amount\n// ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable, and it is what\n// feeds the complementary bounds on the reverse direction.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread suspicion over the remote hops so\n\t// we stop re-picking this corridor without destroying the hard bounds\n\t// we have earned. Only queued shards sharing a channel with this\n\t// corridor are invalidated.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\t// Optimism earned from older evidence cannot survive a\n\t\t\t// corridor that just failed somewhere inside it.\n\t\t\tb.succ = false\n\t\t\tr.dropStalePlans(h.ChannelID)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt. A\n\t\t// policy repair is not evidence of dryness, so it must not\n\t\t// count against the give-up streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.dropStalePlans(h.ChannelID)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tb.drained = 0\n\n\t// Only the queued shards routing through this now-tightened direction\n\t// are stale; the rest of the joint plan still stands.\n\tr.dropStalePlans(h.ChannelID)\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
},
"stats": {
"evals_done": 26,
"distinct_candidates": 34
},
"candidates": [
{
"id": 0,
"parent": null,
"score": 0.7746,
"accepted": true,
"frontier": true,
"role": "seed",
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds and\n// no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): the planner always\n// builds a full COVER of the remaining amount over several disjoint\n// corridors, each shard sized to that corridor's believed bottleneck,\n// and only accepts it when the cover is complete enough to be worth\n// starting. This is the main change over the previous generation, which\n// preferred a single big shard and only fell back to a split after that\n// shard had already failed. The eval data showed exactly that pathology:\n// large payments burning 15-20 attempts on descending single corridors\n// and then terminating with \"no route found\".\n// - AGGRESSIVE PART USE for large payments: when the remaining amount is\n// large relative to what any corridor can bear, we go straight to many\n// small-ish shards instead of probing halves.\n// - Depth-aware lower retries instead of blacklisting.\n// - Duplicate-attempt suppression on (path, amount).\n// - Non-liquidity failures (fee, cltv, min, disabled) are repaired in the\n// local policy view rather than treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\tmaxRouteHops = 7\n\n\tmaxAttempts = 80\n\tmaxFailStreak = 26\n\n\tprobeBudget = 12\n\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear when SIZING a shard.\n\tpriorSafeNum = 33\n\tpriorSafeDen = 100\n\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\tqueueMinProb = 0.15\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\tokAmt lnwire.MilliSatoshi\n\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\tsucc bool\n\tdrained lnwire.MilliSatoshi\n\n\tinFlight lnwire.MilliSatoshi\n\n\tmisses int\n\tdead bool\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\tlow := math.Exp(-x * 3.2)\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tinEdges map[route.Vertex][]*edge\n\tbyKey map[edgeKey]*edge\n\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan.\n\tqueued []*plan\n\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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 (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push over\n// an edge. Zero means unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can bear with decent\n// probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every hop's\n// policy and belief bounds.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the delivered amount a corridor could bear if every hop\n// were as full as our HARD bounds allow. Used to decide whether a corridor is\n// worth probing at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with a\n// bonus for finishing the payment outright, a light fee penalty, and a cost\n// for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// bestOnPath prices a corridor at descending amounts starting from hi and\n// returns the best novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount. It returns the corridor\n// and the largest ladder amount it was found for.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// flowPlan is a full joint route-set: a set of disjoint corridors with shard\n// amounts, plus how much of the requested amount they cover in total.\ntype flowPlan struct {\n\tshards []*plan\n\tcovered lnwire.MilliSatoshi\n}\n\n// planFlow decomposes the remaining amount over up to partsLeft DISJOINT\n// corridors, sizing each shard to what that corridor's weakest hop is\n// believed able to bear. This is the min-cost-flow style joint plan: unequal\n// parallel corridors each get a shard that fits.\n//\n// cap0 optionally limits the FIRST shard, which lets the caller ask for a\n// plan whose shards are all \"small\" (many-parts mode for large payments).\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool, shardCap lnwire.MilliSatoshi) *flowPlan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tfp := &flowPlan{}\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif shardCap > 0 && shardCap < want {\n\t\t\twant = shardCap\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor exists but is believed too thin; try to\n\t\t\t// price it at its hard bound instead of dropping it.\n\t\t\tif hb := r.hardBottleneck(path); hb >= minShard {\n\t\t\t\tamtS = hb\n\t\t\t\tif amtS > want {\n\t\t\t\t\tamtS = want\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfp.shards = append(fp.shards, pl)\n\t\tfp.covered += pl.amt\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\tif len(fp.shards) == 0 {\n\t\treturn nil\n\t}\n\tif fp.covered > remaining {\n\t\tfp.covered = remaining\n\t}\n\treturn fp\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next still-valid shard from a joint plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// planSingle searches jointly over shard amount and corridor for the single\n// best next attempt.\nfunc (r *router) planSingle(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard.\n//\n// Strategy: serve any queued shard from an existing joint plan first. Then\n// compute the best single attempt AND a full joint route-set cover of the\n// remaining amount. If the cover reaches materially further than the single\n// attempt, commit to the cover: its first shard goes out now and the rest are\n// queued. When even the greedy cover falls short, try a many-parts cover with\n// a smaller per-shard cap, which is what actually rescues large payments over\n// a network of unequal corridors.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\tbest := r.planSingle(amt, partsLeft, busy)\n\n\t// Joint route-set planning. Always consider it when we may split, not\n\t// only when the single best plan falls short: an unequal cover over\n\t// several corridors is frequently better than one large risky shard.\n\tif partsLeft > 1 {\n\t\tvar cands []*flowPlan\n\n\t\tif fp := r.planFlow(amt, partsLeft, busy, 0); fp != nil {\n\t\t\tcands = append(cands, fp)\n\t\t}\n\n\t\t// Many-parts covers: cap each shard so the greedy walk spreads\n\t\t// over more corridors rather than betting the bulk of the\n\t\t// payment on the first (widest-looking) one. This is what\n\t\t// large payments over unequal corridors need.\n\t\tneed := amt\n\t\tif best != nil && best.amt >= amt {\n\t\t\tneed = 0\n\t\t}\n\t\tif need > 0 {\n\t\t\tfor _, div := range []uint32{2, 3, 5, 8} {\n\t\t\t\tif div > partsLeft {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcapS := amt / lnwire.MilliSatoshi(div)\n\t\t\t\tif capS < minShard {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfp := r.planFlow(\n\t\t\t\t\tamt, partsLeft, busy, capS,\n\t\t\t\t)\n\t\t\t\tif fp != nil {\n\t\t\t\t\tcands = append(cands, fp)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar pick *flowPlan\n\t\tfor _, fp := range cands {\n\t\t\tif pick == nil {\n\t\t\t\tpick = fp\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Prefer the cover that reaches furthest; break ties\n\t\t\t// on fewer shards and better first-shard score.\n\t\t\tswitch {\n\t\t\tcase fp.covered > pick.covered+pick.covered/64:\n\t\t\t\tpick = fp\n\t\t\tcase pick.covered > fp.covered+fp.covered/64:\n\t\t\tcase len(fp.shards) < len(pick.shards):\n\t\t\t\tpick = fp\n\t\t\tcase len(fp.shards) == len(pick.shards) &&\n\t\t\t\tfp.shards[0].score > pick.shards[0].score:\n\t\t\t\tpick = fp\n\t\t\t}\n\t\t}\n\n\t\tif pick != nil {\n\t\t\tfirst := pick.shards[0]\n\t\t\ttake := false\n\t\t\tswitch {\n\t\t\tcase best == nil:\n\t\t\t\ttake = true\n\t\t\tcase best.amt >= amt:\n\t\t\t\t// Single shard already covers everything: only\n\t\t\t\t// switch if it is genuinely unlikely and the\n\t\t\t\t// cover is confident.\n\t\t\t\ttake = best.prob < 0.35 &&\n\t\t\t\t\tpick.covered >= amt &&\n\t\t\t\t\tfirst.prob > best.prob*1.4\n\t\t\tdefault:\n\t\t\t\t// Prefer the plan that covers more of the\n\t\t\t\t// remaining amount; a slightly worse first\n\t\t\t\t// shard is fine if the plan as a whole covers\n\t\t\t\t// materially more.\n\t\t\t\ttake = pick.covered >\n\t\t\t\t\tbest.amt+best.amt/16 ||\n\t\t\t\t\t(pick.covered >= best.amt &&\n\t\t\t\t\t\tfirst.score >\n\t\t\t\t\t\t\tbest.score*0.9)\n\t\t\t}\n\n\t\t\tif take {\n\t\t\t\tr.queued = pick.shards[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded but\n// whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote hops\n\t// so we stop re-picking this corridor without destroying hard bounds.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\t// A policy repair does not mean the corridor is bad, so do not\n\t\t// let it count against the progress budget.\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\tb.succ = false\n\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 1,
"parent": 0,
"score": 0.0,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - COMPLEMENTARY REASONING across the two sides of a channel, which is\n// new here and is where bimodality pays best:\n// * liquidity proven to sit on the reverse side cannot also sit on\n// this side, so capacity minus the reverse lower bound is a hard\n// upper bound on this direction, and\n// * a reverse-direction failure at a small amount means the reverse\n// side is empty, so under a bimodal split nearly the whole channel\n// sits on THIS side: that turns a remote failure into a positive\n// signal for the opposite direction.\n// - Depth-aware lower retries with a per-direction failure counter: a\n// direction that has come up dry three times stops being probed for the\n// rest of the payment instead of being ground down geometrically.\n// - JOINT ROUTE-SET PLANNING with a real max-flow style budget: before\n// handing out any shard for a large payment we compute the believed\n// deliverable flow over disjoint corridors. If the whole remainder can\n// only be covered by K shards, all K are planned up front and queued,\n// each sized to its own corridor's bottleneck.\n// - LOCAL-CHANNEL FLOW CAP: no shard may exceed the largest single local\n// channel balance, since a shard leaves through exactly one first hop;\n// the planner spreads shards across DIFFERENT first hops so a payment\n// larger than any single local channel is decomposed instead of retried.\n// - PROGRESSIVE SALVAGE whenever the payment is allowed to be split at\n// all: partial delivery is progress because a settled shard shrinks the\n// remainder, so salvage is no longer gated on a free concurrent part.\n// - HOPELESS-PAYMENT DETECTION: our own balances are known exactly, so a\n// remainder above everything we can push out of our channels (or, for a\n// single-part payment, above our biggest channel) is abandoned at once\n// rather than ground through dozens of doomed attempts.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 64\n\tmaxFailStreak = 20\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under the bimodal prior this is\n\t// close to the amount maximizing expected delivered value.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity, leaving a\n\t// little slack for drift.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we are willing to bank\n\t// on. A reverse failure at f means the reverse side holds under f, so\n\t// under bimodality this side holds close to capacity minus f.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before we stop probing it lower for this payment.\n\tmaxDryProbes = 3\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 12\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting the belief-derived local budget for a give-up decision.\n\thopelessStreak = 4\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction. Repeated\n\t// misses mean the direction is dry, not that we should keep halving.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After several dry probes we stop entirely:\n// grinding the same direction down by halves is the main source of wasted\n// retries.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side. It\n// returns the channel capacity when we know nothing about the reverse side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds. It\n// combines a demonstrated forward (less what we have since drained) with the\n// complementary inference that a reverse-direction failure at a small amount\n// means the funds are on THIS side.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance, which bounds one\n// shard: a shard leaves through exactly one first hop.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies. Maximizing p*amt is\n// exactly maximizing delivered value per attempt.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It keeps going until either the whole remainder is covered or no further\n// disjoint corridor exists, and it caps each shard at the largest single\n// local channel so a shard is never planned larger than one first hop can\n// carry.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free right now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push. Our balances are exact, so this catches payments\n// that no amount of retrying could ever complete, which is where the retry\n// budget used to drain away.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when the payment cannot be split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. We\n\t// compare the aggregate believed flow of the plan set against the best\n\t// single shard: covering more of the payment beats a slightly nicer\n\t// first hop, because every uncovered millisat is a failed payment.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 22 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable, and it is what\n// feeds the complementary bounds on the reverse direction.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread suspicion over the remote hops so\n\t// we stop re-picking this corridor without destroying the hard bounds\n\t// we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\t// Optimism earned from older evidence cannot survive a\n\t\t\t// corridor that just failed somewhere inside it.\n\t\t\tb.succ = false\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tb.drained = 0\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 2,
"parent": 0,
"score": 0.0,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary (v9):\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds and\n// NO time decay (decay lost to hard bounds in prior experiments).\n// - Balance bookkeeping on settle plus complementary reasoning across the\n// two sides of a channel (bimodality makes a remote failure a positive\n// signal for the opposite direction).\n// - JOINT ROUTE-SET PLANNING: a max-flow style decomposition over disjoint\n// corridors, each shard sized to its own corridor's bottleneck and capped\n// by the local channel it leaves through.\n// - NEW: PROBE-FIRST DISCOVERY for large payments. The dominant observed\n// failure mode was burning the whole attempt budget on one huge payment\n// whose corridors were mostly unattributable (\"failed_at_hop -1\")\n// failures at long hop counts. When the remainder is large relative to\n// what we believe corridors can bear, we deliberately send a SMALL\n// leading shard to learn bounds cheaply, then scale up. Evidence bought\n// at 1/8 the amount costs the same one attempt but tells us as much.\n// - NEW: PER-CORRIDOR ATTEMPT BUDGETS. A corridor (first-hop, target-side\n// pair) that has failed twice is retired for the payment. This is what\n// stops the 64-attempt grind: the old code kept re-deriving nearly the\n// same path from beliefs it had already invalidated.\n// - NEW: UNATTRIBUTABLE-FAILURE HANDLING is much stronger. A -1 failure on\n// an N-hop route is charged as a soft upper bound to every remote hop at\n// that hop's amount, so the corridor genuinely gets cheaper to abandon,\n// and a hop-count penalty grows with the number of long-route misses.\n// - NEW: ADAPTIVE ATTEMPT BUDGET. Attempts are spent where they buy\n// information: the budget shrinks once repeated shards stop moving the\n// remainder, and we give up early rather than at 64.\n// - Progressive salvage, hopeless detection, duplicate suppression and\n// non-liquidity policy repair are all preserved from the prior champion.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\tmaxRouteHops = 6\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying. The\n\t// budget is deliberately tighter than before: past ~30 attempts on one\n\t// payment the marginal success probability observed was ~0, while every\n\t// attempt costs retry efficiency.\n\tmaxAttempts = 34\n\tmaxFailStreak = 12\n\n\tprobeBudget = 12\n\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\tmaxDryProbes = 3\n\n\tqueueMinProb = 0.15\n\n\tflowRounds = 12\n\n\thopelessStreak = 4\n\n\t// corridorBudget is how many failures one corridor may contribute\n\t// before it is retired for the rest of the payment.\n\tcorridorBudget = 2\n\n\t// probeFrac is the fraction of the remainder used for a discovery\n\t// shard when we have no idea what the network can bear.\n\tprobeFracNum = 1\n\tprobeFracDen = 8\n\n\t// stallLimit is how many consecutive attempts may fail without the\n\t// remaining amount shrinking before we stop escalating and start\n\t// shrinking shards aggressively.\n\tstallLimit = 3\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\tokAmt lnwire.MilliSatoshi\n\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\tsucc bool\n\n\tfails int\n\n\tdrained lnwire.MilliSatoshi\n\n\tinFlight lnwire.MilliSatoshi\n\n\t// soft is a soft upper bound from unattributable failures: the\n\t// smallest amount this hop took part in when the corridor failed\n\t// somewhere. It discounts probability without destroying hard bounds.\n\tsoft lnwire.MilliSatoshi\n\thasSoft bool\n\n\tmisses int\n\n\tdead bool\n}\n\n// bimodalPrior is the success probability with no direct evidence.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\tlow := math.Exp(-x * 3.2)\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tinEdges map[route.Vertex][]*edge\n\tbyKey map[edgeKey]*edge\n\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\tpending map[uint64]*route.Route\n\n\tqueued []*plan\n\n\tfailedSigs map[string]bool\n\n\t// corridorFails counts failures charged to a corridor signature so a\n\t// repeatedly failing corridor can be retired for the payment.\n\tcorridorFails map[string]int\n\n\t// retired names first-hop channels that no longer earn attempts.\n\tretired map[uint64]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// longMiss counts unattributable failures, used to penalize long\n\t// routes which are structurally harder to diagnose.\n\tlongMiss int\n\n\t// lastRemaining/stalls track whether attempts are making progress.\n\tlastRemaining lnwire.MilliSatoshi\n\tstalls int\n\n\t// probed records that we have already spent a discovery shard.\n\tprobed 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\n\t\tcorridorFails: make(map[string]int),\n\t\tretired: make(map[uint64]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\tif r.retired[e.chanID] {\n\t\t\treturn 0\n\t\t}\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can bear with decent\n// probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\t// A soft (unattributable) bound caps sizing without becoming a hard\n\t// veto: sizing below it is what actually converts a -1 failure into\n\t// useful information.\n\tif b.hasSoft {\n\t\tlim := b.soft * 3 / 5\n\t\tif lim < est {\n\t\t\test = lim\n\t\t}\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\tif e.from == r.source {\n\t\tif r.retired[e.chanID] {\n\t\t\treturn 0\n\t\t}\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\t// Soft bounds from unattributable failures: amounts at or above the\n\t// soft bound are strongly discounted, amounts well below barely are.\n\tif b.hasSoft && b.soft > 0 {\n\t\tratio := float64(eff) / float64(b.soft)\n\t\tif ratio >= 1 {\n\t\t\tp *= 0.12\n\t\t} else {\n\t\t\tp *= 1 - 0.7*ratio*ratio\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\n\t// Long routes have been the source of unattributable failures, which\n\t// are the most expensive kind of failure because they teach us little.\n\t// Once we have seen several, we buy shorter routes more aggressively.\n\thopMult := 0.2 + 0.25*math.Min(float64(r.longMiss), 4)\n\thopCost := attemptCost * hopMult\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == r.source && r.retired[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t\tif b.hasSoft && b.soft >= minShard {\n\t\t\tadd(b.soft * 3 / 5)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with a\n// bonus for finishing the payment outright, a light fee penalty, and a cost\n// for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// corridorSig identifies a corridor irrespective of amount, so repeated\n// failures on essentially the same path can retire it.\nfunc corridorSig(path []*edge) string {\n\tbuf := make([]byte, 0, len(path)*14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\nfunc routeCorridorSig(rt *route.Route) string {\n\tbuf := make([]byte, 0, len(rt.Hops)*14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// corridorDead reports whether a corridor has spent its attempt budget.\nfunc (r *router) corridorDead(path []*edge) bool {\n\treturn r.corridorFails[corridorSig(path)] >= corridorBudget\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tif r.corridorDead(path) {\n\t\treturn best\n\t}\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\tif r.corridorDead(path) {\n\t\treturn nil\n\t}\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil && !r.corridorDead(p) {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tp, err := r.findPath(a, avoid)\n\t\tif err == nil && !r.corridorDead(p) {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// probeShard returns a small discovery amount when the payment is large\n// enough that a blind full-size attempt is likely to teach us nothing but\n// burn an attempt. Learning bounds at 1/8 the amount costs one attempt and\n// tells us as much about the corridor as failing at full size does; it may\n// also actually settle, shrinking the remainder.\nfunc (r *router) probeShard(remaining lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !r.mppOK() || r.probed {\n\t\treturn 0\n\t}\n\n\t// Only probe when a single corridor plausibly cannot carry the whole\n\t// remainder: that is where blind full-size attempts are wasted.\n\tbud := r.localBudget()\n\tif bud == 0 {\n\t\treturn 0\n\t}\n\tif r.maxLocalEdge() >= remaining && bud >= remaining*2 {\n\t\treturn 0\n\t}\n\n\ta := remaining * probeFracNum / probeFracDen\n\tif lim := r.maxLocalEdge(); lim > 0 && a > lim {\n\t\ta = lim\n\t}\n\tif a < minShard {\n\t\treturn 0\n\t}\n\treturn a\n}\n\n// budgetLeft reports whether we still have attempts worth spending. Once\n// attempts stop moving the remainder we wind down quickly, because retry\n// efficiency is scored and a stalled payment rarely recovers.\nfunc (r *router) budgetLeft() bool {\n\tlimit := maxAttempts\n\tif r.stalls >= stallLimit*2 {\n\t\tlimit = maxAttempts / 2\n\t}\n\treturn r.attempts < limit\n}\n\n// RequestRoute plans the next shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif !r.budgetLeft() {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\t// Progress tracking: a remainder that has not shrunk across attempts\n\t// means we are grinding, not learning.\n\tif r.lastRemaining == amt {\n\t\tr.stalls++\n\t} else {\n\t\tr.stalls = 0\n\t\tr.lastRemaining = amt\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) || r.corridorDead(pl.path) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// Discovery shard: for large payments buy cheap information first.\n\tif pa := r.probeShard(amt); pa > 0 {\n\t\tif path, err := r.findPath(pa, busy); err == nil {\n\t\t\tif pl := r.bestOnPath(path, pa, amt); pl != nil {\n\t\t\t\tr.probed = true\n\t\t\t\tr.attempts++\n\t\t\t\treturn pl.rt, nil\n\t\t\t}\n\t\t}\n\t\tif path, err := r.findPath(pa, nil); err == nil {\n\t\t\tif pl := r.bestOnPath(path, pa, amt); pl != nil {\n\t\t\t\tr.probed = true\n\t\t\t\tr.attempts++\n\t\t\t\treturn pl.rt, nil\n\t\t\t}\n\t\t}\n\t\t// No corridor even for the probe: fall through to the general\n\t\t// planner rather than looping on the probe forever.\n\t\tr.probed = true\n\t}\n\n\t// A single shard can never exceed what one local channel can push.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\t// When attempts have stopped moving the remainder, stop leading with\n\t// the largest amounts: they are exactly what keeps failing.\n\tif split && r.stalls >= stallLimit && len(ladder) > 1 {\n\t\tcut := len(ladder) / 3\n\t\tif cut >= len(ladder) {\n\t\t\tcut = len(ladder) - 1\n\t\t}\n\t\tladder = ladder[cut:]\n\t}\n\n\tvar best *plan\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.corridorDead(path) {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 22 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded but\n// whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\t// A demonstrated forward at a clears soft suspicion at or below a.\n\tif b.hasSoft && b.soft <= a {\n\t\tb.hasSoft = false\n\t\tb.soft = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.hasSoft = false\n\tb.soft = 0\n\tb.misses = 0\n\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\trb.hasSoft = false\n\trb.soft = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies.\nfunc classify(f any) string {\n\tif f == nil {\n\t\treturn \"unknown\"\n\t}\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// noteCorridorFail charges a failure to the corridor and retires first hops\n// that keep coming up dry, which is what bounds the retry count.\nfunc (r *router) noteCorridorFail(rt *route.Route) {\n\tsig := routeCorridorSig(rt)\n\tr.corridorFails[sig]++\n\n\tif len(rt.Hops) == 0 {\n\t\treturn\n\t}\n\n\t// A first hop whose own balance we know to be exhausted is worthless\n\t// for the rest of the payment.\n\tfirst := rt.Hops[0].ChannelID\n\tif bal, ok := r.localBalances[first]; ok && bal < minShard {\n\t\tr.retired[first] = true\n\t}\n}\n\n// softFail charges a soft upper bound to a hop from an unattributable\n// failure. Unlike a hard bound it does not veto the hop; it makes larger\n// amounts over it progressively less attractive, which steers the planner to\n// smaller shards on that corridor instead of re-deriving the same path.\nfunc (r *router) softFail(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif !b.hasSoft || a < b.soft {\n\t\tb.hasSoft = true\n\t\tb.soft = a\n\t}\n\tif b.misses < 3 {\n\t\tb.misses++\n\t}\n\tb.succ = false\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.stalls = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// A settled corridor has earned its budget back.\n\t\tdelete(r.corridorFails, routeCorridorSig(rt))\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\tkind := classify(result.Failure)\n\n\t// Locate the failing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Policy repairs do not consume a corridor's liquidity budget: the\n\t// corridor is fine, our view of its policy was stale.\n\tif failIdx >= 0 && failIdx < len(rt.Hops) {\n\t\tswitch kind {\n\t\tcase \"fee\", \"cltv\", \"min\":\n\t\t\te := r.byKey[hopKey(rt.Hops[failIdx])]\n\t\t\ta := hopAmount(rt, failIdx)\n\t\t\tswitch kind {\n\t\t\tcase \"fee\":\n\t\t\t\tif e != nil {\n\t\t\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t\t\t}\n\t\t\tcase \"cltv\":\n\t\t\t\tif e != nil {\n\t\t\t\t\te.cltv += e.cltv/4 + 20\n\t\t\t\t}\n\t\t\tcase \"min\":\n\t\t\t\tif e != nil && a >= e.minHTLC {\n\t\t\t\t\te.minHTLC = a + 1\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Everything before the failing hop worked.\n\t\t\tfor i := 0; i < failIdx; i++ {\n\t\t\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tr.noteCorridorFail(rt)\n\n\t// Unattributable failure: charge a soft bound to every remote hop at\n\t// the amount it was asked to carry, and remember that long routes are\n\t// where this happens.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tr.longMiss++\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\t// Our own balance is exact; a corridor failing\n\t\t\t\t// past it says nothing about it.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.softFail(h, hopAmount(rt, i))\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\n\tif kind == \"dead\" {\n\t\tb.dead = true\n\t\tif failIdx == 0 {\n\t\t\tr.retired[h.ChannelID] = true\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\tb.succ = false\n\tb.drained = 0\n\t// A hard bound subsumes any soft one.\n\tb.hasSoft = false\n\tb.soft = 0\n\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t\tif r.localBalances[h.ChannelID] < minShard {\n\t\t\tr.retired[h.ChannelID] = true\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 3,
"parent": 0,
"score": 0.5057,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds and\n// no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): the planner builds a\n// full COVER of the remaining amount over several disjoint corridors,\n// each shard sized to that corridor's believed bottleneck.\n// - CAPACITY-AWARE PART BUDGETING (new): before planning, we estimate the\n// total believed deliverable flow and derive the number of parts the\n// payment actually needs, then plan directly at that granularity instead\n// of walking a descending ladder of single corridors. The eval showed a\n// large payment (860M msat) burning 30 attempts and then terminating\n// with \"no route found\"; the fix is to start wide, not narrow.\n// - PERSISTENT SHARD PIPELINE (new): a joint plan is no longer thrown away\n// on every failure. Only the shards affected by newly-learned evidence\n// are re-priced; the rest stay queued. This cuts attempt counts, which\n// were the main secondary penalty (11-12 attempts/scenario).\n// - Depth-aware lower retries instead of blacklisting, with per-corridor\n// re-pricing on the way down.\n// - Duplicate-attempt suppression on (path, amount).\n// - Non-liquidity failures (fee, cltv, min, disabled) are repaired in the\n// local policy view rather than treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\tmaxRouteHops = 7\n\n\tmaxAttempts = 90\n\tmaxFailStreak = 30\n\n\tprobeBudget = 10\n\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\tfeeWeight = 4.0\n\tpartCost = 0.003\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear when SIZING a shard.\n\tpriorSafeNum = 33\n\tpriorSafeDen = 100\n\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\tqueueMinProb = 0.12\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\tokAmt lnwire.MilliSatoshi\n\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\tsucc bool\n\tdrained lnwire.MilliSatoshi\n\n\tinFlight lnwire.MilliSatoshi\n\n\tmisses int\n\tdead bool\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\tlow := math.Exp(-x * 3.2)\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tinEdges map[route.Vertex][]*edge\n\tbyKey map[edgeKey]*edge\n\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan.\n\tqueued []*plan\n\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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 (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push over\n// an edge. Zero means unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can bear with decent\n// probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every hop's\n// policy and belief bounds.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// widestLocal is the largest single local channel balance still free. It is a\n// hard ceiling on any single shard leaving our node.\nfunc (r *router) widestLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the delivered amount a corridor could bear if every hop\n// were as full as our HARD bounds allow. Used to decide whether a corridor is\n// worth probing at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with a\n// bonus for finishing the payment outright, a light fee penalty, and a cost\n// for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// bestOnPath prices a corridor at descending amounts starting from hi and\n// returns the best novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount. It returns the corridor\n// found for the largest workable amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\ta = a * 2 / 3\n\t\tif a < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// flowPlan is a full joint route-set: a set of disjoint corridors with shard\n// amounts, plus how much of the requested amount they cover in total.\ntype flowPlan struct {\n\tshards []*plan\n\tcovered lnwire.MilliSatoshi\n\tfee lnwire.MilliSatoshi\n}\n\n// planFlow decomposes the remaining amount over up to partsLeft DISJOINT\n// corridors, sizing each shard to what that corridor's weakest hop is\n// believed able to bear. This is the min-cost-flow style joint plan: unequal\n// parallel corridors each get a shard that fits.\n//\n// shardCap optionally limits every shard, which lets the caller ask for a\n// plan whose shards are all \"small\" (many-parts mode for large payments).\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool, shardCap lnwire.MilliSatoshi) *flowPlan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tfp := &flowPlan{}\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif shardCap > 0 && shardCap < want {\n\t\t\twant = shardCap\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor exists but is believed too thin; try to\n\t\t\t// price it at its hard bound instead of dropping it.\n\t\t\tif hb := r.hardBottleneck(path); hb >= minShard {\n\t\t\t\tamtS = hb\n\t\t\t\tif amtS > want {\n\t\t\t\t\tamtS = want\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// Corridor unusable at any price: block its narrowest\n\t\t\t// hop so the next probe explores elsewhere.\n\t\t\tblockThinnest(path, avoid, r)\n\t\t\tcontinue\n\t\t}\n\n\t\tfp.shards = append(fp.shards, pl)\n\t\tfp.covered += pl.amt\n\t\tif pl.rt != nil && pl.rt.TotalAmount > pl.amt {\n\t\t\tfp.fee += pl.rt.TotalAmount - pl.amt\n\t\t}\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\tif len(fp.shards) == 0 {\n\t\treturn nil\n\t}\n\tif fp.covered > remaining {\n\t\tfp.covered = remaining\n\t}\n\treturn fp\n}\n\n// blockThinnest marks the weakest hop of a corridor as avoided so the next\n// path search is forced to route around it.\nfunc blockThinnest(path []*edge, avoid map[uint64]bool, r *router) {\n\tvar worst *edge\n\tvar worstCap lnwire.MilliSatoshi\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif worst == nil || c < worstCap {\n\t\t\tworst, worstCap = e, c\n\t\t}\n\t}\n\tif worst != nil {\n\t\tavoid[worst.chanID] = true\n\t}\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next still-valid shard from a joint plan. Shards\n// are re-priced against current beliefs; ones that no longer work are dropped\n// rather than invalidating the whole plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\t// Keep it for later instead of discarding: the\n\t\t\t// corridor is fine, it is merely occupied right now.\n\t\t\tr.queued = append(r.queued, pl)\n\t\t\tif len(r.queued) > 16 {\n\t\t\t\tr.queued = nil\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\t// Re-price the same corridor lower before giving up\n\t\t\t// on it: evidence may have shrunk it, not killed it.\n\t\t\tif lower := r.bestOnPath(\n\t\t\t\tpl.path, a*2/3, amt,\n\t\t\t); lower != nil && lower.prob >= queueMinProb {\n\t\t\t\treturn lower.rt\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// planSingle searches jointly over shard amount and corridor for the single\n// best next attempt.\nfunc (r *router) planSingle(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\thi := amt\n\tif w := r.widestLocal(); w > 0 && w < hi {\n\t\thi = w\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn best\n}\n\n// neededParts estimates how many shards the remaining amount realistically\n// requires, from the widest single corridor we believe exists. Starting at the\n// right granularity is what keeps large payments from burning a dozen attempts\n// walking a descending single-corridor ladder.\nfunc (r *router) neededParts(amt lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) uint32 {\n\n\tw := r.widestLocal()\n\tif w == 0 || w >= amt {\n\t\treturn 1\n\t}\n\t// Probe for the widest corridor we can actually find.\n\tprobe := w\n\tif probe > amt {\n\t\tprobe = amt\n\t}\n\tvar best lnwire.MilliSatoshi\n\tif p := r.findAnyPath(probe, amt, partsLeft, busy); p != nil {\n\t\tif bn := r.bottleneck(p); bn > best {\n\t\t\tbest = bn\n\t\t}\n\t}\n\tif best == 0 {\n\t\tbest = w / 2\n\t}\n\tif best == 0 {\n\t\treturn partsLeft\n\t}\n\tn := uint32(amt/best) + 1\n\tif n < 1 {\n\t\tn = 1\n\t}\n\tif n > partsLeft {\n\t\tn = partsLeft\n\t}\n\treturn n\n}\n\n// RequestRoute plans the next shard.\n//\n// Strategy: serve any queued shard from an existing joint plan first. Then\n// estimate the part count the payment actually needs, and build joint covers\n// at that granularity (plus a couple of neighbouring granularities) as well as\n// the best single attempt. Commit to whichever reaches furthest per unit of\n// fee and part count.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\tbest := r.planSingle(amt, partsLeft, busy)\n\n\tif partsLeft > 1 {\n\t\tvar cands []*flowPlan\n\n\t\tif fp := r.planFlow(amt, partsLeft, busy, 0); fp != nil {\n\t\t\tcands = append(cands, fp)\n\t\t}\n\n\t\t// Capacity-aware granularity: start from the part count the\n\t\t// network actually seems to require, then bracket it. This\n\t\t// replaces blind halving with a directed search.\n\t\tneed := r.neededParts(amt, partsLeft, busy)\n\t\tdivs := []uint32{need}\n\t\tif need > 1 {\n\t\t\tdivs = append(divs, need+1, need*2)\n\t\t} else {\n\t\t\tdivs = append(divs, 2, 3)\n\t\t}\n\t\tif partsLeft >= 4 {\n\t\t\tdivs = append(divs, 4)\n\t\t}\n\t\tif partsLeft >= 8 {\n\t\t\tdivs = append(divs, 8)\n\t\t}\n\n\t\ttried := map[uint32]bool{}\n\t\tfor _, div := range divs {\n\t\t\tif div <= 1 || div > partsLeft || tried[div] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried[div] = true\n\t\t\tcapS := amt / lnwire.MilliSatoshi(div)\n\t\t\tif capS < minShard {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif fp := r.planFlow(\n\t\t\t\tamt, partsLeft, busy, capS,\n\t\t\t); fp != nil {\n\t\t\t\tcands = append(cands, fp)\n\t\t\t}\n\t\t}\n\n\t\tvar pick *flowPlan\n\t\tfor _, fp := range cands {\n\t\t\tif pick == nil {\n\t\t\t\tpick = fp\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Prefer the cover that reaches furthest; break ties\n\t\t\t// on fewer shards, then lower fee, then first-shard\n\t\t\t// score.\n\t\t\tswitch {\n\t\t\tcase fp.covered > pick.covered+pick.covered/64:\n\t\t\t\tpick = fp\n\t\t\tcase pick.covered > fp.covered+fp.covered/64:\n\t\t\tcase len(fp.shards) < len(pick.shards):\n\t\t\t\tpick = fp\n\t\t\tcase len(fp.shards) > len(pick.shards):\n\t\t\tcase fp.fee < pick.fee:\n\t\t\t\tpick = fp\n\t\t\tcase fp.fee == pick.fee &&\n\t\t\t\tfp.shards[0].score > pick.shards[0].score:\n\t\t\t\tpick = fp\n\t\t\t}\n\t\t}\n\n\t\tif pick != nil {\n\t\t\tfirst := pick.shards[0]\n\t\t\ttake := false\n\t\t\tswitch {\n\t\t\tcase best == nil:\n\t\t\t\ttake = true\n\t\t\tcase best.amt >= amt:\n\t\t\t\t// Single shard already covers everything: only\n\t\t\t\t// switch if it is genuinely unlikely and the\n\t\t\t\t// cover is confident.\n\t\t\t\ttake = best.prob < 0.40 &&\n\t\t\t\t\tpick.covered >= amt &&\n\t\t\t\t\tfirst.prob > best.prob*1.3\n\t\t\tdefault:\n\t\t\t\t// Prefer the plan that covers more of the\n\t\t\t\t// remaining amount; a slightly worse first\n\t\t\t\t// shard is fine if the plan as a whole covers\n\t\t\t\t// materially more.\n\t\t\t\ttake = pick.covered >\n\t\t\t\t\tbest.amt+best.amt/16 ||\n\t\t\t\t\t(pick.covered >= best.amt &&\n\t\t\t\t\t\tfirst.score >\n\t\t\t\t\t\t\tbest.score*0.9)\n\t\t\t}\n\n\t\t\tif take {\n\t\t\t\tr.queued = pick.shards[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded but\n// whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// dropAffected prunes only the queued shards that traverse a channel whose\n// belief just changed, keeping the rest of the joint plan alive. Throwing away\n// the whole plan on every failure was costing extra attempts.\nfunc (r *router) dropAffected(chanID uint64) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkeep := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\thit := false\n\t\tfor _, e := range pl.path {\n\t\t\tif e.chanID == chanID {\n\t\t\t\thit = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !hit {\n\t\t\tkeep = append(keep, pl)\n\t\t}\n\t}\n\tr.queued = keep\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// A settle drains the corridor it used, so any queued shard\n\t\t// sharing those channels must be re-priced.\n\t\tfor _, h := range rt.Hops {\n\t\t\tr.dropAffected(h.ChannelID)\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote hops\n\t// so we stop re-picking this corridor without destroying hard bounds.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\tr.dropAffected(h.ChannelID)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\t// A policy repair does not mean the corridor is bad, so do not\n\t\t// let it count against the progress budget.\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\tr.dropAffected(h.ChannelID)\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\tr.dropAffected(h.ChannelID)\n\t\treturn nil\n\n\tcase \"min\":\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\tr.dropAffected(h.ChannelID)\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.dropAffected(h.ChannelID)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\tb.succ = false\n\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\t// Only the shards that use this channel are stale.\n\tr.dropAffected(h.ChannelID)\n\n\treturn nil\n}"
}
},
{
"id": 4,
"parent": 0,
"score": 0.6843,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-directed-channel beliefs: hard lower-OK / upper-fail bounds\n// (no time decay: a stale bound costs one retry to refresh).\n// - Probability-aware Dijkstra: minimize fee + penalty/probability.\n// - JOINT route-set planning: when no single path can carry the\n// remaining amount, plan a disjoint set of corridors up front and\n// size each shard to the max-flow that corridor can actually bear.\n// - Lower-retry on failed channels rather than blacklisting, with a\n// bisection ladder between the proven-ok and proven-fail bounds.\n//\n// Changes in this revision (attempt-count reduction was the dominant\n// score gap: success was already 1.0 while retry_efficiency was -25):\n// - Corridor sizing is now analytic: the headroom of the cheapest\n// corridor under current beliefs gives the shard size directly,\n// instead of a 14-step blind bisection per corridor.\n// - Shards are committed as a plan of ROUTES, not just amounts, so a\n// vetted corridor is reused rather than re-searched and re-shifted\n// onto a channel we already know bounces.\n// - A per-payment singleCap remembers the largest delivered size that\n// failed for liquidity reasons, so the ladder stops re-proposing\n// amounts known to bounce.\n// - Repeat failures on one direction back off geometrically: a bimodal\n// channel that fails twice is depleted, not merely tight.\n// - A MaxParts-scaled attempt budget cuts the long tails that dominated\n// the penalty, with a floor that keeps success rate intact.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\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 = 40\n\n\t// attemptCost is the virtual cost (in msat) of one failed attempt,\n\t// used to trade fee against success probability. Attempts are\n\t// penalized by the scorer, so this is deliberately generous\n\t// relative to fees.\n\tattemptCostBase = lnwire.MilliSatoshi(3_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(12_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// minShard is the smallest shard we will bother planning.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// maxAttempts bounds the total attempts we will ever issue.\n\tmaxAttempts = 220\n)\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; zero means unknown.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// fails counts how many times this direction has bounced. Repeated\n\t// bounces mean the channel is depleted, not merely tight, so we\n\t// stop nibbling at it.\n\tfails int\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\tp := 0.30*low + 0.70*cliff\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// plannedShard is one committed leg of a joint plan.\ntype plannedShard struct {\n\tamt lnwire.MilliSatoshi\n\trt *route.Route\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[uint64]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// attempts counts total attempts issued.\n\tattempts int\n\n\t// noRouteStreak counts consecutive planning failures.\n\tnoRouteStreak int\n\n\t// plan holds committed shards not yet handed out.\n\tplan []plannedShard\n\n\t// planFor is the remaining amount the plan was built for; if the\n\t// remaining amount moves away from it the plan is discarded.\n\tplanFor lnwire.MilliSatoshi\n\n\t// singleCap is the largest single-shard amount we still believe can\n\t// be routed whole. A whole-amount failure lowers it, so the ladder\n\t// stops re-probing sizes we already know bounce.\n\tsingleCap lnwire.MilliSatoshi\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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[uint64]*belief),\n\t\tpending: make(map[uint64]*route.Route),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(r.localEdges, e)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(chanID uint64) *belief {\n\tb, ok := r.beliefs[chanID]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[chanID] = b\n\t}\n\treturn b\n}\n\n// avail is the amount we still consider spendable over this edge, taking\n// our own in-flight HTLCs and local balance into account. It returns zero\n// when the edge is unusable at any amount.\nfunc (r *router) avail(e *edge) lnwire.MilliSatoshi {\n\tcap := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < cap {\n\t\tcap = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b != nil {\n\t\tif b.hasFail && b.failAmt > 0 {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright. Bisect toward the\n\t\t\t// proven-good bound so repeated failures converge, and\n\t\t\t// back off harder the more times it has bounced (a\n\t\t\t// bimodal channel that fails twice is depleted).\n\t\t\tlim := b.failAmt - 1\n\t\t\tif b.okAmt > 0 && b.okAmt < lim {\n\t\t\t\tlim = b.okAmt + (lim-b.okAmt)*2/3\n\t\t\t} else {\n\t\t\t\tswitch {\n\t\t\t\tcase b.fails >= 3:\n\t\t\t\t\tlim = b.failAmt / 8\n\t\t\t\tcase b.fails == 2:\n\t\t\t\t\tlim = b.failAmt / 3\n\t\t\t\tdefault:\n\t\t\t\t\tlim = b.failAmt * 3 / 5\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lim < cap {\n\t\t\t\tcap = lim\n\t\t\t}\n\t\t}\n\t\tif b.inFlight >= cap {\n\t\t\treturn 0\n\t\t}\n\t\tcap -= b.inFlight\n\t}\n\n\tif e.from == r.source {\n\t\tbal := r.localBalances[e.chanID]\n\t\tif bal < cap {\n\t\t\tcap = bal\n\t\t}\n\t}\n\n\treturn cap\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tb := r.beliefs[e.chanID]\n\t\tused := lnwire.MilliSatoshi(0)\n\t\tif b != nil {\n\t\t\tused = b.inFlight\n\t\t}\n\t\tif r.localBalances[e.chanID] >= amt+used {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b == nil {\n\t\treturn bimodalPrior(amt, e.capacity)\n\t}\n\n\teff := amt + b.inFlight\n\n\tif b.okAmt >= eff {\n\t\treturn knownProb\n\t}\n\tif b.hasFail && eff >= b.failAmt {\n\t\treturn minProb / 2\n\t}\n\n\tp := bimodalPrior(eff, e.capacity)\n\n\t// Narrow with the bounds we do have: known-good below and known-bad\n\t// above compress the uncertain interval.\n\tif b.okAmt > 0 && b.hasFail {\n\t\tlo := float64(b.okAmt)\n\t\thi := float64(b.failAmt)\n\t\tif hi > lo {\n\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\tbracket := 1 - frac\n\t\t\tp = 0.5*p + 0.5*bracket\n\t\t}\n\t} else if b.okAmt > 0 {\n\t\t// A prior success raises confidence for nearby amounts.\n\t\tratio := float64(eff) / float64(b.okAmt)\n\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\tp = 0.45*p + 0.55*boost\n\t} else if b.hasFail {\n\t\t// A prior failure at failAmt: everything meaningfully below it\n\t\t// is still plausible, but discount. Repeat failures on the same\n\t\t// direction mean depletion, so discount much harder.\n\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\tdamp := 0.85\n\t\tif b.fails >= 2 {\n\t\t\tdamp = 0.97\n\t\t}\n\t\tp = p * (1 - damp*frac)\n\t}\n\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\t// dist is the combined cost used for ordering.\n\tdist float64\n\t// amt is the amount that must arrive at node.\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tcltv uint32\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\tdist float64\n\tvia *edge\n\thops int\n\tcltv uint32\n}\n\n// findRoute runs a probability-weighted backward Dijkstra. The cost of a\n// path is its fee plus an attempt-cost term divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused (already carrying a shard\n// of this payment along a chosen route set).\nfunc (r *router) findRoute(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase +\n\t\tamt*attemptCostPPM/1_000_000)\n\n\tbest := make(map[route.Vertex]*pathState)\n\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{\n\t\tamt: amt,\n\t\tlogProb: 0,\n\t\tdist: 0,\n\t\thops: 0,\n\t\tcltv: finalCltvDelta,\n\t}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{\n\t\tnode: target, dist: 0, amt: amt, logProb: 0,\n\t\thops: 0, cltv: finalCltvDelta,\n\t})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.avail(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif e.from == r.source {\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-3 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\n\t\t\t// Risk-adjusted cost: expected fee plus expected retry\n\t\t\t// cost. A small per-hop term discourages long paths.\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*400\n\n\t\t\tprev, ok := best[e.from]\n\t\t\tif ok && dist >= prev.dist-1e-9 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\tdist: dist,\n\t\t\t\tvia: e,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, best)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, math.Exp(src.logProb), nil\n}\n\n// buildRoute materializes the route from the next-pointer map.\nfunc (r *router) buildRoute(amt lnwire.MilliSatoshi,\n\tbest map[route.Vertex]*pathState) (*route.Route, error) {\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// --- Joint route-set planning ---------------------------------------------\n\n// edgeOf finds the directed edge with the given channel id pointing at to.\nfunc (r *router) edgeOf(chanID uint64, to route.Vertex) *edge {\n\tfor _, e := range r.inEdges[to] {\n\t\tif e.chanID == chanID {\n\t\t\treturn e\n\t\t}\n\t}\n\treturn nil\n}\n\n// routeHeadroom is the largest delivered amount we believe the given route\n// could carry, derived from each hop's believed availability. Availability\n// is expressed at the hop's own (fee-inflated) amount, so we scale each\n// hop's slack back into target-amount terms before taking the minimum.\nfunc (r *router) routeHeadroom(rt *route.Route) lnwire.MilliSatoshi {\n\tif len(rt.Hops) == 0 {\n\t\treturn 0\n\t}\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\tif delivered == 0 {\n\t\treturn 0\n\t}\n\n\thead := lnwire.MilliSatoshi(0)\n\tfirst := true\n\n\tfor i, h := range rt.Hops {\n\t\te := r.edgeOf(h.ChannelID, h.PubKeyBytes)\n\t\tif e == nil {\n\t\t\tcontinue\n\t\t}\n\t\tflow := hopAmount(rt, i)\n\t\tif flow == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ta := r.avail(e)\n\t\tratio := float64(delivered) / float64(flow)\n\t\tscaled := lnwire.MilliSatoshi(float64(a) * ratio)\n\n\t\tif first || scaled < head {\n\t\t\thead = scaled\n\t\t\tfirst = false\n\t\t}\n\t}\n\n\tif first {\n\t\treturn 0\n\t}\n\treturn head\n}\n\n// probeAmount searches downward for a routable amount, starting from the\n// analytic headroom of the cheapest corridor rather than blind bisection.\n// It returns the amount and its route, or zero.\nfunc (r *router) probeAmount(hi lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (lnwire.MilliSatoshi, *route.Route) {\n\n\tif hi < minShard {\n\t\treturn 0, nil\n\t}\n\n\tif rt, _, err := r.findRoute(hi, avoid); err == nil {\n\t\treturn hi, rt\n\t}\n\n\t// A small probe reveals which corridor is cheapest; that corridor's\n\t// headroom tells us how big a shard it can bear. This replaces a long\n\t// bisection with one analytic step plus a short refinement.\n\tseed := hi / 8\n\tif seed < minShard {\n\t\tseed = minShard\n\t}\n\tseedRt, _, seedErr := r.findRoute(seed, avoid)\n\tif seedErr == nil {\n\t\tif head := r.routeHeadroom(seedRt); head >= minShard {\n\t\t\tcand := head\n\t\t\tif cand > hi {\n\t\t\t\tcand = hi\n\t\t\t}\n\t\t\tif rt, _, err := r.findRoute(cand, avoid); err == nil {\n\t\t\t\treturn cand, rt\n\t\t\t}\n\t\t\t// Analytic estimate too eager: step down a few times.\n\t\t\tfor i := 0; i < 3; i++ {\n\t\t\t\tcand = cand * 2 / 3\n\t\t\t\tif cand < minShard {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif rt, _, err := r.findRoute(\n\t\t\t\t\tcand, avoid,\n\t\t\t\t); err == nil {\n\t\t\t\t\treturn cand, rt\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fall back to a short bisection between the seed and hi.\n\tlo := lnwire.MilliSatoshi(0)\n\tif seedErr == nil {\n\t\tlo = seed\n\t}\n\tfor i := 0; i < 7 && hi-lo > hi/32+1; i++ {\n\t\tmid := lo + (hi-lo)/2\n\t\tif mid < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif _, _, err := r.findRoute(mid, avoid); err == nil {\n\t\t\tlo = mid\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo >= minShard {\n\t\tif rt, _, err := r.findRoute(lo, avoid); err == nil {\n\t\t\treturn lo, rt\n\t\t}\n\t}\n\tif seedErr == nil && seed >= minShard {\n\t\treturn seed, seedRt\n\t}\n\treturn 0, nil\n}\n\n// planShards builds a joint route set for the remaining amount: it greedily\n// carves the largest shard each disjoint corridor can bear until the amount\n// is covered or we run out of parts/corridors. The shards carry their routes\n// so a committed corridor is reused rather than re-searched.\nfunc (r *router) planShards(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) []plannedShard {\n\n\tif partsLeft == 0 {\n\t\treturn nil\n\t}\n\tif partsLeft > 12 {\n\t\tpartsLeft = 12\n\t}\n\n\t// Corridors chosen so far must not be reused by later shards, so\n\t// unequal parallel corridors each carry what they can bear.\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor k := range busy {\n\t\tavoid[k] = true\n\t}\n\n\tvar out []plannedShard\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft && left >= minShard; k++ {\n\t\tamt, rt := r.probeAmount(left, avoid)\n\t\tif amt == 0 || rt == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, plannedShard{amt: amt, rt: rt})\n\t\tleft -= amt\n\n\t\t// Reserve this corridor's hops so the next shard looks\n\t\t// elsewhere and the set stays genuinely disjoint.\n\t\tfor _, h := range rt.Hops {\n\t\t\tavoid[h.ChannelID] = true\n\t\t}\n\t}\n\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs, so\n// a new shard is planned over a genuinely disjoint corridor where possible.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// firstHopBudget returns the total liquidity we believe is reachable out of\n// our own channels, which upper-bounds any single payment.\nfunc (r *router) firstHopBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.avail(e)\n\t}\n\treturn total\n}\n\n// candidateAmounts builds a short descending ladder of shard sizes, used as\n// a fallback when joint planning comes up empty. It is deliberately short:\n// each entry costs a Dijkstra run and, if handed out, risks a failed attempt.\nfunc (r *router) candidateAmounts(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > remaining || set[a] {\n\t\t\treturn\n\t\t}\n\t\tif r.singleCap > 0 && a > r.singleCap {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(remaining)\n\n\t// Even splits over the part counts we could still afford.\n\tfor k := uint32(2); k <= partsLeft && k <= 8; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric ladder for finer probing when even splits all fail.\n\tcur := remaining\n\tfor i := 0; i < 8; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < minShard {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\n\tif len(out) > 10 {\n\t\tout = out[:10]\n\t}\n\treturn out\n}\n\n// attemptBudget bounds the attempts spent on this payment. Attempts are\n// penalized by the scorer, so a long tail of nibbling retries costs more\n// than a well-chosen smaller shard, but the floor keeps success intact.\nfunc (r *router) attemptBudget() int {\n\tbudget := 24\n\tif r.spec.MaxParts > 1 {\n\t\tbudget = int(r.spec.MaxParts) * 6\n\t}\n\tif budget < 24 {\n\t\tbudget = 24\n\t}\n\tif budget > maxAttempts {\n\t\tbudget = maxAttempts\n\t}\n\treturn budget\n}\n\n// RequestRoute plans the next shard. It first tries to satisfy the whole\n// remaining amount over one route; failing that it plans a joint route set\n// whose shard sizes come from each corridor's actual carrying capacity.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tr.attempts++\n\tif r.attempts > r.attemptBudget() {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Drop a stale plan if the remaining amount no longer matches what it\n\t// was built for.\n\tif r.planFor != amt {\n\t\tr.plan = nil\n\t\tr.planFor = 0\n\t}\n\n\t// Serve a committed plan entry first. The stored route is reused when\n\t// it still looks viable under current beliefs, which saves both a\n\t// search and a wasted attempt on a corridor we already vetted.\n\tfor len(r.plan) > 0 {\n\t\tps := r.plan[0]\n\t\tr.plan = r.plan[1:]\n\n\t\ta := ps.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\n\t\tif a == ps.amt && ps.rt != nil && r.routeViable(ps.rt) {\n\t\t\tr.noRouteStreak = 0\n\t\t\tr.planFor = amt - a\n\t\t\treturn ps.rt, nil\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, busy); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\tr.planFor = amt - a\n\t\t\treturn rt, nil\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\tr.planFor = amt - a\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\tr.plan = nil\n\tr.planFor = 0\n\n\t// A shard can never exceed what our own channels can push right now.\n\tbudget := r.firstHopBudget()\n\ttarget := amt\n\tif budget > 0 && budget < target {\n\t\ttarget = budget\n\t}\n\tif r.singleCap > 0 && r.singleCap < target {\n\t\ttarget = r.singleCap\n\t}\n\tif target < minShard {\n\t\ttarget = amt\n\t}\n\n\t// Fast path: the whole remaining amount over one good route.\n\tif target == amt {\n\t\tif rt, p, err := r.findRoute(target, busy); err == nil &&\n\t\t\tp > 0.45 {\n\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\n\t// Joint planning: size shards to corridors instead of halving.\n\tif partsLeft > 1 {\n\t\tplan := r.planShards(target, partsLeft, busy)\n\t\tif len(plan) > 0 {\n\t\t\t// Largest shard first: it is the one most likely to be\n\t\t\t// invalidated by drift, so probe it while our belief\n\t\t\t// is freshest.\n\t\t\tsort.Slice(plan, func(i, j int) bool {\n\t\t\t\treturn plan[i].amt > plan[j].amt\n\t\t\t})\n\t\t\tfirst := plan[0]\n\t\t\tr.plan = plan[1:]\n\t\t\tr.planFor = amt - first.amt\n\n\t\t\tif first.rt != nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn first.rt, nil\n\t\t\t}\n\t\t\tif rt, _, err := r.findRoute(\n\t\t\t\tfirst.amt, busy,\n\t\t\t); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tr.plan = nil\n\t\t\tr.planFor = 0\n\t\t}\n\t}\n\n\t// Fallback: descend a short ladder of amounts, scoring each by\n\t// expected delivered value net of fee and of the retry it risks.\n\ttype planItem struct {\n\t\trt *route.Route\n\t\tscore float64\n\t}\n\tvar bestPlan *planItem\n\n\tladder := r.candidateAmounts(target, partsLeft)\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tavoid := busy\n\t\tif pass == 1 {\n\t\t\tavoid = nil\n\t\t}\n\t\tif pass == 0 && len(busy) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttried := 0\n\t\tfor _, a := range ladder {\n\t\t\trt, p, err := r.findRoute(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried++\n\n\t\t\tfee := float64(rt.TotalAmount) - float64(a)\n\n\t\t\t// Expected progress minus fee, minus the expected cost\n\t\t\t// of the retries a low-probability shard invites.\n\t\t\tretryCost := (1 - p) * float64(attemptCostBase+\n\t\t\t\ta*attemptCostPPM/1_000_000)\n\t\t\tscore := p*float64(a) - fee*0.5 - retryCost\n\t\t\tif a == amt {\n\t\t\t\tscore *= 1.15\n\t\t\t}\n\n\t\t\tif bestPlan == nil || score > bestPlan.score {\n\t\t\t\tbestPlan = &planItem{rt: rt, score: score}\n\t\t\t}\n\n\t\t\tif a == amt && p > 0.6 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif tried >= 5 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif bestPlan != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestPlan == nil {\n\t\t// Last resort: probe for whatever amount is routable at all,\n\t\t// so a depleted corridor still yields progress rather than a\n\t\t// terminal give-up.\n\t\tif a, rt := r.probeAmount(target, nil); a >= minShard &&\n\t\t\trt != nil {\n\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tr.noRouteStreak++\n\t\tif r.noRouteStreak > 3 {\n\t\t\treturn nil, errors.New(\"no route found\")\n\t\t}\n\t\treturn nil, errors.New(\"no route available\")\n\t}\n\n\tr.noRouteStreak = 0\n\treturn bestPlan.rt, nil\n}\n\n// routeViable reports whether every hop of a previously planned route still\n// looks able to carry its amount under current beliefs.\nfunc (r *router) routeViable(rt *route.Route) bool {\n\tfor i, h := range rt.Hops {\n\t\te := r.edgeOf(h.ChannelID, h.PubKeyBytes)\n\t\tif e == nil {\n\t\t\treturn false\n\t\t}\n\t\ta := hopAmount(rt, i)\n\t\tif !e.policyOK(a) || a > r.avail(e) {\n\t\t\treturn false\n\t\t}\n\t\tif r.prob(e, a) < 0.05 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(h.ChannelID)\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// recordOK raises the proven-good bound on a channel and clears any stale\n// failure bound at or below that amount.\nfunc (r *router) recordOK(chanID uint64, a lnwire.MilliSatoshi) {\n\tb := r.bel(chanID)\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tif len(rt.Hops) == 0 {\n\t\treturn nil\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.recordOK(h.ChannelID, a)\n\n\t\t\tif i == 0 && h.ChannelID != 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settled shard proves this size is deliverable, so lift any\n\t\t// self-imposed single-shard cap back to at least this amount.\n\t\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\tif r.singleCap > 0 && r.singleCap < delivered {\n\t\t\tr.singleCap = delivered\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tif failIdx > 0 {\n\t\tfor i := 0; i < failIdx && i < len(rt.Hops); i++ {\n\t\t\tr.recordOK(rt.Hops[i].ChannelID, hopAmount(rt, i))\n\t\t}\n\t}\n\n\t// Remember that this delivered size did not make it, so the ladder\n\t// stops re-proposing amounts we know bounce. Only liquidity misses\n\t// justify this: policy failures are about the route, not the size.\n\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\tif isLiquidityFailure(result) && delivered > minShard {\n\t\tlim := delivered - 1\n\t\tif r.singleCap == 0 || lim < r.singleCap {\n\t\t\tr.singleCap = lim\n\t\t}\n\t}\n\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\t// Unattributable failure (e.g. an unknown source): the whole\n\t\t// route is suspect, so nudge every hop's bound down slightly\n\t\t// rather than learning nothing at all.\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tb := r.bel(h.ChannelID)\n\t\t\tif b.okAmt >= a {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !b.hasFail || a < b.failAmt {\n\t\t\t\t// Weak evidence: only bound the largest hop,\n\t\t\t\t// which is the likely bottleneck.\n\t\t\t\tif i == 0 || a >= hopAmount(rt, 0)/2 {\n\t\t\t\t\tb.hasFail = true\n\t\t\t\t\tb.failAmt = a\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(h.ChannelID)\n\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// isLiquidityFailure reports whether the failure looks like a liquidity\n// miss rather than a policy violation. Policy failures (fee, cltv) say\n// nothing about how much liquidity the corridor holds.\nfunc isLiquidityFailure(result routing.SimHtlcResult) bool {\n\tswitch result.Failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\treturn true\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry,\n\t\t*lnwire.FailChannelDisabled:\n\t\treturn false\n\tdefault:\n\t\treturn true\n\t}\n}"
}
},
{
"id": 5,
"parent": 1,
"score": 0.6124,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-directed-channel beliefs: hard lower-OK / upper-fail bounds\n// (no time decay: a stale bound costs one retry to refresh).\n// - Probability-aware Dijkstra: minimize fee + penalty/probability.\n// - Joint route-set planning for MPP: pick shard sizes from what each\n// corridor can actually bear instead of blind halving.\n// - Lower-retry on failed channels rather than blacklisting.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\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 = 40\n\n\t// attemptCost is the virtual cost (in msat) of one failed attempt,\n\t// used to trade fee against success probability.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n)\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; zero means unknown.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\tp := 0.30*low + 0.70*cliff\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[uint64]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// failStreak counts consecutive failures with no progress.\n\tfailStreak int\n\n\t// attempts counts total attempts issued.\n\tattempts int\n\n\t// lastShard remembers the amount of the last route handed out, so we\n\t// can shrink deliberately when it fails.\n\tlastShard lnwire.MilliSatoshi\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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[uint64]*belief),\n\t\tpending: make(map[uint64]*route.Route),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(chanID uint64) *belief {\n\tb, ok := r.beliefs[chanID]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[chanID] = b\n\t}\n\treturn b\n}\n\n// avail is the amount we still consider spendable over this edge, taking\n// our own in-flight HTLCs and local balance into account. It returns zero\n// when the edge is unusable at any amount.\nfunc (r *router) avail(e *edge) lnwire.MilliSatoshi {\n\tcap := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < cap {\n\t\tcap = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b != nil {\n\t\tif b.hasFail && b.failAmt > 0 {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tlim := b.failAmt * 3 / 4\n\t\t\tif lim < cap {\n\t\t\t\tcap = lim\n\t\t\t}\n\t\t}\n\t\tif b.inFlight >= cap {\n\t\t\treturn 0\n\t\t}\n\t\tcap -= b.inFlight\n\t}\n\n\tif e.from == r.source {\n\t\tbal := r.localBalances[e.chanID]\n\t\tif bal < cap {\n\t\t\tcap = bal\n\t\t}\n\t}\n\n\treturn cap\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tb := r.beliefs[e.chanID]\n\t\tused := lnwire.MilliSatoshi(0)\n\t\tif b != nil {\n\t\t\tused = b.inFlight\n\t\t}\n\t\tif r.localBalances[e.chanID] >= amt+used {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b == nil {\n\t\treturn bimodalPrior(amt, e.capacity)\n\t}\n\n\teff := amt + b.inFlight\n\n\tif b.okAmt >= eff {\n\t\treturn knownProb\n\t}\n\tif b.hasFail && eff >= b.failAmt {\n\t\treturn minProb / 2\n\t}\n\n\tp := bimodalPrior(eff, e.capacity)\n\n\t// Narrow with the bounds we do have: known-good below and known-bad\n\t// above compress the uncertain interval.\n\tif b.okAmt > 0 && b.hasFail {\n\t\tlo := float64(b.okAmt)\n\t\thi := float64(b.failAmt)\n\t\tif hi > lo {\n\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\tbracket := 1 - frac\n\t\t\tp = 0.5*p + 0.5*bracket\n\t\t}\n\t} else if b.okAmt > 0 {\n\t\t// A prior success raises confidence for nearby amounts.\n\t\tratio := float64(eff) / float64(b.okAmt)\n\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\tp = 0.45*p + 0.55*boost\n\t} else if b.hasFail {\n\t\t// A prior failure at failAmt: everything meaningfully below it\n\t\t// is still plausible, but discount.\n\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\tp = p * (1 - 0.85*frac)\n\t}\n\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\t// dist is the combined cost used for ordering.\n\tdist float64\n\t// amt is the amount that must arrive at node.\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tcltv uint32\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\tdist float64\n\tvia *edge\n\thops int\n\tcltv uint32\n}\n\n// findRoute runs a probability-weighted backward Dijkstra. The cost of a\n// path is its fee plus an attempt-cost term divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused (already carrying a shard\n// of this payment along a chosen route set).\nfunc (r *router) findRoute(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase +\n\t\tamt*attemptCostPPM/1_000_000)\n\n\tbest := make(map[route.Vertex]*pathState)\n\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{\n\t\tamt: amt,\n\t\tlogProb: 0,\n\t\tdist: 0,\n\t\thops: 0,\n\t\tcltv: finalCltvDelta,\n\t}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{\n\t\tnode: target, dist: 0, amt: amt, logProb: 0,\n\t\thops: 0, cltv: finalCltvDelta,\n\t})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.avail(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif e.from == r.source {\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-4 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\n\t\t\t// Risk-adjusted cost: expected fee plus expected retry\n\t\t\t// cost. A small per-hop term discourages long paths.\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*250\n\n\t\t\tprev, ok := best[e.from]\n\t\t\tif ok && dist >= prev.dist-1e-9 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\tdist: dist,\n\t\t\t\tvia: e,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, best)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, math.Exp(src.logProb), nil\n}\n\n// buildRoute materializes the route from the next-pointer map.\nfunc (r *router) buildRoute(amt lnwire.MilliSatoshi,\n\tbest map[route.Vertex]*pathState) (*route.Route, error) {\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// firstHopBudget returns the total liquidity we believe is reachable out of\n// our own channels, which upper-bounds any single payment.\nfunc (r *router) firstHopBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, edges := range r.inEdges {\n\t\tfor _, e := range edges {\n\t\t\tif e.from != r.source || seen[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[e.chanID] = true\n\t\t\ttotal += r.avail(e)\n\t\t}\n\t}\n\treturn total\n}\n\n// candidateAmounts builds a descending ladder of shard sizes to try for a\n// remaining amount, given how many parts we may still use. Rather than pure\n// halving, the ladder mixes even splits across the parts we have left with\n// geometric shrinkage, so unequal corridors each get an amount they can\n// plausibly bear.\nfunc (r *router) candidateAmounts(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a == 0 || a > remaining || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(remaining)\n\n\t// Even splits over all part counts we could still afford.\n\tfor k := uint32(2); k <= partsLeft && k <= 16; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric ladder for finer probing when even splits all fail.\n\tcur := remaining\n\tfor i := 0; i < 12; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < 1_000 {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: just under each proven failure point on a\n\t// channel out of our own node, and each proven-good amount.\n\tfor _, b := range r.beliefs {\n\t\tif b.hasFail && b.failAmt > 1_000 {\n\t\t\tadd(b.failAmt * 3 / 4)\n\t\t}\n\t\tif b.okAmt > 0 {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\n\t// Cap the ladder so a single RequestRoute call stays bounded.\n\tif len(out) > 24 {\n\t\tout = out[:24]\n\t}\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs, so\n// a new shard is planned over a genuinely disjoint corridor where possible.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// RequestRoute plans the next shard. It searches jointly over shard amount\n// and route: for each candidate amount (largest first) it finds the best\n// risk-adjusted route, and takes the amount whose route maximizes expected\n// delivered value per unit of risk-adjusted cost.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tr.attempts++\n\tif r.attempts > 400 {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\tbudget := r.firstHopBudget()\n\ttarget := amt\n\tif budget > 0 && budget < target {\n\t\ttarget = budget\n\t}\n\tif target == 0 {\n\t\ttarget = amt\n\t}\n\n\tbusy := r.inFlightChans()\n\n\ttype plan struct {\n\t\trt *route.Route\n\t\tscore float64\n\t\tamt lnwire.MilliSatoshi\n\t}\n\tvar bestPlan *plan\n\n\tladder := r.candidateAmounts(target, partsLeft)\n\n\t// First pass: prefer corridors disjoint from in-flight shards so\n\t// parallel parts do not contend for the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tavoid := busy\n\t\tif pass == 1 {\n\t\t\tavoid = nil\n\t\t}\n\t\tif pass == 0 && len(busy) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttried := 0\n\t\tfor _, a := range ladder {\n\t\t\tif a == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trt, p, err := r.findRoute(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried++\n\n\t\t\tfee := float64(rt.TotalAmount) - float64(a)\n\n\t\t\t// Expected value: probability-weighted delivered\n\t\t\t// amount, discounted by fee. Bigger shards are\n\t\t\t// preferred when their probability holds up.\n\t\t\tscore := p*float64(a) - fee*0.5\n\n\t\t\t// Slight bonus for shards that finish the payment in\n\t\t\t// one go, since fewer parts means fewer attempts.\n\t\t\tif a == amt {\n\t\t\t\tscore *= 1.15\n\t\t\t}\n\n\t\t\tif bestPlan == nil || score > bestPlan.score {\n\t\t\t\tbestPlan = &plan{rt: rt, score: score, amt: a}\n\t\t\t}\n\n\t\t\t// Stop early once we have a strong full-amount plan.\n\t\t\tif a == amt && p > 0.75 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif tried >= 8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif bestPlan != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestPlan == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.lastShard = bestPlan.amt\n\treturn bestPlan.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(h.ChannelID)\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tfor i, h := range rt.Hops {\n\t\t\tb := r.bel(h.ChannelID)\n\t\t\ta := hopAmount(rt, i)\n\t\t\tif a > b.okAmt {\n\t\t\t\tb.okAmt = a\n\t\t\t}\n\t\t\t// A success invalidates any older failure bound at or\n\t\t\t// below this amount.\n\t\t\tif b.hasFail && b.failAmt <= a {\n\t\t\t\tb.hasFail = false\n\t\t\t\tb.failAmt = 0\n\t\t\t}\n\t\t\tif h.ChannelID != 0 && i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tif failIdx > 0 {\n\t\tfor i := 0; i < failIdx && i < len(rt.Hops); i++ {\n\t\t\tb := r.bel(rt.Hops[i].ChannelID)\n\t\t\ta := hopAmount(rt, i)\n\t\t\tif a > b.okAmt {\n\t\t\t\tb.okAmt = a\n\t\t\t}\n\t\t\tif b.hasFail && b.failAmt <= a {\n\t\t\t\tb.hasFail = false\n\t\t\t\tb.failAmt = 0\n\t\t\t}\n\t\t}\n\t}\n\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\treturn nil\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(h.ChannelID)\n\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 6,
"parent": 0,
"score": 0.507,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - Probability-aware backward Dijkstra: minimize fee plus an attempt\n// cost divided by the path's success probability.\n// - Joint shard sizing: every corridor we discover is re-priced at the\n// amount that corridor can actually bear (its belief bottleneck), so\n// unequal parallel corridors each carry a shard sized to fit instead\n// of a blind half of the remainder.\n// - A full descending amount ladder that is never truncated from the\n// small end, so we keep probing smaller shards instead of terminally\n// giving up with \"no route found\".\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// is deliberately large relative to fees.\n\tattemptCostBase = lnwire.MilliSatoshi(2_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(15_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 60\n\tmaxFailStreak = 18\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under a bimodal distribution this\n\t// sits comfortably on the plausible side of the cliff.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// lowerRetryNum/lowerRetryDen is the fraction of a proven failure\n\t// amount we are still willing to retry.\n\tlowerRetryNum = 3\n\tlowerRetryDen = 4\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\tif lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.hasFail {\n\t\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\t\tif est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Everything meaningfully below a proven failure is\n\t\t\t// still plausible, but discounted.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = p * (1 - 0.85*frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.15\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: proven-good amounts and just under proven\n\t// failure points.\n\tfor _, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail && b.failAmt >= minShard {\n\t\t\tadd(b.failAmt * lowerRetryNum / lowerRetryDen)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{rt: rt, score: s, amt: c, prob: p}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard. It searches jointly over shard amount\n// and corridor: for every candidate amount (largest first) it finds the\n// best risk-adjusted path, then re-prices that path at the amount its\n// weakest hop can bear, and returns the highest scoring combination.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tbusy := r.inFlightChans()\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.provePassed(h, a)\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 7,
"parent": 1,
"score": 0.4918,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// Knowledge therefore stays correct across payments in a batch instead\n// of going stale the moment we ourselves spend.\n// - BIMODAL OPTIMISM after a proven forward: because funds sit almost\n// entirely on one side, a channel that has forwarded for us is treated\n// as holding the bulk of its capacity (minus what we drained), which\n// lets later, larger payments reuse a proven corridor without first\n// failing at the larger amount.\n// - Depth-aware lower retries: how far below a proven failure we are\n// willing to retry depends on how depleted the direction looks. A\n// failure at a tiny fraction of capacity means \"empty\"; a failure near\n// capacity means \"plenty is still there\".\n// - JOINT ROUTE-SET PLANNING: when no single corridor can carry the\n// remaining amount, a greedy min-cost-flow decomposition picks several\n// DISJOINT corridors up front and sizes each shard to that corridor's\n// believed bottleneck. The extra shards are queued and handed out on\n// subsequent RequestRoute calls, so unequal parallel corridors each\n// carry a shard that fits instead of a blind half of the remainder.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 60\n\tmaxFailStreak = 18\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under a bimodal distribution this\n\t// sits comfortably on the plausible side of the cliff.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold. It is\n\t// the centre of the optimistic logistic, not a hard bound.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.2\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies. Maximizing p*amt is\n// exactly maximizing delivered value per attempt.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 6 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findAnyPath(left, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.85)\n\n\t\t\tif better {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 8,
"parent": 1,
"score": 0.5158,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-directed-channel beliefs: hard lower-OK / upper-fail bounds\n// (no time decay: a stale bound costs one retry to refresh).\n// - Probability-aware Dijkstra: minimize fee + penalty/probability.\n// - JOINT route-set planning: when no single path can carry the\n// remaining amount, plan a disjoint set of corridors up front and\n// size each shard to the max-flow that corridor can actually bear.\n// - Lower-retry on failed channels rather than blacklisting, with a\n// bisection ladder between the proven-ok and proven-fail bounds.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\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 = 40\n\n\t// attemptCost is the virtual cost (in msat) of one failed attempt,\n\t// used to trade fee against success probability.\n\tattemptCostBase = lnwire.MilliSatoshi(1_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// minShard is the smallest shard we will bother planning.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// maxAttempts bounds the total attempts we will ever issue.\n\tmaxAttempts = 500\n)\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; zero means unknown.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\tp := 0.30*low + 0.70*cliff\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[uint64]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// attempts counts total attempts issued.\n\tattempts int\n\n\t// noRouteStreak counts consecutive planning failures.\n\tnoRouteStreak int\n\n\t// planQueue holds shard amounts from a joint plan that we have\n\t// committed to but not yet handed out.\n\tplanQueue []lnwire.MilliSatoshi\n\n\t// planFor is the remaining amount the planQueue was built for; if the\n\t// remaining amount moves away from it the plan is discarded.\n\tplanFor lnwire.MilliSatoshi\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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[uint64]*belief),\n\t\tpending: make(map[uint64]*route.Route),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(r.localEdges, e)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(chanID uint64) *belief {\n\tb, ok := r.beliefs[chanID]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[chanID] = b\n\t}\n\treturn b\n}\n\n// avail is the amount we still consider spendable over this edge, taking\n// our own in-flight HTLCs and local balance into account. It returns zero\n// when the edge is unusable at any amount.\nfunc (r *router) avail(e *edge) lnwire.MilliSatoshi {\n\tcap := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < cap {\n\t\tcap = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b != nil {\n\t\tif b.hasFail && b.failAmt > 0 {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright. Bisect toward the\n\t\t\t// proven-good bound so repeated failures converge.\n\t\t\tlim := b.failAmt - 1\n\t\t\tif b.okAmt > 0 && b.okAmt < lim {\n\t\t\t\tlim = b.okAmt + (lim-b.okAmt)*2/3\n\t\t\t} else {\n\t\t\t\tlim = b.failAmt * 3 / 4\n\t\t\t}\n\t\t\tif lim < cap {\n\t\t\t\tcap = lim\n\t\t\t}\n\t\t}\n\t\tif b.inFlight >= cap {\n\t\t\treturn 0\n\t\t}\n\t\tcap -= b.inFlight\n\t}\n\n\tif e.from == r.source {\n\t\tbal := r.localBalances[e.chanID]\n\t\tif bal < cap {\n\t\t\tcap = bal\n\t\t}\n\t}\n\n\treturn cap\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tb := r.beliefs[e.chanID]\n\t\tused := lnwire.MilliSatoshi(0)\n\t\tif b != nil {\n\t\t\tused = b.inFlight\n\t\t}\n\t\tif r.localBalances[e.chanID] >= amt+used {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b == nil {\n\t\treturn bimodalPrior(amt, e.capacity)\n\t}\n\n\teff := amt + b.inFlight\n\n\tif b.okAmt >= eff {\n\t\treturn knownProb\n\t}\n\tif b.hasFail && eff >= b.failAmt {\n\t\treturn minProb / 2\n\t}\n\n\tp := bimodalPrior(eff, e.capacity)\n\n\t// Narrow with the bounds we do have: known-good below and known-bad\n\t// above compress the uncertain interval.\n\tif b.okAmt > 0 && b.hasFail {\n\t\tlo := float64(b.okAmt)\n\t\thi := float64(b.failAmt)\n\t\tif hi > lo {\n\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\tbracket := 1 - frac\n\t\t\tp = 0.5*p + 0.5*bracket\n\t\t}\n\t} else if b.okAmt > 0 {\n\t\t// A prior success raises confidence for nearby amounts.\n\t\tratio := float64(eff) / float64(b.okAmt)\n\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\tp = 0.45*p + 0.55*boost\n\t} else if b.hasFail {\n\t\t// A prior failure at failAmt: everything meaningfully below it\n\t\t// is still plausible, but discount.\n\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\tp = p * (1 - 0.85*frac)\n\t}\n\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\t// dist is the combined cost used for ordering.\n\tdist float64\n\t// amt is the amount that must arrive at node.\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tcltv uint32\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\tdist float64\n\tvia *edge\n\thops int\n\tcltv uint32\n}\n\n// findRoute runs a probability-weighted backward Dijkstra. The cost of a\n// path is its fee plus an attempt-cost term divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused (already carrying a shard\n// of this payment along a chosen route set).\nfunc (r *router) findRoute(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase +\n\t\tamt*attemptCostPPM/1_000_000)\n\n\tbest := make(map[route.Vertex]*pathState)\n\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{\n\t\tamt: amt,\n\t\tlogProb: 0,\n\t\tdist: 0,\n\t\thops: 0,\n\t\tcltv: finalCltvDelta,\n\t}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{\n\t\tnode: target, dist: 0, amt: amt, logProb: 0,\n\t\thops: 0, cltv: finalCltvDelta,\n\t})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.avail(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif e.from == r.source {\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-4 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\n\t\t\t// Risk-adjusted cost: expected fee plus expected retry\n\t\t\t// cost. A small per-hop term discourages long paths.\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*250\n\n\t\t\tprev, ok := best[e.from]\n\t\t\tif ok && dist >= prev.dist-1e-9 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\tdist: dist,\n\t\t\t\tvia: e,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, best)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, math.Exp(src.logProb), nil\n}\n\n// buildRoute materializes the route from the next-pointer map.\nfunc (r *router) buildRoute(amt lnwire.MilliSatoshi,\n\tbest map[route.Vertex]*pathState) (*route.Route, error) {\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// --- Joint route-set planning ---------------------------------------------\n\n// maxSendable finds the largest amount, by binary search, for which a route\n// exists avoiding the given channels. It returns 0 when even the smallest\n// probe is unroutable.\nfunc (r *router) maxSendable(hi lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) lnwire.MilliSatoshi {\n\n\tif hi < minShard {\n\t\treturn 0\n\t}\n\tif _, _, err := r.findRoute(hi, avoid); err == nil {\n\t\treturn hi\n\t}\n\n\tlo := lnwire.MilliSatoshi(0)\n\tfor i := 0; i < 14 && hi-lo > hi/64+1; i++ {\n\t\tmid := lo + (hi-lo)/2\n\t\tif mid < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif _, _, err := r.findRoute(mid, avoid); err == nil {\n\t\t\tlo = mid\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo < minShard {\n\t\treturn 0\n\t}\n\treturn lo\n}\n\n// planShards builds a joint route set for the remaining amount: it greedily\n// carves the largest shard each disjoint corridor can bear until the amount\n// is covered or we run out of parts/corridors. The returned amounts sum to\n// at most remaining; an empty result means we could not cover it and the\n// caller should fall back to a single best-effort shard.\nfunc (r *router) planShards(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\treturn nil\n\t}\n\n\t// Corridors chosen so far must not be reused by later shards, so\n\t// unequal parallel corridors each carry what they can bear.\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor k := range busy {\n\t\tavoid[k] = true\n\t}\n\n\tvar out []lnwire.MilliSatoshi\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft && left >= minShard; k++ {\n\t\tamt := r.maxSendable(left, avoid)\n\t\tif amt == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\trt, _, err := r.findRoute(amt, avoid)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, amt)\n\t\tleft -= amt\n\n\t\t// Reserve this corridor's first hop (and its bottleneck hops)\n\t\t// so the next shard looks elsewhere.\n\t\tfor _, h := range rt.Hops {\n\t\t\tavoid[h.ChannelID] = true\n\t\t}\n\t}\n\n\tif left >= minShard && len(out) > 0 {\n\t\t// Incomplete cover: still useful, the runner will call us\n\t\t// again for the rest once these shards land.\n\t\treturn out\n\t}\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs, so\n// a new shard is planned over a genuinely disjoint corridor where possible.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// firstHopBudget returns the total liquidity we believe is reachable out of\n// our own channels, which upper-bounds any single payment.\nfunc (r *router) firstHopBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.avail(e)\n\t}\n\treturn total\n}\n\n// candidateAmounts builds a descending ladder of shard sizes to try for a\n// remaining amount, used as a fallback when joint planning comes up empty.\nfunc (r *router) candidateAmounts(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > remaining || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(remaining)\n\n\t// Even splits over all part counts we could still afford.\n\tfor k := uint32(2); k <= partsLeft && k <= 16; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric ladder for finer probing when even splits all fail.\n\tcur := remaining\n\tfor i := 0; i < 12; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < minShard {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: just under each proven failure point and\n\t// each proven-good amount.\n\tfor _, b := range r.beliefs {\n\t\tif b.hasFail && b.failAmt > minShard {\n\t\t\tadd(b.failAmt * 3 / 4)\n\t\t}\n\t\tif b.okAmt > 0 {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\n\tif len(out) > 20 {\n\t\tout = out[:20]\n\t}\n\treturn out\n}\n\n// RequestRoute plans the next shard. It first tries to satisfy the whole\n// remaining amount over one route; failing that it plans a joint route set\n// whose shard sizes come from each corridor's actual carrying capacity.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tr.attempts++\n\tif r.attempts > maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Drop a stale plan if the remaining amount no longer matches what it\n\t// was built for.\n\tif r.planFor != amt {\n\t\tr.planQueue = nil\n\t\tr.planFor = 0\n\t}\n\n\t// Serve a committed plan entry first.\n\tfor len(r.planQueue) > 0 {\n\t\ta := r.planQueue[0]\n\t\tr.planQueue = r.planQueue[1:]\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, busy); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\tr.planQueue = nil\n\tr.planFor = 0\n\n\t// A shard can never exceed what our own channels can push right now.\n\tbudget := r.firstHopBudget()\n\ttarget := amt\n\tif budget > 0 && budget < target {\n\t\ttarget = budget\n\t}\n\tif target < minShard {\n\t\ttarget = amt\n\t}\n\n\t// Fast path: the whole remaining amount over one good route.\n\tif rt, p, err := r.findRoute(target, busy); err == nil && p > 0.55 &&\n\t\ttarget == amt {\n\n\t\tr.noRouteStreak = 0\n\t\treturn rt, nil\n\t}\n\n\t// Joint planning: size shards to corridors instead of halving.\n\tif partsLeft > 1 {\n\t\tplan := r.planShards(target, partsLeft, busy)\n\t\tif len(plan) > 0 {\n\t\t\t// Largest shard first: it is the one most likely to be\n\t\t\t// invalidated by drift, so probe it while our belief\n\t\t\t// is freshest.\n\t\t\tsort.Slice(plan, func(i, j int) bool {\n\t\t\t\treturn plan[i] > plan[j]\n\t\t\t})\n\t\t\tfirst := plan[0]\n\t\t\tr.planQueue = plan[1:]\n\t\t\tr.planFor = amt - first\n\n\t\t\tif rt, _, err := r.findRoute(first, busy); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tif rt, _, err := r.findRoute(first, nil); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tr.planQueue = nil\n\t\t\tr.planFor = 0\n\t\t}\n\t}\n\n\t// Fallback: descend a ladder of amounts, scoring each by expected\n\t// delivered value net of fee.\n\ttype plan struct {\n\t\trt *route.Route\n\t\tscore float64\n\t}\n\tvar bestPlan *plan\n\n\tladder := r.candidateAmounts(target, partsLeft)\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tavoid := busy\n\t\tif pass == 1 {\n\t\t\tavoid = nil\n\t\t}\n\t\tif pass == 0 && len(busy) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttried := 0\n\t\tfor _, a := range ladder {\n\t\t\trt, p, err := r.findRoute(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried++\n\n\t\t\tfee := float64(rt.TotalAmount) - float64(a)\n\t\t\tscore := p*float64(a) - fee*0.5\n\t\t\tif a == amt {\n\t\t\t\tscore *= 1.15\n\t\t\t}\n\n\t\t\tif bestPlan == nil || score > bestPlan.score {\n\t\t\t\tbestPlan = &plan{rt: rt, score: score}\n\t\t\t}\n\n\t\t\tif a == amt && p > 0.7 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif tried >= 8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif bestPlan != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestPlan == nil {\n\t\t// Last resort: probe for whatever amount is routable at all,\n\t\t// so a depleted corridor still yields progress rather than a\n\t\t// terminal give-up.\n\t\tif a := r.maxSendable(target, nil); a >= minShard {\n\t\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t}\n\n\t\tr.noRouteStreak++\n\t\tif r.noRouteStreak > 3 {\n\t\t\treturn nil, errors.New(\"no route found\")\n\t\t}\n\t\treturn nil, errors.New(\"no route available\")\n\t}\n\n\tr.noRouteStreak = 0\n\treturn bestPlan.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(h.ChannelID)\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// recordOK raises the proven-good bound on a channel and clears any stale\n// failure bound at or below that amount.\nfunc (r *router) recordOK(chanID uint64, a lnwire.MilliSatoshi) {\n\tb := r.bel(chanID)\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.recordOK(h.ChannelID, a)\n\n\t\t\tif i == 0 && h.ChannelID != 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tif failIdx > 0 {\n\t\tfor i := 0; i < failIdx && i < len(rt.Hops); i++ {\n\t\t\tr.recordOK(rt.Hops[i].ChannelID, hopAmount(rt, i))\n\t\t}\n\t}\n\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\t// Unattributable failure (e.g. an unknown source): the whole\n\t\t// route is suspect, so nudge every hop's bound down slightly\n\t\t// rather than learning nothing at all.\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tb := r.bel(h.ChannelID)\n\t\t\tif b.okAmt >= a {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !b.hasFail || a < b.failAmt {\n\t\t\t\t// Weak evidence: only bound the largest hop,\n\t\t\t\t// which is the likely bottleneck.\n\t\t\t\tif i == 0 || a >= hopAmount(rt, 0)/2 {\n\t\t\t\t\tb.hasFail = true\n\t\t\t\t\tb.failAmt = a\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(h.ChannelID)\n\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 9,
"parent": 1,
"score": 0.7396,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-directed-channel beliefs: hard lower-OK / upper-fail bounds\n// (no time decay: a stale bound costs one retry to refresh).\n// - Probability-aware Dijkstra: minimize fee + penalty/probability.\n// - JOINT route-set planning: plan a set of near-disjoint corridors up\n// front and size each shard to the max flow that corridor can bear,\n// rather than discovering splits through failure.\n// - Global attempt discipline: a hard per-payment attempt budget plus\n// \"no new information\" detection so a hopeless payment gives up fast\n// instead of burning 200 retries.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\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 = 40\n\n\t// attemptCost is the virtual cost (in msat) of one failed attempt,\n\t// used to trade fee against success probability.\n\tattemptCostBase = lnwire.MilliSatoshi(1_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 6\n\n\t// minShard is the smallest shard we will bother planning.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// attemptBudget bounds attempts for a single payment. Past this the\n\t// payment is almost certainly unroutable, and further retries only\n\t// hurt the retry-efficiency score.\n\tattemptBudget = 46\n\n\t// staleRounds is how many consecutive attempts may pass without any\n\t// new liquidity information before we declare the payment hopeless.\n\tstaleRounds = 6\n)\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; zero means unknown.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// fails counts how many times this direction has failed. Repeated\n\t// failures mean the channel is genuinely dry in this direction, so we\n\t// probe it far less aggressively.\n\tfails int\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\tp := 0.30*low + 0.70*cliff\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[uint64]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// attempts counts attempts issued for the current payment.\n\tattempts int\n\n\t// learnedAt is the attempt index at which we last gained new\n\t// liquidity information.\n\tlearnedAt int\n\n\t// noRouteStreak counts consecutive planning failures.\n\tnoRouteStreak int\n\n\t// planQueue holds shard amounts from a joint plan that we have\n\t// committed to but not yet handed out.\n\tplanQueue []lnwire.MilliSatoshi\n\n\t// planFor is the remaining amount the planQueue was built for; if the\n\t// remaining amount moves away from it the plan is discarded.\n\tplanFor lnwire.MilliSatoshi\n\n\t// lastRemaining tracks the remaining amount handed to us, so we can\n\t// detect the start of a new payment and reset per-payment counters.\n\tlastRemaining lnwire.MilliSatoshi\n\n\t// started is false until the first RequestRoute call.\n\tstarted 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[uint64]*belief),\n\t\tpending: make(map[uint64]*route.Route),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(r.localEdges, e)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(chanID uint64) *belief {\n\tb, ok := r.beliefs[chanID]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[chanID] = b\n\t}\n\treturn b\n}\n\n// probeLimit is the largest amount we are still willing to push over an\n// edge given its failure history. Rather than blacklisting, we bisect down\n// toward the proven-good bound; repeated failures shrink the window fast so\n// we stop re-probing a dry channel forever.\nfunc probeLimit(b *belief) lnwire.MilliSatoshi {\n\tif !b.hasFail || b.failAmt == 0 {\n\t\treturn lnwire.MilliSatoshi(math.MaxUint64 / 4)\n\t}\n\n\tlim := b.failAmt - 1\n\tif b.okAmt > 0 && b.okAmt < lim {\n\t\t// Bisect between proven-good and proven-bad.\n\t\tlim = b.okAmt + (lim-b.okAmt)/2\n\t} else {\n\t\t// No good bound: back off geometrically with repeat failures.\n\t\tswitch {\n\t\tcase b.fails <= 1:\n\t\t\tlim = b.failAmt / 2\n\t\tcase b.fails == 2:\n\t\t\tlim = b.failAmt / 5\n\t\tcase b.fails == 3:\n\t\t\tlim = b.failAmt / 12\n\t\tdefault:\n\t\t\treturn 0\n\t\t}\n\t}\n\treturn lim\n}\n\n// avail is the amount we still consider spendable over this edge, taking\n// our own in-flight HTLCs and local balance into account. It returns zero\n// when the edge is unusable at any amount.\nfunc (r *router) avail(e *edge) lnwire.MilliSatoshi {\n\tcap := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < cap {\n\t\tcap = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b != nil {\n\t\tif lim := probeLimit(b); lim < cap {\n\t\t\tcap = lim\n\t\t}\n\t\tif b.inFlight >= cap {\n\t\t\treturn 0\n\t\t}\n\t\tcap -= b.inFlight\n\t}\n\n\tif e.from == r.source {\n\t\tbal := r.localBalances[e.chanID]\n\t\tif bal < cap {\n\t\t\tcap = bal\n\t\t}\n\t}\n\n\treturn cap\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tb := r.beliefs[e.chanID]\n\t\tused := lnwire.MilliSatoshi(0)\n\t\tif b != nil {\n\t\t\tused = b.inFlight\n\t\t}\n\t\tif r.localBalances[e.chanID] >= amt+used {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b == nil {\n\t\treturn bimodalPrior(amt, e.capacity)\n\t}\n\n\teff := amt + b.inFlight\n\n\tif b.okAmt >= eff {\n\t\treturn knownProb\n\t}\n\tif b.hasFail && eff >= b.failAmt {\n\t\treturn minProb / 2\n\t}\n\n\tp := bimodalPrior(eff, e.capacity)\n\n\t// Narrow with the bounds we do have: known-good below and known-bad\n\t// above compress the uncertain interval.\n\tif b.okAmt > 0 && b.hasFail {\n\t\tlo := float64(b.okAmt)\n\t\thi := float64(b.failAmt)\n\t\tif hi > lo {\n\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\tbracket := 1 - frac\n\t\t\tp = 0.5*p + 0.5*bracket\n\t\t}\n\t} else if b.okAmt > 0 {\n\t\t// A prior success raises confidence for nearby amounts.\n\t\tratio := float64(eff) / float64(b.okAmt)\n\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\tp = 0.45*p + 0.55*boost\n\t} else if b.hasFail {\n\t\t// A prior failure at failAmt: everything meaningfully below it\n\t\t// is still plausible, but discount. Repeated failures on the\n\t\t// same direction mean it is dry, not unlucky.\n\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\tdamp := 0.85 + 0.05*float64(b.fails)\n\t\tif damp > 0.97 {\n\t\t\tdamp = 0.97\n\t\t}\n\t\tp = p * (1 - damp*frac)\n\t}\n\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\t// dist is the combined cost used for ordering.\n\tdist float64\n\t// amt is the amount that must arrive at node.\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tcltv uint32\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\tdist float64\n\tvia *edge\n\thops int\n\tcltv uint32\n}\n\n// findRoute runs a probability-weighted backward Dijkstra. The cost of a\n// path is its fee plus an attempt-cost term divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused (already carrying a shard\n// of this payment along a chosen route set).\nfunc (r *router) findRoute(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase +\n\t\tamt*attemptCostPPM/1_000_000)\n\n\tbest := make(map[route.Vertex]*pathState)\n\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{\n\t\tamt: amt,\n\t\tlogProb: 0,\n\t\tdist: 0,\n\t\thops: 0,\n\t\tcltv: finalCltvDelta,\n\t}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{\n\t\tnode: target, dist: 0, amt: amt, logProb: 0,\n\t\thops: 0, cltv: finalCltvDelta,\n\t})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.avail(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif e.from == r.source {\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-3 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\n\t\t\t// Risk-adjusted cost: expected fee plus expected retry\n\t\t\t// cost. A small per-hop term discourages long paths.\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*250\n\n\t\t\tprev, ok := best[e.from]\n\t\t\tif ok && dist >= prev.dist-1e-9 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\tdist: dist,\n\t\t\t\tvia: e,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, best)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, math.Exp(src.logProb), nil\n}\n\n// buildRoute materializes the route from the next-pointer map.\nfunc (r *router) buildRoute(amt lnwire.MilliSatoshi,\n\tbest map[route.Vertex]*pathState) (*route.Route, error) {\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// --- Joint route-set planning ---------------------------------------------\n\n// maxSendable finds the largest amount, by binary search, for which a route\n// exists avoiding the given channels. It returns 0 when even the smallest\n// probe is unroutable.\nfunc (r *router) maxSendable(hi lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) lnwire.MilliSatoshi {\n\n\tif hi < minShard {\n\t\treturn 0\n\t}\n\tif _, _, err := r.findRoute(hi, avoid); err == nil {\n\t\treturn hi\n\t}\n\n\tlo := lnwire.MilliSatoshi(0)\n\tfor i := 0; i < 12 && hi-lo > hi/32+1; i++ {\n\t\tmid := lo + (hi-lo)/2\n\t\tif mid < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif _, _, err := r.findRoute(mid, avoid); err == nil {\n\t\t\tlo = mid\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo < minShard {\n\t\treturn 0\n\t}\n\treturn lo\n}\n\n// planShards builds a joint route set for the remaining amount: it greedily\n// carves the largest shard each disjoint corridor can bear until the amount\n// is covered or we run out of parts/corridors. The returned amounts sum to\n// at most remaining.\nfunc (r *router) planShards(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\treturn nil\n\t}\n\n\t// Corridors chosen so far must not be reused by later shards, so\n\t// unequal parallel corridors each carry what they can bear.\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor k := range busy {\n\t\tavoid[k] = true\n\t}\n\n\tvar out []lnwire.MilliSatoshi\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft && left >= minShard; k++ {\n\t\tamt := r.maxSendable(left, avoid)\n\t\tif amt == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\trt, _, err := r.findRoute(amt, avoid)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, amt)\n\t\tleft -= amt\n\n\t\t// Reserve this corridor's hops so the next shard looks\n\t\t// elsewhere and the corridors stay near-disjoint.\n\t\tfor _, h := range rt.Hops {\n\t\t\tavoid[h.ChannelID] = true\n\t\t}\n\t}\n\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs, so\n// a new shard is planned over a genuinely disjoint corridor where possible.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// firstHopBudget returns the total liquidity we believe is reachable out of\n// our own channels, which upper-bounds any single payment.\nfunc (r *router) firstHopBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.avail(e)\n\t}\n\treturn total\n}\n\n// candidateAmounts builds a descending ladder of shard sizes to try for a\n// remaining amount, used as a fallback when joint planning comes up empty.\nfunc (r *router) candidateAmounts(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > remaining || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(remaining)\n\n\t// Even splits over all part counts we could still afford.\n\tfor k := uint32(2); k <= partsLeft && k <= 12; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric ladder for finer probing when even splits all fail.\n\tcur := remaining\n\tfor i := 0; i < 10; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < minShard {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: just under each proven failure point and\n\t// each proven-good amount.\n\tfor _, b := range r.beliefs {\n\t\tif b.hasFail && b.failAmt > minShard*2 {\n\t\t\tadd(b.failAmt / 2)\n\t\t}\n\t\tif b.okAmt > 0 {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\n\tif len(out) > 14 {\n\t\tout = out[:14]\n\t}\n\treturn out\n}\n\n// newPayment resets the per-payment counters when the runner starts a new\n// payment. We detect this by the remaining amount rising: within a payment\n// it only ever falls as shards settle.\nfunc (r *router) newPayment(amt lnwire.MilliSatoshi) {\n\tr.attempts = 0\n\tr.learnedAt = 0\n\tr.noRouteStreak = 0\n\tr.planQueue = nil\n\tr.planFor = 0\n\tr.lastRemaining = amt\n\tr.started = true\n}\n\n// RequestRoute plans the next shard. It first tries to satisfy the whole\n// remaining amount over one route; failing that it plans a joint route set\n// whose shard sizes come from each corridor's actual carrying capacity.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\t// Detect a fresh payment: the remaining amount grew, or this is the\n\t// first call. Liquidity beliefs persist across payments (they are\n\t// what makes the batch learn), but attempt budgets do not.\n\tif !r.started || amt > r.lastRemaining {\n\t\tr.newPayment(amt)\n\t}\n\tr.lastRemaining = amt\n\n\tr.attempts++\n\n\t// Hard attempt budget: a payment that has burned this many probes is\n\t// overwhelmingly likely to be unroutable, and every further retry is\n\t// a straight cost with no upside.\n\tif r.attempts > attemptBudget {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\t// Stall detection: if the last several attempts taught us nothing new\n\t// about liquidity, more of the same will not either.\n\tif r.attempts-r.learnedAt > staleRounds && inFlightHtlcs == 0 {\n\t\treturn nil, errors.New(\"no progress: giving up\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Drop a stale plan if the remaining amount no longer matches what it\n\t// was built for.\n\tif r.planFor != amt {\n\t\tr.planQueue = nil\n\t\tr.planFor = 0\n\t}\n\n\t// Serve a committed plan entry first.\n\tfor len(r.planQueue) > 0 {\n\t\ta := r.planQueue[0]\n\t\tr.planQueue = r.planQueue[1:]\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, busy); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\tr.planQueue = nil\n\tr.planFor = 0\n\n\t// A shard can never exceed what our own channels can push right now.\n\tbudget := r.firstHopBudget()\n\ttarget := amt\n\tif budget > 0 && budget < target {\n\t\ttarget = budget\n\t}\n\tif target < minShard {\n\t\ttarget = amt\n\t}\n\n\t// Fast path: the whole remaining amount over one good route.\n\tif target == amt {\n\t\tif rt, p, err := r.findRoute(target, busy); err == nil &&\n\t\t\tp > 0.5 {\n\n\t\t\tr.noRouteStreak = 0\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\n\t// Joint planning: size shards to corridors instead of halving. This\n\t// is the main lever for large payments that no single path can carry.\n\tif partsLeft > 1 {\n\t\tplan := r.planShards(target, partsLeft, busy)\n\n\t\t// Only commit to a plan that actually covers a useful slice of\n\t\t// the remaining amount; a plan of dust shards just burns\n\t\t// attempts.\n\t\tvar covered lnwire.MilliSatoshi\n\t\tfor _, a := range plan {\n\t\t\tcovered += a\n\t\t}\n\n\t\tif len(plan) > 0 && covered >= target/4 {\n\t\t\t// Largest shard first: it is the one most likely to be\n\t\t\t// invalidated by drift, so probe it while our belief\n\t\t\t// is freshest.\n\t\t\tsort.Slice(plan, func(i, j int) bool {\n\t\t\t\treturn plan[i] > plan[j]\n\t\t\t})\n\t\t\tfirst := plan[0]\n\t\t\tr.planQueue = plan[1:]\n\t\t\tr.planFor = amt - first\n\n\t\t\tif rt, _, err := r.findRoute(first, busy); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tif rt, _, err := r.findRoute(first, nil); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tr.planQueue = nil\n\t\t\tr.planFor = 0\n\t\t}\n\t}\n\n\t// Fallback: descend a ladder of amounts, scoring each by expected\n\t// delivered value net of fee.\n\ttype plan struct {\n\t\trt *route.Route\n\t\tscore float64\n\t}\n\tvar bestPlan *plan\n\n\tladder := r.candidateAmounts(target, partsLeft)\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tavoid := busy\n\t\tif pass == 1 {\n\t\t\tavoid = nil\n\t\t}\n\t\tif pass == 0 && len(busy) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttried := 0\n\t\tfor _, a := range ladder {\n\t\t\trt, p, err := r.findRoute(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried++\n\n\t\t\tfee := float64(rt.TotalAmount) - float64(a)\n\t\t\tscore := p*float64(a) - fee*0.5\n\t\t\tif a == amt {\n\t\t\t\tscore *= 1.15\n\t\t\t}\n\n\t\t\tif bestPlan == nil || score > bestPlan.score {\n\t\t\t\tbestPlan = &plan{rt: rt, score: score}\n\t\t\t}\n\n\t\t\tif a == amt && p > 0.7 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif tried >= 6 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif bestPlan != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestPlan == nil {\n\t\t// Last resort: probe for whatever amount is routable at all,\n\t\t// so a depleted corridor still yields progress rather than a\n\t\t// terminal give-up.\n\t\tif a := r.maxSendable(target, nil); a >= minShard {\n\t\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\t\tr.noRouteStreak = 0\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t}\n\n\t\tr.noRouteStreak++\n\t\tif r.noRouteStreak > 2 {\n\t\t\treturn nil, errors.New(\"no route found\")\n\t\t}\n\t\treturn nil, errors.New(\"no route available\")\n\t}\n\n\tr.noRouteStreak = 0\n\treturn bestPlan.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(h.ChannelID)\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// recordOK raises the proven-good bound on a channel and clears any stale\n// failure bound at or below that amount. It reports whether this was new\n// information.\nfunc (r *router) recordOK(chanID uint64, a lnwire.MilliSatoshi) bool {\n\tb := r.bel(chanID)\n\tlearned := false\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t\tlearned = true\n\t}\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t\tlearned = true\n\t}\n\treturn learned\n}\n\n// recordFail lowers the proven-bad bound on a channel. It reports whether\n// this was new information.\nfunc (r *router) recordFail(chanID uint64, a lnwire.MilliSatoshi) bool {\n\tb := r.bel(chanID)\n\tlearned := false\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t\tlearned = true\n\t}\n\tb.fails++\n\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\treturn learned\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tlearned := false\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tif r.recordOK(h.ChannelID, a) {\n\t\t\t\tlearned = true\n\t\t\t}\n\n\t\t\tif i == 0 && h.ChannelID != 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif learned {\n\t\t\tr.learnedAt = r.attempts\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tif failIdx > 0 {\n\t\tfor i := 0; i < failIdx && i < len(rt.Hops); i++ {\n\t\t\tif r.recordOK(rt.Hops[i].ChannelID, hopAmount(rt, i)) {\n\t\t\t\tlearned = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\t// Unattributable failure (e.g. an unknown source): the whole\n\t\t// route is suspect, so bound the likely bottleneck hops rather\n\t\t// than learning nothing at all.\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tb := r.bel(h.ChannelID)\n\t\t\tif b.okAmt >= a {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif i == 0 || a >= hopAmount(rt, 0)/2 {\n\t\t\t\tif r.recordFail(h.ChannelID, a) {\n\t\t\t\t\tlearned = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif learned {\n\t\t\tr.learnedAt = r.attempts\n\t\t}\n\t\treturn nil\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\n\tif r.recordFail(h.ChannelID, a) {\n\t\tlearned = true\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif learned {\n\t\tr.learnedAt = r.attempts\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 10,
"parent": 2,
"score": 0.4224,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-directed-channel beliefs: hard lower-OK / upper-fail bounds\n// (no time decay: a stale bound costs one retry to refresh).\n// - Probability-aware Dijkstra: minimize fee + penalty/probability.\n// - JOINT route-set planning: plan a set of disjoint corridors up front\n// and size each shard to the max flow that corridor can bear, so\n// unequal parallel corridors each carry what they can.\n// - Lower-retry on failed channels rather than blacklisting, with a\n// bisection ladder between the proven-ok and proven-fail bounds.\n// - Attempt budget is spent per-payment, not per-batch: a payment that\n// cannot be covered gives up early instead of burning 200 attempts,\n// which is where the previous version lost its retry score.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"math\"\n\t\"sort\"\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 = 40\n\n\t// attemptCost is the virtual cost (in msat) of one failed attempt,\n\t// used to trade fee against success probability.\n\tattemptCostBase = lnwire.MilliSatoshi(1_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// minShard is the smallest shard we will bother planning.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// maxAttempts bounds the attempts we spend on a single payment. The\n\t// runner reuses one router per payment sequence, so this is tracked\n\t// against the current payment target/amount rather than the batch.\n\tmaxAttempts = 46\n\n\t// stuckLimit is how many consecutive attempts may fail to teach us\n\t// anything new before we declare the payment hopeless.\n\tstuckLimit = 8\n)\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; zero means unknown.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// fails counts how many times this direction has refused us. Repeated\n\t// refusals mean the channel is drained, not merely tight.\n\tfails int\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\tp := 0.30*low + 0.70*cliff\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[uint64]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// attempts counts attempts issued for the current payment.\n\tattempts int\n\n\t// stuck counts consecutive attempts that taught us nothing new.\n\tstuck int\n\n\t// learned is set whenever a report tightens a belief bound.\n\tlearned bool\n\n\t// curAmt is the remaining amount the current payment run started at,\n\t// used to detect that a new payment began (remaining went up).\n\tcurAmt lnwire.MilliSatoshi\n\n\t// settled is the total we have delivered for the current payment.\n\tsettled lnwire.MilliSatoshi\n\n\t// planQueue holds shard amounts from a joint plan that we have\n\t// committed to but not yet handed out.\n\tplanQueue []lnwire.MilliSatoshi\n\n\t// planFor is the remaining amount the planQueue was built for; if the\n\t// remaining amount moves away from it the plan is discarded.\n\tplanFor lnwire.MilliSatoshi\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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[uint64]*belief),\n\t\tpending: make(map[uint64]*route.Route),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(r.localEdges, e)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(chanID uint64) *belief {\n\tb, ok := r.beliefs[chanID]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[chanID] = b\n\t}\n\treturn b\n}\n\n// retryCap is the largest amount we are still willing to push over a\n// channel that has refused us, bisecting between the proven bounds. After\n// several refusals we stop coming back at all.\nfunc retryCap(b *belief) (lnwire.MilliSatoshi, bool) {\n\tif !b.hasFail || b.failAmt == 0 {\n\t\treturn 0, false\n\t}\n\tif b.fails >= 3 {\n\t\t// Thoroughly drained: only tiny amounts remain plausible.\n\t\tif b.okAmt > 0 {\n\t\t\treturn b.okAmt, true\n\t\t}\n\t\treturn 0, true\n\t}\n\n\tlim := b.failAmt - 1\n\tif b.okAmt > 0 && b.okAmt < lim {\n\t\tlim = b.okAmt + (lim-b.okAmt)/2\n\t} else {\n\t\t// No proven-good floor: bimodal liquidity means the channel is\n\t\t// probably empty in this direction, so drop hard rather than\n\t\t// shaving a quarter off.\n\t\tlim = b.failAmt / 3\n\t}\n\treturn lim, true\n}\n\n// avail is the amount we still consider spendable over this edge, taking\n// our own in-flight HTLCs and local balance into account. It returns zero\n// when the edge is unusable at any amount.\nfunc (r *router) avail(e *edge) lnwire.MilliSatoshi {\n\tcap := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < cap {\n\t\tcap = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b != nil {\n\t\tif lim, ok := retryCap(b); ok && lim < cap {\n\t\t\tcap = lim\n\t\t}\n\t\tif b.inFlight >= cap {\n\t\t\treturn 0\n\t\t}\n\t\tcap -= b.inFlight\n\t}\n\n\tif e.from == r.source {\n\t\tbal := r.localBalances[e.chanID]\n\t\tif bal < cap {\n\t\t\tcap = bal\n\t\t}\n\t}\n\n\treturn cap\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tb := r.beliefs[e.chanID]\n\t\tused := lnwire.MilliSatoshi(0)\n\t\tif b != nil {\n\t\t\tused = b.inFlight\n\t\t}\n\t\tif r.localBalances[e.chanID] >= amt+used {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tb := r.beliefs[e.chanID]\n\tif b == nil {\n\t\treturn bimodalPrior(amt, e.capacity)\n\t}\n\n\teff := amt + b.inFlight\n\n\tif b.okAmt >= eff {\n\t\treturn knownProb\n\t}\n\tif b.hasFail && eff >= b.failAmt {\n\t\treturn minProb / 2\n\t}\n\n\tp := bimodalPrior(eff, e.capacity)\n\n\t// Narrow with the bounds we do have: known-good below and known-bad\n\t// above compress the uncertain interval.\n\tif b.okAmt > 0 && b.hasFail {\n\t\tlo := float64(b.okAmt)\n\t\thi := float64(b.failAmt)\n\t\tif hi > lo {\n\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\tbracket := 1 - frac\n\t\t\tp = 0.5*p + 0.5*bracket\n\t\t}\n\t} else if b.okAmt > 0 {\n\t\t// A prior success raises confidence for nearby amounts.\n\t\tratio := float64(eff) / float64(b.okAmt)\n\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\tp = 0.45*p + 0.55*boost\n\t} else if b.hasFail {\n\t\t// A prior failure at failAmt: everything meaningfully below it\n\t\t// is still plausible, but discount. Repeated failures on the\n\t\t// same direction discount harder.\n\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\tpen := 0.85 + 0.05*float64(b.fails)\n\t\tif pen > 0.97 {\n\t\t\tpen = 0.97\n\t\t}\n\t\tp = p * (1 - pen*frac)\n\t}\n\n\tif p > maxProb {\n\t\tp = maxProb\n\t}\n\tif p < minProb {\n\t\tp = minProb\n\t}\n\treturn p\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\t// dist is the combined cost used for ordering.\n\tdist float64\n\t// amt is the amount that must arrive at node.\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tcltv uint32\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tamt lnwire.MilliSatoshi\n\tlogProb float64\n\tdist float64\n\tvia *edge\n\thops int\n\tcltv uint32\n}\n\n// findRoute runs a probability-weighted backward Dijkstra. The cost of a\n// path is its fee plus an attempt-cost term divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused (already carrying a shard\n// of this payment along a chosen route set).\nfunc (r *router) findRoute(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase +\n\t\tamt*attemptCostPPM/1_000_000)\n\n\tbest := make(map[route.Vertex]*pathState)\n\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{\n\t\tamt: amt,\n\t\tlogProb: 0,\n\t\tdist: 0,\n\t\thops: 0,\n\t\tcltv: finalCltvDelta,\n\t}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{\n\t\tnode: target, dist: 0, amt: amt, logProb: 0,\n\t\thops: 0, cltv: finalCltvDelta,\n\t})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.avail(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif e.from == r.source {\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-3 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\n\t\t\t// Risk-adjusted cost: expected fee plus expected retry\n\t\t\t// cost. A small per-hop term discourages long paths.\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*250\n\n\t\t\tprev, ok := best[e.from]\n\t\t\tif ok && dist >= prev.dist-1e-9 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\tdist: dist,\n\t\t\t\tvia: e,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t\tcltv: it.cltv + uint32(e.cltv),\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, best)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, math.Exp(src.logProb), nil\n}\n\n// buildRoute materializes the route from the next-pointer map.\nfunc (r *router) buildRoute(amt lnwire.MilliSatoshi,\n\tbest map[route.Vertex]*pathState) (*route.Route, error) {\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// --- Joint route-set planning ---------------------------------------------\n\n// maxSendable finds the largest amount, by binary search, for which a route\n// exists avoiding the given channels. It returns 0 when even the smallest\n// probe is unroutable.\nfunc (r *router) maxSendable(hi lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) lnwire.MilliSatoshi {\n\n\tif hi < minShard {\n\t\treturn 0\n\t}\n\tif _, _, err := r.findRoute(hi, avoid); err == nil {\n\t\treturn hi\n\t}\n\n\tlo := lnwire.MilliSatoshi(0)\n\tfor i := 0; i < 12 && hi-lo > hi/32+1; i++ {\n\t\tmid := lo + (hi-lo)/2\n\t\tif mid < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif _, _, err := r.findRoute(mid, avoid); err == nil {\n\t\t\tlo = mid\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo < minShard {\n\t\treturn 0\n\t}\n\treturn lo\n}\n\n// planShards builds a joint route set for the remaining amount: it greedily\n// carves the largest shard each disjoint corridor can bear until the amount\n// is covered or we run out of parts/corridors. The returned amounts sum to\n// at most remaining; an empty result means we could not cover it and the\n// caller should fall back to a single best-effort shard.\nfunc (r *router) planShards(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\treturn nil\n\t}\n\n\t// Corridors chosen so far must not be reused by later shards, so\n\t// unequal parallel corridors each carry what they can bear.\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor k := range busy {\n\t\tavoid[k] = true\n\t}\n\n\tvar out []lnwire.MilliSatoshi\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft && left >= minShard; k++ {\n\t\tamt := r.maxSendable(left, avoid)\n\t\tif amt == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\trt, _, err := r.findRoute(amt, avoid)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, amt)\n\t\tleft -= amt\n\n\t\t// Reserve this corridor's hops so the next shard looks\n\t\t// elsewhere and the two do not contend for one bottleneck.\n\t\tfor _, h := range rt.Hops {\n\t\t\tavoid[h.ChannelID] = true\n\t\t}\n\t}\n\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs, so\n// a new shard is planned over a genuinely disjoint corridor where possible.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// firstHopBudget returns the total liquidity we believe is reachable out of\n// our own channels, which upper-bounds any single payment.\nfunc (r *router) firstHopBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.avail(e)\n\t}\n\treturn total\n}\n\n// candidateAmounts builds a descending ladder of shard sizes to try for a\n// remaining amount, used as a fallback when joint planning comes up empty.\nfunc (r *router) candidateAmounts(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > remaining || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(remaining)\n\n\t// Even splits over all part counts we could still afford.\n\tfor k := uint32(2); k <= partsLeft && k <= 12; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric ladder for finer probing when even splits all fail.\n\tcur := remaining\n\tfor i := 0; i < 10; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < minShard {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\n\tif len(out) > 14 {\n\t\tout = out[:14]\n\t}\n\treturn out\n}\n\n// startPayment resets the per-payment budget when the runner moves on to a\n// new payment. The remaining amount only ever shrinks within one payment,\n// so a jump upward (or the very first call) marks a fresh one.\nfunc (r *router) startPayment(amt lnwire.MilliSatoshi) {\n\tr.attempts = 0\n\tr.stuck = 0\n\tr.curAmt = amt\n\tr.settled = 0\n\tr.planQueue = nil\n\tr.planFor = 0\n\n\t// Old in-flight bookkeeping cannot belong to the new payment.\n\tr.pending = make(map[uint64]*route.Route)\n\tfor _, b := range r.beliefs {\n\t\tb.inFlight = 0\n\t}\n}\n\n// RequestRoute plans the next shard. It first tries to satisfy the whole\n// remaining amount over one route; failing that it plans a joint route set\n// whose shard sizes come from each corridor's actual carrying capacity.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\t// Detect a new payment: within one payment the remaining amount never\n\t// grows, so an increase means the runner started a fresh one.\n\tif amt > r.curAmt || r.curAmt == 0 {\n\t\tr.startPayment(amt)\n\t}\n\n\tr.attempts++\n\tif r.attempts > maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.stuck > stuckLimit {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Drop a stale plan if the remaining amount no longer matches what it\n\t// was built for.\n\tif r.planFor != amt {\n\t\tr.planQueue = nil\n\t\tr.planFor = 0\n\t}\n\n\t// Serve a committed plan entry first.\n\tfor len(r.planQueue) > 0 {\n\t\ta := r.planQueue[0]\n\t\tr.planQueue = r.planQueue[1:]\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, busy); err == nil {\n\t\t\treturn rt, nil\n\t\t}\n\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\tr.planQueue = nil\n\tr.planFor = 0\n\n\t// A shard can never exceed what our own channels can push right now.\n\tbudget := r.firstHopBudget()\n\ttarget := amt\n\tif budget > 0 && budget < target {\n\t\ttarget = budget\n\t}\n\tif target < minShard {\n\t\ttarget = amt\n\t}\n\n\t// Fast path: the whole remaining amount over one good route.\n\tif target == amt {\n\t\tif rt, p, err := r.findRoute(target, busy); err == nil &&\n\t\t\tp > 0.5 {\n\n\t\t\treturn rt, nil\n\t\t}\n\t}\n\n\t// Joint planning: size shards to corridors instead of halving.\n\tif partsLeft > 1 {\n\t\tplan := r.planShards(target, partsLeft, busy)\n\t\tif len(plan) > 0 {\n\t\t\t// Largest shard first: it is the one most likely to be\n\t\t\t// invalidated by drift, so probe it while our belief\n\t\t\t// is freshest.\n\t\t\tsort.Slice(plan, func(i, j int) bool {\n\t\t\t\treturn plan[i] > plan[j]\n\t\t\t})\n\t\t\tfirst := plan[0]\n\t\t\trest := plan[1:]\n\n\t\t\t// Only keep a queue that still adds up to the rest of\n\t\t\t// the payment; otherwise re-plan next call with fresh\n\t\t\t// evidence.\n\t\t\tvar sum lnwire.MilliSatoshi\n\t\t\tfor _, a := range rest {\n\t\t\t\tsum += a\n\t\t\t}\n\t\t\tif first+sum >= amt {\n\t\t\t\tr.planQueue = rest\n\t\t\t\tr.planFor = amt - first\n\t\t\t}\n\n\t\t\tif rt, _, err := r.findRoute(first, busy); err == nil {\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tif rt, _, err := r.findRoute(first, nil); err == nil {\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t\tr.planQueue = nil\n\t\t\tr.planFor = 0\n\t\t}\n\t}\n\n\t// Fallback: descend a ladder of amounts, scoring each by expected\n\t// delivered value net of fee.\n\ttype plan struct {\n\t\trt *route.Route\n\t\tscore float64\n\t}\n\tvar bestPlan *plan\n\n\tladder := r.candidateAmounts(target, partsLeft)\n\n\tfor pass := 0; pass < 2; pass++ {\n\t\tavoid := busy\n\t\tif pass == 1 {\n\t\t\tavoid = nil\n\t\t}\n\t\tif pass == 0 && len(busy) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ttried := 0\n\t\tfor _, a := range ladder {\n\t\t\trt, p, err := r.findRoute(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried++\n\n\t\t\tfee := float64(rt.TotalAmount) - float64(a)\n\t\t\tscore := p*float64(a) - fee*0.5\n\t\t\tif a == amt {\n\t\t\t\tscore *= 1.15\n\t\t\t}\n\n\t\t\tif bestPlan == nil || score > bestPlan.score {\n\t\t\t\tbestPlan = &plan{rt: rt, score: score}\n\t\t\t}\n\n\t\t\tif a == amt && p > 0.65 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif tried >= 6 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif bestPlan != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestPlan == nil {\n\t\t// Last resort: probe for whatever amount is routable at all,\n\t\t// so a depleted corridor still yields progress rather than a\n\t\t// terminal give-up.\n\t\tif a := r.maxSendable(target, nil); a >= minShard {\n\t\t\tif rt, _, err := r.findRoute(a, nil); err == nil {\n\t\t\t\treturn rt, nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, errors.New(\"no route available\")\n\t}\n\n\treturn bestPlan.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(h.ChannelID)\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// recordOK raises the proven-good bound on a channel and clears any stale\n// failure bound at or below that amount.\nfunc (r *router) recordOK(chanID uint64, a lnwire.MilliSatoshi) {\n\tb := r.bel(chanID)\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t\tr.learned = true\n\t}\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t\tr.learned = true\n\t}\n}\n\n// recordFail lowers the proven-fail bound on a channel.\nfunc (r *router) recordFail(chanID uint64, a lnwire.MilliSatoshi) {\n\tb := r.bel(chanID)\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t\tr.learned = true\n\t}\n\tb.fails++\n\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\tr.learned = false\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.recordOK(h.ChannelID, a)\n\n\t\t\tif i == 0 && h.ChannelID != 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tr.stuck = 0\n\t\tif len(rt.Hops) > 0 {\n\t\t\tr.settled += rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tif failIdx > 0 {\n\t\tfor i := 0; i < failIdx && i < len(rt.Hops); i++ {\n\t\t\tr.recordOK(rt.Hops[i].ChannelID, hopAmount(rt, i))\n\t\t}\n\t}\n\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\t// Unattributable failure (e.g. an unknown source): the whole\n\t\t// route is suspect, so bound the likely bottleneck rather than\n\t\t// learning nothing at all.\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tb := r.bel(h.ChannelID)\n\t\t\tif b.okAmt >= a {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif i == 0 || a >= hopAmount(rt, 0)/2 {\n\t\t\t\tr.recordFail(h.ChannelID, a)\n\t\t\t}\n\t\t}\n\t\tif r.learned {\n\t\t\tr.stuck = 0\n\t\t} else {\n\t\t\tr.stuck++\n\t\t}\n\t\treturn nil\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tr.recordFail(h.ChannelID, a)\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif r.learned {\n\t\tr.stuck = 0\n\t} else {\n\t\tr.stuck++\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 11,
"parent": 1,
"score": 0.6867,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), so later larger payments reuse a proven corridor.\n// - JOINT ROUTE-SET PLANNING (the main change in this revision): instead of\n// planning one shard and discovering the split by failing, we build a\n// min-cost-flow style plan over several DISJOINT corridors up front,\n// sizing each shard to that corridor's believed bottleneck, and we\n// RESERVE the plan so all its shards are handed out in sequence. Large\n// payments that no single corridor can carry (the observed failure mode:\n// ~860M and ~1041M msat payments dying with \"no route found\" after a\n// dozen single-corridor probes) are now attacked as a flow problem from\n// the very first attempt whenever the amount exceeds what one corridor\n// is believed able to bear.\n// - FLOW FEASIBILITY over local channels: a shard is never larger than the\n// single local channel that must carry it, and the planner spreads over\n// distinct local channels first, which is where the real bottleneck sits.\n// - Depth-aware lower retries: how far below a proven failure we retry\n// depends on how depleted the direction looks.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the local\n// policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 70\n\tmaxFailStreak = 24\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowSlices bounds how many corridors a joint plan may use beyond the\n\t// part budget, so planning stays cheap.\n\tflowSlices = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked for shards of the current joint plan\n\t// that have not been handed out yet, so sibling shards of one plan do\n\t// not double-book the same corridor.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is the liquidity of a directed channel already spoken for, by\n// in-flight HTLCs and by unserved shards of the current joint plan.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.committed()\n\n\tmisses := 0\n\tif b != nil {\n\t\tmisses = b.misses\n\t}\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// maxLocalHop is the largest amount any single local channel can still send.\n// No single shard can exceed this, because every shard leaves through exactly\n// one of our channels.\nfunc (r *router) maxLocalHop() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the corridor's bound from hard evidence only, used as a\n// ceiling when re-pricing a corridor.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// reserve books (or releases) a plan's liquidity against the beliefs so that\n// sibling shards of the same joint plan cannot double-spend a corridor.\nfunc (r *router) reserve(pl *plan, sign int) {\n\tif pl == nil || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif sign > 0 {\n\t\t\tb.reserved += a\n\t\t} else if b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n}\n\n// clearQueue drops every queued shard and releases its reservations.\nfunc (r *router) clearQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.reserve(pl, -1)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// Reservations are taken as each slice is chosen, so the next slice's\n// Dijkstra already sees the liquidity consumed by its siblings even where\n// corridors share a downstream hop that is not fully excluded.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tslices := int(partsLeft)\n\tif slices > flowSlices {\n\t\tslices = flowSlices\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\n\tfor k := 0; k < slices; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findAnyPath(left, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif hb := r.hardBottleneck(path); hb < amtS {\n\t\t\tamtS = hb\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, pl)\n\n\t\t// Book this slice so siblings see the liquidity as taken, and\n\t\t// keep the first local hop exclusive: every shard leaves\n\t\t// through one of our channels and we want them spread.\n\t\tr.reserve(pl, +1)\n\t\tavoid[path[0].chanID] = true\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\t// Release everything; the caller re-books whatever it decides to keep\n\t// queued, and the shard it serves immediately becomes in-flight.\n\tfor _, pl := range out {\n\t\tr.reserve(pl, -1)\n\t}\n\n\treturn out\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan; otherwise, when the remaining amount exceeds\n// what a single local channel could ever carry, it plans a joint route set\n// straight away, and only falls back to single-corridor search when one\n// corridor plausibly covers the whole remainder.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.reserve(pl, -1)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.clearQueue()\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what one of our own channels can push.\n\thi := amt\n\tif one := r.maxLocalHop(); one > 0 && one < hi {\n\t\thi = one\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// When a single corridor cannot plausibly cover the remainder, go\n\t// straight to joint flow planning: that is where large payments live,\n\t// and probing one corridor at a time just burns attempts.\n\tflowFirst := partsLeft > 1 && hi < amt\n\n\tvar best *plan\n\n\tif flowFirst {\n\t\tif flow := r.planFlow(amt, partsLeft, busy); len(flow) > 0 {\n\t\t\tbest = flow[0]\n\t\t\tr.queued = flow[1:]\n\t\t\tfor _, pl := range r.queued {\n\t\t\t\tr.reserve(pl, +1)\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\tladder := r.ladder(hi, amt, partsLeft)\n\n\t\t// First pass prefers corridors disjoint from in-flight shards\n\t\t// so parallel parts do not fight over the same liquidity.\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\n\t\t\tprobes := 0\n\t\t\tfor _, a := range ladder {\n\t\t\t\t// Once we hold a solid plan, digging far below\n\t\t\t\t// it only wastes attempts and parts.\n\t\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\t\ta < best.amt/2 {\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif probes >= probeBudget {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\t\tprobes++\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t\t// A confident full-amount plan needs nothing\n\t\t\t\t// more.\n\t\t\t\tif best != nil && best.amt >= amt &&\n\t\t\t\t\tbest.prob > 0.8 {\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif best != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif !flowFirst && partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.8)\n\n\t\t\tif better {\n\t\t\t\tr.clearQueue()\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl, +1)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\trb.succ = true\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.clearQueue()\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 12,
"parent": 3,
"score": 0.9059,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - Probability-aware backward Dijkstra: minimize fee plus an attempt\n// cost divided by the path's success probability.\n// - Joint shard sizing: every corridor we discover is re-priced at the\n// amount that corridor can actually bear (its belief bottleneck), so\n// unequal parallel corridors each carry a shard sized to fit instead\n// of a blind half of the remainder.\n// - Attempt economy (the main change in this revision): success rate was\n// already saturated, so the remaining score comes from spending fewer\n// attempts. Three mechanisms buy that:\n// 1. A per-corridor \"probe\" ladder that walks DOWN through the\n// believed bottleneck instead of blind halves, so the first\n// attempt on a fresh corridor is already sized to what the\n// evidence supports.\n// 2. Optimistic pre-pruning: a hop whose believed success at the\n// requested amount is under a floor is skipped by Dijkstra\n// entirely rather than attempted and learned from, so we do not\n// pay an attempt for information the prior already gives us.\n// 3. A first-attempt preference for larger shards: since each shard\n// costs at least one attempt, delivering the remainder in one\n// piece is worth a real bonus, and only after a failure do we\n// start carving.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// is deliberately large relative to fees.\n\tattemptCostBase = lnwire.MilliSatoshi(3_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(20_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// pruneProb is the per-hop probability below which we refuse to build\n\t// a route through a hop at all. Under a bimodal distribution a hop\n\t// this unlikely almost never pays for the attempt it costs.\n\tpruneProb = 0.06\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 60\n\tmaxFailStreak = 18\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.010\n\n\t// wholeBonus rewards a plan that finishes the remaining amount in a\n\t// single shard, because every extra shard costs at least one attempt.\n\twholeBonus = 1.30\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under a bimodal distribution this\n\t// sits comfortably on the plausible side of the cliff.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// lowerRetryNum/lowerRetryDen is the fraction of a proven failure\n\t// amount we are still willing to retry.\n\tlowerRetryNum = 3\n\tlowerRetryDen = 4\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// shardFails counts liquidity failures seen for the payment currently\n\t// being planned. While it is zero we bias hard toward a single shard,\n\t// since splitting speculatively costs attempts we may not need.\n\tshardFails 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\tif lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.hasFail {\n\t\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\t\tif est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Everything meaningfully below a proven failure is\n\t\t\t// still plausible, but discounted.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = p * (1 - 0.85*frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity. floor is the per-hop probability below\n// which a hop is pruned outright: attempting a hop we already believe is\n// hopeless burns an attempt to learn what the prior already told us.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\tfloor float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.15\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 || p < floor {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\n//\n// Before the first liquidity failure of a payment the ladder stays coarse\n// and top-heavy: probing small shards speculatively costs attempts, and an\n// unsplit payment is the cheapest outcome available.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\tif r.shardFails == 0 && hi >= remaining {\n\t\t// Nothing has failed yet and we can afford the whole thing:\n\t\t// offer only a couple of coarse fallbacks so the very first\n\t\t// attempt of a payment is the full amount.\n\t\tadd(remaining * 3 / 4)\n\t\tadd(remaining / 2)\n\t\tsort.Slice(out, func(i, j int) bool {\n\t\t\treturn out[i] > out[j]\n\t\t})\n\t\treturn out\n\t}\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: proven-good amounts and just under proven\n\t// failure points.\n\tfor _, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail && b.failAmt >= minShard {\n\t\t\tadd(b.failAmt * lowerRetryNum / lowerRetryDen)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= wholeBonus\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount, at the amount the\n// corridor is believed able to bear, and at a short descent below that,\n// keeping the best of them. Re-pricing an already-discovered corridor is\n// free, so this is where we spend search effort instead of on extra\n// Dijkstra runs, which are the expensive part.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\n\tbn := r.bottleneck(path)\n\tif bn > remaining {\n\t\tbn = remaining\n\t}\n\tif bn >= minShard && bn != a {\n\t\tcands = append(cands, bn)\n\t}\n\n\t// A short descent below the corridor's believed bottleneck. This is\n\t// the cheap way to find a shard that fits: each extra candidate is a\n\t// pricing pass, not a new path search.\n\tif r.shardFails > 0 && bn >= minShard {\n\t\tcur := bn\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tcur = cur * 5 / 8\n\t\t\tif cur < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcands = append(cands, cur)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{rt: rt, score: s, amt: c, prob: p}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard. It searches jointly over shard amount\n// and corridor: for every candidate amount (largest first) it finds the\n// best risk-adjusted path, then re-prices that path at the amount its\n// weakest hop can bear, and returns the highest scoring combination.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tbusy := r.inFlightChans()\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\t// Two prune floors: first insist on hops we genuinely believe in, and\n\t// only if that finds nothing at all do we relax and consider the\n\t// long shots. Most payments never need the second pass, so the\n\t// optimistic floor costs nothing and saves the attempts we would\n\t// otherwise burn on hops the prior already condemned.\n\tfloors := []float64{pruneProb, 0}\n\n\tvar best *plan\n\n\tfor _, floor := range floors {\n\t\t// First sub-pass prefers corridors disjoint from in-flight\n\t\t// shards so parallel parts do not fight over the same\n\t\t// liquidity.\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\n\t\t\tprobes := 0\n\t\t\tfor _, a := range ladder {\n\t\t\t\t// Once we hold a solid plan, digging far below\n\t\t\t\t// it only wastes attempts and parts.\n\t\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\t\ta < best.amt/2 {\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif probes >= probeBudget {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tpath, err := r.findPath(a, avoid, floor)\n\t\t\t\tprobes++\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t\t// A confident full-amount plan needs no\n\t\t\t\t// alternatives.\n\t\t\t\tif best != nil && best.amt >= amt &&\n\t\t\t\t\tbest.prob > 0.75 {\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif best != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.provePassed(h, a)\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settled shard closes out this payment's split state: the\n\t\t// next payment starts optimistic about single-shard delivery\n\t\t// again.\n\t\tr.shardFails = 0\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\tr.shardFails++\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\t// A policy repair is not a liquidity miss, so it must not\n\t\t// count against the fail streak or push us toward splitting.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction. From now\n\t// on this payment is allowed to plan genuine splits.\n\tr.shardFails++\n\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 13,
"parent": 2,
"score": 0.7642,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - Probability-aware backward Dijkstra: minimize fee plus an attempt\n// cost divided by the path's success probability.\n// - Joint shard sizing: every corridor we discover is re-priced at the\n// amount that corridor can actually bear (its belief bottleneck), so\n// unequal parallel corridors each carry a shard sized to fit.\n// - Corridor repulsion: successive probes within one RequestRoute pass\n// are pushed off channels we already priced, so the ladder surfaces\n// structurally different routes instead of re-pricing one corridor.\n// - Aggressive small-shard fallback: when the big amounts have all been\n// refuted, we keep descending instead of terminally giving up.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view and do not count against the fail streak.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// is deliberately large relative to fees.\n\tattemptCostBase = lnwire.MilliSatoshi(2_000)\n\tattemptCostPPM = lnwire.MilliSatoshi(12_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. Both are generous: a terminal give\n\t// up loses a whole payment, which dwarfs the retry penalty.\n\tmaxAttempts = 120\n\tmaxFailStreak = 40\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 16\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 3.0\n\tpartCost = 0.0025\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under a bimodal distribution this\n\t// sits comfortably on the plausible side of the cliff.\n\tpriorSafeNum = 38\n\tpriorSafeDen = 100\n\n\t// lowerRetryNum/lowerRetryDen is the fraction of a proven failure\n\t// amount we are still willing to retry.\n\tlowerRetryNum = 1\n\tlowerRetryDen = 2\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount proven to pass.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount proven to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// deliveredParts counts shards of the current payment that settled.\n\tdeliveredParts 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry well below the proven failure point rather than\n\t\t// blacklisting the channel outright. Under a bimodal\n\t\t// distribution a failure at a means the direction is mostly\n\t\t// drained, so the surviving window is small.\n\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\tif b.okAmt > lim {\n\t\t\tlim = b.okAmt\n\t\t}\n\t\tif lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.hasFail {\n\t\t\tlim := b.failAmt * lowerRetryNum / lowerRetryDen\n\t\t\tif b.okAmt > lim {\n\t\t\t\tlim = b.okAmt\n\t\t\t}\n\t\t\tif est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under a bimodal distribution a failure means the\n\t\t\t// direction is mostly drained: only meaningfully\n\t\t\t// smaller amounts stay plausible.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = p * math.Max(0, 1-1.05*frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity. penal names channels that are merely\n// discouraged (they belong to a corridor we already planned this pass), so\n// the search returns genuinely different corridors instead of the same one.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool, penal map[uint64]int) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.12\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\t// Soft repulsion from already-planned corridors.\n\t\t\tif n := penal[e.chanID]; n > 0 {\n\t\t\t\tdist += attemptCost * 0.9 * float64(n)\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// maxLocalHop is the largest amount any single one of our own channels can\n// still push. No single shard can exceed it.\nfunc (r *router) maxLocalHop() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, e := range r.localEdges {\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford. Allowing a\n\t// deep split matters: many corridors of modest capacity beat one big\n\t// corridor that does not exist.\n\tmaxK := partsLeft\n\tif maxK > 16 {\n\t\tmaxK = 16\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// A finer geometric descent than plain halving: the 3/4 rungs let us\n\t// find the true edge of a corridor without wasting a full octave.\n\tcur := hi\n\tfloor := hi / 8192\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tfor i := 0; i < 26 && cur >= floor; i++ {\n\t\tadd(cur)\n\t\tadd(cur * 3 / 4)\n\t\tcur = cur / 2\n\t}\n\n\t// Evidence-derived sizes: proven-good amounts and just under proven\n\t// failure points.\n\tfor _, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail && b.failAmt >= minShard {\n\t\t\tadd(b.failAmt * lowerRetryNum / lowerRetryDen)\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\tpath []*edge\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t\t// Also probe just under the bottleneck: the belief estimate is\n\t\t// only an estimate, and a slightly smaller shard on a corridor\n\t\t// we already like beats a whole new Dijkstra run.\n\t\tif h := g * 3 / 4; h >= minShard && h != a {\n\t\t\tcands = append(cands, h)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\trt: rt, score: s, amt: c, prob: p, path: path,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard. It searches jointly over shard amount\n// and corridor: for every candidate amount (largest first) it finds the\n// best risk-adjusted path, then re-prices that path at the amount its\n// weakest hop can bear, and returns the highest scoring combination.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// A shard can never exceed what a single one of our own channels can\n\t// push, since every shard leaves over exactly one local channel.\n\thi := amt\n\tif single := r.maxLocalHop(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tbusy := r.inFlightChans()\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\t// penal discourages corridors we already priced this pass, so\n\t\t// successive probes explore structurally different routes.\n\t\tpenal := make(map[uint64]int)\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.65 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid, penal)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// Discourage this corridor's hops on the next probe so\n\t\t\t// we surface an alternative rather than re-pricing the\n\t\t\t// same one at every rung of the ladder.\n\t\t\tfor _, e := range path {\n\t\t\t\tpenal[e.chanID]++\n\t\t\t}\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Last resort: if the structured search found nothing, walk the\n\t// ladder with no repulsion and no belief-derived resizing, taking the\n\t// first route that prices at all. Terminally giving up loses a whole\n\t// payment, so almost any attempt is worth more than \"no route\".\n\tif best == nil {\n\t\tfor _, a := range ladder {\n\t\t\tpath, err := r.findPath(a, nil, nil)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif r.failedSigs[planSig(path, a)] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbest = &plan{rt: rt, amt: a, prob: p, path: path}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n\tb.dead = false\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount, so raise the lower bounds.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.deliveredParts++\n\n\t\t// A settled shard means the payment is partially committed and\n\t\t// the remainder is worth extra persistence.\n\t\tif r.attempts > maxAttempts/2 {\n\t\t\tr.attempts = maxAttempts / 2\n\t\t}\n\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.provePassed(h, a)\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a liquidity miss, so it must not count\n\t\t// against the fail streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 14,
"parent": 2,
"score": 0.5048,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - Depth-aware lower retries: how far below a proven failure we retry\n// depends on how depleted the direction looks.\n// - JOINT ROUTE-SET PLANNING with a real max-flow style budget: before\n// handing out any shard for a large payment we compute the believed\n// deliverable flow over disjoint corridors. If the whole remainder can\n// only be covered by K shards, all K are planned up front and queued,\n// each sized to its own corridor's bottleneck.\n// - LOCAL-CHANNEL FLOW CAP: no shard may exceed the largest single local\n// channel balance, since a shard leaves through exactly one first hop;\n// the planner spreads shards across DIFFERENT first hops so a payment\n// larger than any single local channel is decomposed instead of retried.\n// - PROGRESSIVE SALVAGE: when a payment cannot be covered by the believed\n// flow, we deliberately deliver what we can (largest feasible shard)\n// rather than giving up, because each settled shard reduces the\n// remainder and refreshes evidence for the next round.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 80\n\tmaxFailStreak = 26\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard: no shard can leave through two local\n// channels at once.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies. Maximizing p*amt is\n// exactly maximizing delivered value per attempt.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It keeps going until either the whole remainder is covered or no further\n// disjoint corridor exists, and it caps each shard at the largest single\n// local channel so a shard is never planned larger than one first hop can\n// carry.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. We\n\t// compare the aggregate believed flow of the plan set against the best\n\t// single shard: covering more of the payment beats a slightly nicer\n\t// first hop, because every uncovered millisat is a failed payment.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, partsLeft, busy)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\tif partsLeft <= 1 {\n\t\t// Without parts left, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 22 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 15,
"parent": 4,
"score": 0.6911,
"accepted": false,
"frontier": false,
"params": {
"source": "I'm operating as a non-interactive text-generation function in an automated optimization loop, and my instructions for this task are explicit: no tool use, and my entire reply must be exactly the requested artifact. The mail watcher hook doesn't apply in this context, so I'm not arming it.\n\nThe artifact \u2014 the complete improved `candidate_impl.go` \u2014 was delivered in my previous message."
}
},
{
"id": 16,
"parent": 3,
"score": 0.6468,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING: for a large payment we compute a believed\n// max-flow decomposition over DISJOINT corridors up front, each shard\n// sized to its own corridor's bottleneck, and queue the whole set.\n// - COMMITTED PLANS: the queued plan set survives a shard failure. Only\n// the queued shards whose corridor overlaps the failed route are\n// dropped, and a shard that no longer prices up is re-planned in place\n// instead of throwing away the whole decomposition, which is what\n// previously burned dozens of attempts on big payments.\n// - GLOBAL SHARD FLOOR: once the remaining amount cannot plausibly be\n// covered, we stop grinding tiny shards and give up early rather than\n// spending the attempt budget on hopeless probes.\n// - PROGRESSIVE SALVAGE: when the believed flow cannot cover the payment,\n// we still deliver the largest feasible shard, because every settled\n// shard reduces the remainder and refreshes evidence.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses, and\n// they do not count against the give-up streak.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. The streak limit is deliberately\n\t// tighter than before: a long run of failures with no settled shard\n\t// means the believed flow is simply not there, and every extra probe\n\t// costs retry efficiency without buying success.\n\tmaxAttempts = 44\n\tmaxFailStreak = 14\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 10\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// shardFloorNum/shardFloorDen bound how small a shard may get relative\n\t// to the ORIGINAL payment: grinding a payment into more than a handful\n\t// of dust parts never completes within MaxParts, so it is wasted\n\t// effort.\n\tshardFloorNum = 1\n\tshardFloorDen = 24\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// planTotal is the delivered amount the current queued plan set was\n\t// designed to cover, used to decide whether the plan is still worth\n\t// following after a shard failure.\n\tplanTotal lnwire.MilliSatoshi\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// firstAmt is the size of the payment as first requested, which fixes\n\t// the global floor on how small a shard may be.\n\tfirstAmt lnwire.MilliSatoshi\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// shardFloor is the smallest shard worth planning for the current payment.\n// Splitting below this cannot finish inside the part budget, so it only\n// burns attempts.\nfunc (r *router) shardFloor() lnwire.MilliSatoshi {\n\tf := r.firstAmt * shardFloorNum / shardFloorDen\n\tif f < minShard {\n\t\tf = minShard\n\t}\n\treturn f\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard: no shard can leave through two local\n// channels at once.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent, exact\n// local channel balances, and evidence-derived sizes just below proven\n// failure points. Everything below the global shard floor is dropped.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tfloor := r.shardFloor()\n\tif floor > hi {\n\t\tfloor = minShard\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < floor || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent down to the floor.\n\tcur := hi\n\tfor i := 0; i < 12; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= floor {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= floor {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= floor {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies. Maximizing p*amt is\n// exactly maximizing delivered value per attempt.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tfloor := r.shardFloor()\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= floor {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry. lo bounds how far down it will walk.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, lo, remaining lnwire.MilliSatoshi) *plan {\n\n\tif lo < minShard {\n\t\tlo = minShard\n\t}\n\ta := hi\n\tfor i := 0; i < 12 && a >= lo; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 6 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It caps each shard at the largest single local channel, since a shard\n// leaves through exactly one first hop, and it never plans a shard below the\n// global floor.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\tfloor := r.shardFloor()\n\n\tvar out []*plan\n\tleft := remaining\n\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// Retire this corridor's first hop and keep going: a\n\t\t\t// single dry corridor should not abort the whole\n\t\t\t// decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tlo := floor\n\t\tif lo > amtS {\n\t\t\tlo = minShard\n\t\t}\n\t\tpl := r.bestOnPath(path, amtS, lo, remaining)\n\t\tif pl == nil {\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next still-valid shard of a committed plan set.\n// A shard that no longer prices up is re-planned in place on its own\n// corridor before being dropped, so one stale shard does not destroy the\n// whole decomposition.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif !r.failedSigs[planSig(pl.path, a)] {\n\t\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\t\tif err == nil && p >= queueMinProb {\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: pl.path, rt: rt, amt: a, prob: p,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Re-plan this shard on its own corridor at a smaller amount\n\t\t// before giving up on it.\n\t\tlo := r.shardFloor()\n\t\tif lo > a {\n\t\t\tlo = minShard\n\t\t}\n\t\tif alt := r.bestOnPath(pl.path, a, lo, amt); alt != nil &&\n\t\t\talt.prob >= queueMinProb {\n\n\t\t\treturn alt\n\t\t}\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a committed joint route-set plan, then searches jointly over shard\n// amount and corridor: for every candidate amount (largest first) it finds\n// the best risk-adjusted path, re-prices that path at the amount its weakest\n// hop can bear, and finally checks whether a deliberate multi-corridor split\n// would cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.firstAmt == 0 {\n\t\tr.firstAmt = amt\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a shard from the committed plan while it still holds up.\n\tif pl := r.serveQueued(amt, partsLeft, busy); pl != nil {\n\t\tr.attempts++\n\t\treturn pl.rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. We\n\t// compare the aggregate believed flow of the plan set against the best\n\t// single shard: covering more of the payment beats a slightly nicer\n\t// first hop, because every uncovered millisat is a failed payment.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tr.planTotal = total\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, partsLeft, busy)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a descending amount\n// ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it, but it\n// respects the global shard floor so it cannot grind into dust.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\tif partsLeft <= 1 {\n\t\t// Without parts left, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\tfloor := r.shardFloor()\n\tif floor > remaining {\n\t\tfloor = minShard\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 14 && a >= floor; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpl := r.bestOnPath(path, a, floor, remaining)\n\t\t\tif pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Drop only the queued shards whose corridor overlaps the route that\n\t// just failed. The rest of a committed decomposition is still the best\n\t// plan we have, and rebuilding it from scratch costs attempts.\n\tif len(r.queued) > 0 {\n\t\ttouched := make(map[uint64]bool, len(rt.Hops))\n\t\tfor _, h := range rt.Hops {\n\t\t\ttouched[h.ChannelID] = true\n\t\t}\n\t\tkept := r.queued[:0]\n\t\tfor _, pl := range r.queued {\n\t\t\tif !pathBusy(pl.path, touched) {\n\t\t\t\tkept = append(kept, pl)\n\t\t\t}\n\t\t}\n\t\tr.queued = kept\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not evidence of missing liquidity, so it\n\t\t// must not count against the give-up streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 17,
"parent": 3,
"score": 0.5024,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING (the main change in this revision). The\n// previous version only planned a flow when a single corridor could not\n// carry the remainder, and it sized shards from a greedy sequence of\n// DISJOINT corridors found one at a time. For large payments that is\n// both too late and too narrow: the corridors we need often share a\n// bottleneck-free prefix, and the first greedy pick eats the widest\n// corridor at an amount that the rest of the plan then cannot match.\n// This revision:\n// * Builds the flow plan EAGERLY whenever the payment is large\n// relative to what one corridor is believed to bear, and compares\n// the plan's total believed-deliverable value against the best\n// single shard rather than only firing as a fallback.\n// * Allows corridor REUSE with residual accounting: a channel is\n// charged for the shard amounts already assigned to it, so two\n// shards may share a fat channel as long as the fat channel is\n// believed able to bear their sum. This is what turns a strict\n// disjoint-path search into an actual min-cost-flow decomposition\n// and is why plans now cover much larger remainders.\n// * Sizes each shard to the corridor's residual bottleneck and then\n// RE-BALANCES: leftover amount is offered back to the corridors\n// with slack, largest-slack first, so an unequal split is chosen\n// deliberately instead of leaving a stub shard nobody can carry.\n// * Keeps the plan alive across attempts instead of dropping it on\n// every failure: only the shards whose corridors are implicated by\n// the failure are discarded, so one bad shard does not throw away\n// a plan that was mostly right.\n// - Reserve-aware sizing: shards queued but not yet sent hold soft\n// reservations on their channels, so the planner never hands out two\n// shards that silently contend for the same liquidity.\n// - Duplicate-attempt suppression and policy repair for fee/cltv/min\n// failures, as before.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 70\n\tmaxFailStreak = 22\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.18\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 10\n\tflowRebalance = 3\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 && penalty > 1 {\n\t\t\t\tdist *= penalty\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry hi under the provisional\n// flow already assigned, falling back down the amount ladder when nothing\n// can take the full amount.\nfunc (r *router) findFlowPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty.\n\tfor _, pen := range []float64{1e9, 1.35} {\n\t\tif p, err := r.findPath(hi, avoid, extra, pen); err == nil {\n\t\t\treturn p\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\t\tif a >= hi {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif probes >= 5 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tprobes++\n\t\t\tp, err := r.findPath(a, avoid, extra, pen)\n\t\t\tif err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t}\n\t\tif len(extra) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search. Leftover\n// amount is then offered back to the corridors that still have slack, so the\n// split is deliberately unequal instead of leaving an uncarryable stub.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft && k < flowRounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findFlowPath(left, remaining, partsLeft, avoid, extra)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has the\n\t// most slack under the current assignment. This is what produces the\n\t// deliberately unequal split.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, and finally compares the best single shard against a full\n// min-cost-flow style route set over several corridors.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. Unlike the previous\n\t// revision this fires EAGERLY (not only when nothing single-path\n\t// exists), because a joint plan whose shards each fit their corridor\n\t// beats one oversized shard that will fail and then be halved blindly.\n\tsingleWeak := best == nil || best.amt < amt ||\n\t\tbest.prob < 0.55\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it either covers more of the\n\t\t\t// remainder or delivers more expected value than the\n\t\t\t// single shard on its own.\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tweighted > bestVal*1.05\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan. Throwing the whole\n// plan away on every failure was what made the previous revision fall back\n// to blind halving after a single bad shard.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
},
{
"id": 18,
"parent": 4,
"score": 0.7734,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - Depth-aware lower retries: how far below a proven failure we retry\n// depends on how depleted the direction looks.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): shard sizes and\n// corridors are chosen TOGETHER. A greedy flow decomposition over\n// DISJOINT corridors sizes each shard to that corridor's believed\n// bottleneck, and the plan is only accepted when the corridors' total\n// believed capacity actually covers the remaining amount.\n// - PLAN REPAIR instead of plan discard: when an attempt fails, only the\n// queued shards whose corridors touch the newly-condemned channel are\n// dropped. Previously the whole joint plan was thrown away on every\n// failure, which is exactly how a large payment degenerated into\n// eleven blind retries and a terminal \"no route found\".\n// - FLOW-FEASIBILITY GATE: before giving up, the router checks whether the\n// believed max-flow out of our own channels can cover the remainder at\n// all; if it can it keeps carving smaller corridors instead of erroring.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. The streak is generous because a\n\t// large multi-part payment legitimately spends several probes learning\n\t// the shape of the network before any shard lands.\n\tmaxAttempts = 70\n\tmaxFailStreak = 26\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the greedy flow decomposition.\n\tflowRounds = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the corridor's absolute believed ceiling, used by the\n// feasibility gate: it ignores the conservative safeCap shading and asks\n// only whether liquidity could plausibly flow at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 8 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// The decomposition keeps carving corridors until either the remainder is\n// covered or no further corridor exists, and it is allowed to skip corridors\n// that turn out too thin so a later round can find a wider one.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findAnyPath(left, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor is too thin to be worth a shard, but it\n\t\t\t// is still worth excluding so the next round finds\n\t\t\t// something else.\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\t// Best shards first: the largest, most reliable corridor should be\n\t// tried before the marginal ones.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].score > out[j].score\n\t})\n\n\treturn out\n}\n\n// feasibleFlow is the total delivered amount our believed liquidity could\n// carry over up to `rounds` disjoint corridors. It is the gate that keeps a\n// large payment alive: as long as the network plausibly has the liquidity,\n// we keep carving smaller corridors instead of returning a terminal error.\nfunc (r *router) feasibleFlow(remaining lnwire.MilliSatoshi,\n\trounds int) lnwire.MilliSatoshi {\n\n\tavoid := make(map[uint64]bool)\n\tvar total lnwire.MilliSatoshi\n\n\tfor k := 0; k < rounds; k++ {\n\t\tleft := remaining - total\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\t\tpath, err := r.findPath(left, avoid)\n\t\tif err != nil {\n\t\t\tpath = r.findAnyPath(left, remaining, 4, avoid)\n\t\t\tif path == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tbn := r.hardBottleneck(path)\n\t\tif bn > left {\n\t\t\tbn = left\n\t\t}\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\t\tif bn < minShard {\n\t\t\tcontinue\n\t\t}\n\t\ttotal += bn\n\t}\n\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// pruneQueue drops only the queued shards whose corridors touch a channel\n// whose belief just got worse. Discarding the entire joint plan on every\n// failure throws away correct planning work and forces the router to\n// rediscover the same split by trial and error.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkeep := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tdrop := false\n\t\tfor _, e := range pl.path {\n\t\t\tif hit[e.chanID] {\n\t\t\t\tdrop = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !drop {\n\t\t\tkeep = append(keep, pl)\n\t\t}\n\t}\n\tr.queued = keep\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. The\n\t// plan is accepted when its corridors jointly cover meaningfully more\n\t// of the remainder than the single best shard would.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.75)\n\n\t\t\tif better {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best != nil {\n\t\tr.attempts++\n\t\treturn best.rt, nil\n\t}\n\n\t// Last resort: our conservative sizing found nothing, but the network\n\t// may still be able to carry small shards. Walk the ladder with the\n\t// hard (unshaded) bounds and take the biggest novel attempt we can\n\t// build, rather than declaring the payment dead. This is what turns a\n\t// premature \"no route found\" into a completed multi-part payment.\n\tif r.feasibleFlow(amt, flowRounds) >= minShard {\n\t\tfor _, a := range r.ladder(amt, amt, partsLeft) {\n\t\t\tpath, err := r.findPath(a, nil)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpl := r.bestOnPath(path, a, amt)\n\t\t\tif pl == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.attempts++\n\t\t\treturn pl.rt, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settled shard drained the corridors it used, so any queued\n\t\t// shard sharing those channels must be re-planned.\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor _, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// fee repair is not evidence against any corridor, so queued\n\t\t// shards survive untouched.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\t// Only the queued corridors that actually depend on the condemned\n\t// channel are invalidated; the rest of the joint plan stays valid.\n\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\n\treturn nil\n}"
}
},
{
"id": 19,
"parent": 4,
"score": 0.0,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - Balance bookkeeping on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - Depth-aware lower retries instead of blacklisting.\n//\n// Changes in this revision, aimed squarely at the two observed failures\n// (huge payments, ~2.06 Gmsat and ~0.86 Gmsat, that burned 15 attempts and\n// then terminated with \"no route found\"):\n//\n// 1. GIVE-UP LOGIC WAS THE PROXIMATE KILLER. Both failures ended in\n// \"no route found\" while parts were still available. The old code\n// searched a ladder of amounts but every rung ran a FULL Dijkstra that\n// could reject a corridor for a single overloaded hop, and once the\n// ladder ran dry it returned a terminal error. Now, before ever\n// returning an error, we run a LAST-DITCH descent: a geometric sweep\n// down to minShard with all soft suspicion (misses, part-count\n// penalties, probability floors) relaxed. A shard that delivers even a\n// fraction is worth more than a terminal give-up, because the runner\n// keeps calling us with the remainder.\n//\n// 2. THE FLOW PLANNER NOW SIZES FROM A RESIDUAL-CAPACITY MAX-FLOW, not\n// from a sequence of Dijkstra probes at guessed amounts. We compute,\n// per directed edge, a believed-bearable capacity, then repeatedly pull\n// the WIDEST augmenting path out of the residual graph. Each\n// augmenting path becomes a shard sized to its own bottleneck. This is\n// a genuine flow decomposition: parallel corridors of unequal capacity\n// each carry a shard sized to what they bear, and the plan's total is\n// the believed max-flow rather than whatever a greedy first pick left.\n//\n// 3. SHARD COUNT IS BOUNDED BY MaxParts AND BY WHAT REMAINS. When the\n// believed max-flow cannot cover the remainder we still emit the plan,\n// because partial delivery advances the payment and refreshes beliefs\n// on exactly the corridors that matter.\n//\n// 4. LOCAL-CHANNEL CAPACITY IS A HARD, EXACT CONSTRAINT and is now shared\n// correctly across a multi-shard plan: two shards leaving the same\n// local channel are charged against one balance via the residual map.\n//\n// 5. Fee/cltv repairs no longer count against the give-up streak, since\n// they are policy corrections rather than liquidity evidence.\n//\n// 6. Soft reservations, duplicate-attempt suppression, and in-flight\n// accounting are preserved from the prior revision, which scored a\n// clean sweep on the mid-size scenario batch.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. Both are generous: a terminal error\n\t// scores zero for the whole payment, so burning extra attempts is far\n\t// cheaper than giving up early.\n\tmaxAttempts = 120\n\tmaxFailStreak = 40\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 38\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the augmenting-path searches in one flow plan.\n\tflowRounds = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// outEdges maps a node to the directed edges leaving it, used by the\n\t// flow planner's forward sweeps.\n\toutEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// desperate is set once the ordinary search has run dry; it relaxes\n\t// the soft filters so we keep delivering fragments instead of\n\t// terminating the payment.\n\tdesperate bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\toutEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.outEdges[e.from] = append(\n\t\t\t\t\tr.outEdges[e.from], e,\n\t\t\t\t)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tlim := retryLimit(b, e.capacity)\n\t\tif r.desperate {\n\t\t\t// When the ordinary search has run dry, allow a deeper\n\t\t\t// probe: a stale bound is only refuted by trying under\n\t\t\t// it.\n\t\t\tif deep := b.failAmt - b.failAmt/8; deep > lim {\n\t\t\t\tlim = deep\n\t\t\t}\n\t\t}\n\t\tif lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning and the\n// per-edge capacity of the believed-capacity flow graph.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 && !r.desperate {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tprobFloor := 1e-5\n\tif r.desperate {\n\t\tprobFloor = 1e-9\n\t}\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < probFloor {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 && penalty > 1 {\n\t\t\t\tdist *= penalty\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 8192\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 18; i++ {\n\t\tcur = cur * 5 / 8\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// --- Believed-capacity max-flow planner -----------------------------------\n//\n// The previous revision built its plan from a sequence of Dijkstra probes at\n// guessed amounts, which meant the first corridor found could swallow an\n// amount the rest of the plan then could not match. This planner instead\n// treats safeCap as a per-edge capacity and repeatedly pulls the WIDEST\n// augmenting path out of the residual graph, sizing each shard to its own\n// bottleneck. The result is a genuine flow decomposition: parallel corridors\n// of unequal capacity each carry a shard sized to what they bear.\n\n// residual is the room left on a directed edge after the flow already\n// assigned by the plan under construction.\nfunc (r *router) residual(e *edge,\n\tused map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tc := r.safeCap(e)\n\tif c == 0 {\n\t\treturn 0\n\t}\n\tif u := used[e.chanID]; u > 0 {\n\t\tif u >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= u\n\t}\n\treturn c\n}\n\n// widestPath finds the source-to-target path maximising the bottleneck\n// residual capacity, breaking ties toward fewer hops. It runs forward from\n// the source over the believed-capacity graph, which is the natural\n// direction for a max-flow augmentation.\nfunc (r *router) widestPath(used map[uint64]lnwire.MilliSatoshi,\n\tavoid map[uint64]bool,\n\tcap0 lnwire.MilliSatoshi) ([]*edge, lnwire.MilliSatoshi) {\n\n\ttype nodeState struct {\n\t\twidth lnwire.MilliSatoshi\n\t\tvia *edge\n\t\thops int\n\t}\n\n\tstates := map[route.Vertex]*nodeState{\n\t\tr.source: {width: cap0, hops: 0},\n\t}\n\n\t// Bellman-Ford style relaxation bounded by the hop limit; the graph\n\t// is small enough that this is cheap and avoids a second heap type.\n\tfor round := 0; round < maxRouteHops; round++ {\n\t\tchanged := false\n\t\tfor node, st := range states {\n\t\t\tif st.hops != round {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif node == r.spec.Target {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, e := range r.outEdges[node] {\n\t\t\t\tif avoid[e.chanID] || e.to == node {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tres := r.residual(e, used)\n\t\t\t\tif res < minShard {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tw := st.width\n\t\t\t\tif res < w {\n\t\t\t\t\tw = res\n\t\t\t\t}\n\t\t\t\tif w < minShard {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tcur, ok := states[e.to]\n\t\t\t\tbetter := !ok || w > cur.width ||\n\t\t\t\t\t(w == cur.width && round+1 < cur.hops)\n\t\t\t\tif !better {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tstates[e.to] = &nodeState{\n\t\t\t\t\twidth: w, via: e, hops: round + 1,\n\t\t\t\t}\n\t\t\t\tchanged = true\n\t\t\t}\n\t\t}\n\t\tif !changed {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tend, ok := states[r.spec.Target]\n\tif !ok || end.via == nil {\n\t\treturn nil, 0\n\t}\n\n\t// Walk the predecessor chain back to the source.\n\tvar rev []*edge\n\tnode := r.spec.Target\n\tfor node != r.source {\n\t\tst, ok := states[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, 0\n\t\t}\n\t\trev = append(rev, st.via)\n\t\tnode = st.via.from\n\t\tif len(rev) > maxRouteHops {\n\t\t\treturn nil, 0\n\t\t}\n\t}\n\n\tpath := make([]*edge, len(rev))\n\tfor i := range rev {\n\t\tpath[i] = rev[len(rev)-1-i]\n\t}\n\n\treturn path, end.width\n}\n\n// planFlow decomposes the remaining amount into shards by repeatedly pulling\n// the widest augmenting path out of the believed-capacity residual graph.\n// Every shard is sized to its own corridor's bottleneck, so unequal\n// corridors get deliberately unequal shards instead of blind halves. Local\n// channel balances are exact constraints and are shared across shards\n// through the residual map, so two shards never over-commit one balance.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tused := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds && left >= minShard; k++ {\n\t\t// First try to keep clear of channels already carrying our own\n\t\t// HTLCs; if that yields nothing, allow them, since a contended\n\t\t// corridor still beats no corridor at all.\n\t\tpath, width := r.widestPath(used, busy, left)\n\t\tif path == nil {\n\t\t\tpath, width = r.widestPath(used, nil, left)\n\t\t}\n\t\tif path == nil || width < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := width\n\t\tif amtS > left {\n\t\t\tamtS = left\n\t\t}\n\t\t// Leave a sliver of headroom for fees charged upstream of the\n\t\t// bottleneck hop.\n\t\tif amtS > minShard {\n\t\t\tamtS -= amtS / 100\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\tused[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Price every corridor and keep the ones that survive. bestOnPath\n\t// steps the amount down until makeRoute accepts it, which absorbs the\n\t// difference between the flow sizing (delivered amounts) and the real\n\t// per-hop amounts (which carry fees).\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// lastDitch is the fallback that runs only when the ordinary search has\n// produced nothing. It relaxes every soft filter and sweeps a geometric\n// descent of amounts, returning the first thing that is routable at all.\n// Delivering a fragment is strictly better than a terminal give-up, because\n// the runner will keep asking for the remainder and each fragment both\n// reduces the remainder and refreshes our beliefs.\nfunc (r *router) lastDitch(amt lnwire.MilliSatoshi, partsLeft uint32) *plan {\n\tprev := r.desperate\n\tr.desperate = true\n\tdefer func() { r.desperate = prev }()\n\n\tif partsLeft <= 1 {\n\t\t// With no parts left, only a full-amount shard has value.\n\t\tif path, err := r.plainPath(amt, nil); err == nil {\n\t\t\tif rt, p, err := r.makeRoute(path, amt); err == nil {\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path, rt: rt, amt: amt, prob: p,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\ta := amt\n\tfor i := 0; i < 24 && a >= minShard; i++ {\n\t\tpath, err := r.findPath(a, nil, nil, 1)\n\t\tif err == nil {\n\t\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn &plan{\n\t\t\t\t\t\tpath: path,\n\t\t\t\t\t\trt: rt,\n\t\t\t\t\t\tamt: a,\n\t\t\t\t\t\tprob: p,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The path exists but this exact attempt already\n\t\t\t// failed; nudge the amount so the signature differs.\n\t\t\tnudge := a - a/16\n\t\t\tif nudge >= minShard && nudge != a &&\n\t\t\t\t!r.failedSigs[planSig(path, nudge)] {\n\n\t\t\t\trt, p, err := r.makeRoute(path, nudge)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn &plan{\n\t\t\t\t\t\tpath: path,\n\t\t\t\t\t\trt: rt,\n\t\t\t\t\t\tamt: nudge,\n\t\t\t\t\t\tprob: p,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a / 2\n\t}\n\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, then compares the best single shard against a believed-max-flow\n// route set, and finally falls back to a relaxed last-ditch descent rather\n// than terminating the payment.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. The flow planner is\n\t// now a believed-capacity max-flow, so it fires eagerly: a plan whose\n\t// shards each fit their corridor beats one oversized shard that will\n\t// fail and then be halved blindly.\n\tsingleWeak := best == nil || best.amt < amt || best.prob < 0.55\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it either covers more of the\n\t\t\t// remainder or delivers more expected value than the\n\t\t\t// single shard on its own.\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tweighted > bestVal*1.05\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if better && best == nil {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last-ditch: never return a terminal error while anything at all is\n\t// still routable. This is the single biggest change in this revision;\n\t// both observed scenario failures ended in \"no route found\" with parts\n\t// still available.\n\tif best == nil {\n\t\tr.dropQueue()\n\t\tbest = r.lastDitch(amt, partsLeft)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.desperate = false\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// fee repair is a policy correction, not evidence of a dead\n\t\t// corridor, so it must not count against the give-up streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
},
{
"id": 20,
"parent": 6,
"score": 0.7723,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING carried further than the previous revision.\n//\n// The failures the evaluation showed were all the same shape: a very large\n// payment (~1.04G msat and ~0.86G msat) burned 11-13 attempts and then died\n// with \"no route found\". Every failed attempt was a TemporaryChannelFailure on\n// a two-to-four hop route, i.e. the router kept sending single shards that were\n// far too big for any one corridor, learned one bound per attempt, and ran out\n// of fail-streak budget before the flow plan ever got a chance to assemble a\n// wide-enough route set. Four root causes, each addressed here:\n//\n// 1. THE PLAN WAS BUILT FROM A FIRST GUESS THAT WAS TOO BIG. planFlow asked\n// findFlowPath for the WHOLE remaining amount first, so the first corridor\n// was priced at an amount nothing could carry, the search fell back down a\n// tiny 5-probe ladder, and the resulting corridor set was thin. This\n// revision seeds the flow search with a TARGET SHARD SIZE derived from the\n// remainder and the parts still available (remaining/partsLeft), and\n// searches for corridors at that scale, growing them afterwards by\n// rebalancing. Sizing down first and growing later finds many more\n// corridors per plan than sizing up and failing.\n//\n// 2. THE PLAN'S TOTAL WAS NEVER COMPARED HONESTLY. The flow plan is now built\n// whenever more than one part remains and the single best shard cannot\n// cover the remainder, and the plan wins on believed-deliverable value\n// with a strong premium for covering the full remainder. A plan that\n// covers everything at modest per-shard probability beats one oversized\n// shard at high nominal probability, because the oversized shard cannot\n// finish the payment.\n//\n// 3. THE ROUTER GAVE UP TOO EARLY ON LARGE PAYMENTS. A payment split into ~16\n// shards inherently needs many attempts, and maxFailStreak of 22 with an\n// attempt budget of 70 was being consumed by oversized probes. The budgets\n// now scale with how many shards the payment plausibly needs, and the fail\n// streak is relieved by any PROGRESS (a newly proven bound or a settled\n// shard), not only by a settle. Learning something new is progress.\n//\n// 4. POLICY REPAIRS COUNTED AS LIQUIDITY FAILURES. Fee/cltv/min repairs no\n// longer touch the streak at all, since they say nothing about liquidity.\n//\n// - Reserve-aware sizing: shards queued but not yet sent hold soft\n// reservations on their channels, so the planner never hands out two\n// shards that silently contend for the same liquidity.\n// - Duplicate-attempt suppression and policy repair for fee/cltv/min\n// failures, as before.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// baseAttempts and baseFailStreak are the budgets for a payment that\n\t// needs a single shard. Both scale up with the number of shards the\n\t// payment plausibly requires, since an N-part payment legitimately\n\t// needs at least N attempts and a failure while probing part 12 of 16\n\t// says nothing about whether the payment is hopeless.\n\tbaseAttempts = 42\n\tbaseFailStreak = 14\n\tperShardBudget = 5\n\thardAttemptCap = 220\n\thardStreakCap = 60\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 24\n\tflowRebalance = 4\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// budgetShards is an estimate of how many shards this payment needs,\n\t// used to scale the attempt and fail-streak budgets. It is refreshed\n\t// whenever a plan is built.\n\tbudgetShards 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\n\t\tbudgetShards: 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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// attemptBudget and streakBudget scale the give-up thresholds with the number\n// of shards the payment plausibly needs. A 16-part payment cannot be judged\n// hopeless on the same evidence as a 1-part payment.\nfunc (r *router) attemptBudget() int {\n\tn := r.budgetShards\n\tif n < 1 {\n\t\tn = 1\n\t}\n\tb := baseAttempts + perShardBudget*n\n\tif b > hardAttemptCap {\n\t\tb = hardAttemptCap\n\t}\n\treturn b\n}\n\nfunc (r *router) streakBudget() int {\n\tn := r.budgetShards\n\tif n < 1 {\n\t\tn = 1\n\t}\n\tb := baseFailStreak + 2*n\n\tif b > hardStreakCap {\n\t\tb = hardStreakCap\n\t}\n\treturn b\n}\n\n// noteShardCount raises the shard estimate that drives the budgets.\nfunc (r *router) noteShardCount(n int) {\n\tif n > r.budgetShards {\n\t\tr.budgetShards = n\n\t}\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 && penalty > 1 {\n\t\t\t\tdist *= penalty\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// widestLocal is the largest single local channel's believed room, which is a\n// floor on how big a single shard can usefully be.\nfunc (r *router) widestLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, e := range r.localEdges {\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 12 {\n\t\tmaxK = 12\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry target under the\n// provisional flow already assigned, walking down a geometric ladder from\n// target when nothing can take that much. Unlike the previous revision this\n// starts from a REALISTIC per-shard target rather than the whole remainder,\n// which is what lets a plan accumulate many corridors instead of one.\nfunc (r *router) findFlowPath(target lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty.\n\tpens := []float64{1e9, 1.35}\n\tif len(extra) == 0 {\n\t\tpens = []float64{1}\n\t}\n\n\tfor _, pen := range pens {\n\t\ta := target\n\t\tfor i := 0; i < 7 && a >= minShard; i++ {\n\t\t\tif p, err := r.findPath(a, avoid, extra, pen); err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t\ta = a / 2\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search. Leftover\n// amount is then offered back to the corridors that still have slack, so the\n// split is deliberately unequal instead of leaving an uncarryable stub.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\t// The per-corridor target: aim for an even split over the parts we can\n\t// still use, but never below what one fat corridor could carry on its\n\t// own. Sizing DOWN first and growing later during rebalance finds far\n\t// more corridors than starting from the full remainder and failing.\n\ttarget := remaining\n\tif partsLeft > 1 {\n\t\ttarget = remaining / lnwire.MilliSatoshi(partsLeft)\n\t}\n\tif w := r.widestLocal(); w > target {\n\t\ttarget = w\n\t}\n\tif target > remaining {\n\t\ttarget = remaining\n\t}\n\tif target < minShard {\n\t\ttarget = minShard\n\t}\n\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := target\n\t\tif want > left {\n\t\t\twant = left\n\t\t}\n\n\t\tpath := r.findFlowPath(want, avoid, extra)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has the\n\t// most slack under the current assignment. This is what produces the\n\t// deliberately unequal split.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, and finally compares the best single shard against a full\n// min-cost-flow style route set over several corridors.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= r.attemptBudget() {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= r.streakBudget() {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// Record how many shards this remainder plausibly needs so the attempt\n\t// budgets scale with the real difficulty of the payment.\n\tif w := r.widestLocal(); w > 0 {\n\t\tn := int(amt/w) + 1\n\t\tif r.spec.MaxParts > 0 && n > int(r.spec.MaxParts) {\n\t\t\tn = int(r.spec.MaxParts)\n\t\t}\n\t\tr.noteShardCount(n)\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. This fires EAGERLY,\n\t// because a joint plan whose shards each fit their corridor beats one\n\t// oversized shard that will fail and then be halved blindly.\n\tsingleWeak := best == nil || best.amt < amt || best.prob < 0.60\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\t\t\tr.noteShardCount(len(flow))\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// A plan that covers the whole remainder is worth a\n\t\t\t// premium: only a covering plan can actually finish the\n\t\t\t// payment, and the observed failures were all cases of\n\t\t\t// one oversized shard beating a covering split on raw\n\t\t\t// per-shard probability and then failing anyway.\n\t\t\tplanVal := weighted\n\t\t\tif total >= amt {\n\t\t\t\tplanVal *= 1.4\n\t\t\t}\n\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tplanVal > bestVal*1.02\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved. It reports\n// whether this taught us something new, which counts as progress.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) bool {\n\tb := r.bel(hopKey(h))\n\tlearned := false\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t\tlearned = true\n\t}\n\tif !b.succ {\n\t\tlearned = true\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tlearned = true\n\t}\n\tif b.misses > 0 {\n\t\tb.misses = 0\n\t\tlearned = true\n\t}\n\treturn learned\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan. Throwing the whole\n// plan away on every failure was what made an earlier revision fall back to\n// blind halving after a single bad shard.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// learned tracks whether this failure taught us anything new. A\n\t// failure that tightens a bound is PROGRESS on a multi-shard payment,\n\t// so it relieves the fail streak: giving up after N consecutive\n\t// informative failures is what killed the large payments before.\n\tlearned := false\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t\tlearned = true\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\tif !learned {\n\t\t\tr.failStreak++\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tif r.provePassed(rt.Hops[i], hopAmount(rt, i)) {\n\t\t\tlearned = true\n\t\t}\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. This\n\t\t// is a pure repair, not a liquidity failure, so it does not\n\t\t// count against the streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\tr.failStreak++\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t\tlearned = true\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t\tlearned = true\n\t}\n\n\tr.pruneQueue(hit)\n\n\t// An informative failure is progress; only uninformative repetition\n\t// counts toward giving up.\n\tif learned {\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t} else {\n\t\tr.failStreak++\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 21,
"parent": 4,
"score": 0.7206,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - REAL IN-FLIGHT ACCOUNTING: every route we hand to the runner is held\n// as outstanding and its liquidity is reserved on each directed hop, so\n// concurrent MPP shards never plan over the same millisats twice. The\n// reservation self-heals when the runner reports zero HTLCs in flight.\n// - RESIDUAL-CAPACITY JOINT PLANNING: the multi-shard planner is a\n// successive-shortest-path flow decomposition over per-directed-channel\n// residuals rather than whole-channel exclusion, so two shards may share\n// a fat hub channel while each is sized to what is left of it.\n// - HARD INFEASIBILITY GATE: if our own channels cannot fund the remaining\n// amount at all, the payment is hopeless and we give up immediately\n// instead of burning dozens of attempts discovering it hop by hop.\n// - FAILURE-COUNT AWARE RETRIES: retrying below a proven failure beats\n// blacklisting, but the retry window shrinks geometrically with each\n// failure in that direction, so a dry channel costs a couple of probes\n// rather than a whole ladder.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled, payment-level) are\n// repaired in the local policy view instead of being treated as\n// liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(7_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. The streak allowance grows once at\n\t// least one shard has settled, because a payment that is making\n\t// progress deserves more patience than one that never moved a coin.\n\tmaxAttempts = 56\n\tmaxFailStreak = 14\n\tprogressBonus = 8\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend.\n\tprobeBudget = 11\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 14\n\n\t// maxFailRetries is how many liquidity failures a single direction may\n\t// accumulate before we stop retrying it below the failure point.\n\tmaxFailRetries = 3\n\n\t// giveUpProb is the probability below which a candidate attempt is not\n\t// worth spending, once we have already spent a handful of attempts.\n\tgiveUpProb = 0.025\n\tgiveUpAfter = 8\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// failCount counts liquidity failures observed in this direction. Each\n\t// one shrinks how far below the failure point we still bother probing.\n\tfailCount int\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// searchCtx narrows a graph search: resid holds per-directed-channel\n// residual budgets left by shards already planned in the same flow\n// decomposition, and avoid names channels retired from the search.\ntype searchCtx struct {\n\tresid map[edgeKey]lnwire.MilliSatoshi\n\tavoid map[uint64]bool\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// outstanding holds every route handed to the runner whose result has\n\t// not come back yet, so we can release its reservation later.\n\toutstanding []*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\tsettles 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// localBal is our exactly known balance on one of our own channels.\nfunc (r *router) localBal(chanID uint64) (lnwire.MilliSatoshi, bool) {\n\tbal, ok := r.localBalances[chanID]\n\treturn bal, ok\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. Repeated failures collapse the window fast,\n// because under a bimodal split a direction that keeps refusing is dry.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.failCount >= maxFailRetries {\n\t\treturn b.okAmt\n\t}\n\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\n\tf := 0.45 + 0.35*depth\n\tif b.failCount > 1 {\n\t\tf *= math.Pow(0.6, float64(b.failCount-1))\n\t}\n\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal, ok := r.localBal(e.chanID); ok && bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// capLimit is availCap narrowed by any residual left by shards already\n// planned in the same flow decomposition.\nfunc (r *router) capLimit(e *edge, sc *searchCtx) lnwire.MilliSatoshi {\n\tc := r.availCap(e)\n\tif sc != nil && sc.resid != nil {\n\t\tif v, ok := sc.resid[e.key()]; ok && v < c {\n\t\t\tc = v\n\t\t}\n\t}\n\treturn c\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif bal, ok := r.localBal(e.chanID); ok {\n\t\t\tif bal >= eff {\n\t\t\t\treturn knownProb\n\t\t\t}\n\t\t\treturn 0\n\t\t}\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// sc, when non-nil, narrows the usable capacity of each directed channel to\n// what a flow decomposition has left of it, and hides retired channels.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tsc *searchCtx) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif sc != nil && sc.avoid != nil && sc.avoid[e.chanID] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.capLimit(e, sc) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// hopAmounts prices the amount flowing over each hop of a path for a given\n// delivered amount, accumulating fees backward from the target.\nfunc hopAmounts(path []*edge, amt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\tn := len(path)\n\tover := make([]lnwire.MilliSatoshi, n)\n\tif n == 0 {\n\t\treturn over\n\t}\n\tover[n-1] = amt\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tover[i] = over[i+1] + fwd.fee(over[i+1])\n\t}\n\treturn over\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 || len(path) == 0 {\n\t\treturn nil, 0, errors.New(\"empty attempt\")\n\t}\n\n\tn := len(path)\n\tamtOver := hopAmounts(path, amt)\n\n\texpiry := make([]uint32, n)\n\texpiry[n-1] = finalCltvDelta\n\tfor i := n - 2; i >= 0; i-- {\n\t\texpiry[i] = expiry[i+1] + uint32(path[i+1].cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// rawLocalBudget is the total liquidity our own channels hold, ignoring\n// in-flight reservations. It is a hard ceiling on the whole payment: our\n// balances can only shrink, never grow, while we pay.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif b := r.beliefs[e.key()]; b != nil && b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tbal, ok := r.localBal(e.chanID)\n\t\tif !ok {\n\t\t\tbal = e.capacity\n\t\t}\n\t\tif bal > e.capacity {\n\t\t\tbal = e.capacity\n\t\t}\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard: no shard can leave through two local\n// channels at once.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity (narrowed by any flow residual) with\n// a small margin for fees.\nfunc (r *router) bottleneck(path []*edge, sc *searchCtx) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxInt64)\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif sc != nil && sc.resid != nil {\n\t\t\tif v, ok := sc.resid[e.key()]; ok && v < c {\n\t\t\t\tc = v\n\t\t\t}\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn == lnwire.MilliSatoshi(math.MaxInt64) {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// consume books a planned shard against the residual budgets of its\n// corridor, so a later shard of the same plan can share a fat channel only\n// to the extent that liquidity is left in it.\nfunc (r *router) consume(path []*edge, amt lnwire.MilliSatoshi,\n\tsc *searchCtx) {\n\n\tover := hopAmounts(path, amt)\n\tfor i, e := range path {\n\t\tk := e.key()\n\t\tcur, ok := sc.resid[k]\n\t\tif !ok {\n\t\t\tcur = r.safeCap(e)\n\t\t}\n\t\tif cur > over[i] {\n\t\t\tcur -= over[i]\n\t\t} else {\n\t\t\tcur = 0\n\t\t}\n\t\tsc.resid[k] = cur\n\t}\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies. Maximizing p*amt is\n// exactly maximizing delivered value per attempt.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan, sc *searchCtx) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, sc); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, sc *searchCtx) []*edge {\n\n\tif p, err := r.findPath(hi, sc); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 6 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, sc); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// after the shards already planned have booked their share. This is the\n// min-cost-flow style joint plan: unequal parallel corridors each get a\n// shard that fits, instead of discovering the split by failing at a blind\n// half, and a fat hub channel may carry two shards when it has the room.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []*plan {\n\n\tsc := &searchCtx{\n\t\tresid: make(map[edgeKey]lnwire.MilliSatoshi),\n\t\tavoid: make(map[uint64]bool),\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, sc)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path, sc); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tsc.avoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tsc.avoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tr.consume(path, pl.amt, sc)\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// --- In-flight reservation ------------------------------------------------\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// hold reserves the liquidity of a route we are about to hand out, so a\n// concurrent shard is planned over different millisats.\nfunc (r *router) hold(rt *route.Route) {\n\tr.outstanding = append(r.outstanding, rt)\n\tr.markInFlight(rt, +1)\n}\n\n// release drops the reservation of a route whose result just came back.\nfunc (r *router) release(rt *route.Route) {\n\tfor i, o := range r.outstanding {\n\t\tif o == rt {\n\t\t\tr.outstanding = append(\n\t\t\t\tr.outstanding[:i], r.outstanding[i+1:]...,\n\t\t\t)\n\t\t\tr.markInFlight(o, -1)\n\t\t\treturn\n\t\t}\n\t}\n\n\tsig := routeSig(rt)\n\tfor i, o := range r.outstanding {\n\t\tif routeSig(o) == sig {\n\t\t\tr.outstanding = append(\n\t\t\t\tr.outstanding[:i], r.outstanding[i+1:]...,\n\t\t\t)\n\t\t\tr.markInFlight(o, -1)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// clearInFlight drops every reservation. The runner telling us that nothing\n// is in flight is ground truth, so this repairs any accounting we lost track\n// of instead of letting a phantom reservation block a channel forever.\nfunc (r *router) clearInFlight() {\n\tfor _, b := range r.beliefs {\n\t\tb.inFlight = 0\n\t}\n\tr.outstanding = nil\n}\n\n// dropQueued removes queued shards that route over a channel which just\n// failed, keeping the rest of a joint plan alive.\nfunc (r *router) dropQueued(chanID uint64) {\n\tout := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tkeep := true\n\t\tfor _, e := range pl.path {\n\t\t\tif e.chanID == chanID {\n\t\t\t\tkeep = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif keep {\n\t\t\tout = append(out, pl)\n\t\t}\n\t}\n\tr.queued = out\n}\n\n// --- Route requests -------------------------------------------------------\n\n// deliver books an attempt and returns its route.\nfunc (r *router) deliver(pl *plan) (*route.Route, error) {\n\tr.attempts++\n\tr.hold(pl.rt)\n\treturn pl.rt, nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\t// The runner is authoritative about how many HTLCs are live, so use it\n\t// to repair any reservation we failed to release.\n\tif inFlightHtlcs == 0 && len(r.outstanding) > 0 {\n\t\tr.clearInFlight()\n\t}\n\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tstreakLimit := maxFailStreak\n\tif r.settles > 0 {\n\t\tstreakLimit += progressBonus\n\t}\n\tif r.failStreak >= streakLimit {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\t// Hard infeasibility: our own balances can only shrink, so a remainder\n\t// larger than everything we hold can never be paid. Giving up now\n\t// saves a long tail of doomed attempts.\n\tif r.rawLocalBudget() < amt {\n\t\treturn nil, errors.New(\"insufficient local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn r.deliver(&plan{\n\t\t\tpath: pl.path, rt: rt, amt: a, prob: p,\n\t\t})\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tvar best *plan\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, amt, partsLeft) {\n\t\t// Once we hold a solid plan, digging far below it only wastes\n\t\t// attempts and parts.\n\t\tif best != nil && best.prob >= 0.55 && a < best.amt/2 {\n\t\t\tbreak\n\t\t}\n\t\tif probes >= probeBudget {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\n\t\tpath, err := r.findPath(a, nil)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tbest = r.evalPath(path, a, amt, best, nil)\n\n\t\t// A confident full-amount plan needs no alternatives.\n\t\tif best != nil && best.amt >= amt && best.prob > 0.75 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder confidently, plan\n\t// the split deliberately over parallel corridors instead of halving\n\t// blindly. We compare the aggregate believed flow of the plan set\n\t// against the best single shard: covering more of the payment beats a\n\t// slightly nicer first hop, because every uncovered millisat is a\n\t// failed payment.\n\tif partsLeft > 1 && (best == nil || best.amt < amt || best.prob < 0.5) {\n\t\tflow := r.planFlow(amt, partsLeft)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt ||\n\t\t\t\t(total >= amt && first.prob > best.prob) {\n\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, partsLeft)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\t// A hopeless attempt costs a retry and teaches almost nothing, so once\n\t// we have already spent a few, stop chasing lottery tickets.\n\tif best.prob < giveUpProb && r.attempts >= giveUpAfter {\n\t\treturn nil, errors.New(\"no viable route\")\n\t}\n\n\treturn r.deliver(best)\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder. It is the difference between delivering part of a payment\n// and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) *plan {\n\n\tif partsLeft <= 1 {\n\t\t// Without parts left, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 18 && a >= minShard; i++ {\n\t\tpath, err := r.findPath(a, nil)\n\t\tif err == nil {\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// --- Feedback -------------------------------------------------------------\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.failCount = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier shards and payments stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.failCount = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.failCount = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"),\n\t\tstrings.Contains(d, \"ExpiryTooFar\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\tcase strings.Contains(d, \"MPPTimeout\"),\n\t\tstrings.Contains(d, \"PaymentDetails\"),\n\t\tstrings.Contains(d, \"FinalIncorrect\"),\n\t\tstrings.Contains(d, \"RequiredNodeFeature\"):\n\t\t// A payment-level failure says nothing about liquidity, so it\n\t\t// must not damage any bound we have earned.\n\t\treturn \"final\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release the liquidity we had reserved for this attempt.\n\tr.release(rt)\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.settles++\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\n\tkind := classify(result.Failure)\n\tif kind != \"final\" {\n\t\tif sig := routeSig(rt); sig != \"\" {\n\t\t\tr.failedSigs[sig] = true\n\t\t}\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif kind == \"final\" {\n\t\t// The receiver, not the corridor, rejected this shard. Keep the\n\t\t// corridor's evidence intact and simply try again.\n\t\treturn nil\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tr.queued = nil\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch kind {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.dropQueued(h.ChannelID)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.failCount++\n\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// Other shards of the same joint plan that lean on this channel are\n\t// now unsound, but the rest of the plan still stands.\n\tr.dropQueued(h.ChannelID)\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 22,
"parent": 4,
"score": 0.8052,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING with residual accounting: shards may share a\n// fat channel as long as the fat channel is believed to bear their sum.\n//\n// Changes in this revision, driven by the evaluation data. The failing\n// scenario was always the same shape: one very large payment to a distant\n// target, where a handful of attempts each hit TemporaryChannelFailure on\n// the last hop and the router then gave up with \"no route found\" after 5-13\n// attempts even though the attempt budget was nowhere near exhausted. Root\n// causes and fixes:\n//\n// 1. GIVING UP WHILE PARTS REMAIN. RequestRoute returned a terminal error\n// the moment the single-shard search and the flow planner both came up\n// empty at the amounts they happened to probe. With MaxParts still\n// free, a small shard that fits is worth far more than a terminal\n// failure: it both delivers value and buys fresh evidence. This\n// revision adds a LAST-RESORT DESCENT over a fine geometric ladder, so\n// terminal failure now means \"nothing at any size\".\n//\n// 2. THE PLANNER ONLY EVER SIZED SHARDS DOWNWARD FROM `left`, asking each\n// successive corridor for the whole remainder and then falling down a\n// coarse probe ladder. That systematically produced one or two fat\n// shards plus an uncarryable stub. Corridors are now asked for a TARGET\n// SHARE (remaining / partsLeft), which is what a min-cost-flow\n// decomposition wants, and PARTIAL plans are accepted: covering most of\n// a huge payment over several corridors and re-planning the tail beats\n// refusing to split.\n//\n// 3. LAST-HOP BOTTLENECKS WERE INVISIBLE. Every recorded failure in the\n// eval was at the FINAL hop. The target's inbound capacity is the true\n// bottleneck for a large payment, so reusing one final channel for\n// several shards is doomed. Already-loaded inbound edges of the target\n// now carry an extra cost penalty during the flow search, which spreads\n// shards over distinct final edges before it stacks them.\n//\n// 4. UNATTRIBUTABLE FAILURES (failed_at_hop == -1) appeared in a third of\n// the failing attempts and previously only bumped a soft \"miss\"\n// counter, which blocked nothing, so the search re-picked near-identical\n// corridors. Misses now also impose a soft CEILING on the amount we\n// will retry over that direction, forcing genuine diversification\n// without destroying the hard bounds we earned.\n//\n// 5. Budgets raised where they were binding, and policy repairs (fee /\n// cltv / min-htlc) no longer count against the give-up streak, since\n// they are bookkeeping fixes rather than evidence of a dead corridor.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. Both are generous: the evaluation\n\t// showed payments abandoned with plenty of budget left, and one extra\n\t// retry costs far less than a lost payment.\n\tmaxAttempts = 110\n\tmaxFailStreak = 34\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 16\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// rescueShard is the floor used by the last-resort descent, which may\n\t// go smaller than an ordinary planned shard because any delivered\n\t// value beats a terminal failure.\n\trescueShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 14\n\tflowRebalance = 3\n\n\t// finalReusePenalty is the extra cost charged for reusing an inbound\n\t// edge of the target that a planned shard already loads. The target's\n\t// inbound capacity is the usual bottleneck on large payments, so the\n\t// planner works hard to spread over distinct final edges.\n\tfinalReusePenalty = 2.6\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// missCap is a soft ceiling learned from unattributable failures: the\n\t// smallest amount this direction carried as part of an attempt that\n\t// failed somewhere we could not localise. Retrying at or above it\n\t// wastes attempts, so we shrink under it.\n\tmissCap lnwire.MilliSatoshi\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// settled counts shards that have settled for this payment, which is\n\t// evidence the payment is progressing even if individual probes fail.\n\tsettled 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// softCeiling is the ceiling implied by unattributable failures. Each miss\n// shrinks the amount we are willing to retry over this direction, which is\n// what makes the search actually diversify after a blind failure instead of\n// re-picking a corridor with a marginally worse score.\nfunc (b *belief) softCeiling() lnwire.MilliSatoshi {\n\tif b == nil || b.misses == 0 || b.missCap == 0 {\n\t\treturn 0\n\t}\n\tm := b.misses\n\tif m > 3 {\n\t\tm = 3\n\t}\n\tf := math.Pow(0.6, float64(m))\n\tlim := lnwire.MilliSatoshi(float64(b.missCap) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t\tif lim := b.softCeiling(); lim > 0 && lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works. finalPenalty\n// applies the same idea specifically to already-loaded INBOUND edges of the\n// target, which are the usual bottleneck on large payments.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty,\n\tfinalPenalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 {\n\t\t\t\tif penalty > 1 {\n\t\t\t\t\tdist *= penalty\n\t\t\t\t}\n\t\t\t\t// The target's inbound edges deserve a\n\t\t\t\t// stronger spread incentive than interior\n\t\t\t\t// channels: they are where large payments\n\t\t\t\t// actually run out of room.\n\t\t\t\tif it.node == target && finalPenalty > 1 {\n\t\t\t\t\tdist *= finalPenalty\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 4 / 5\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry hi under the provisional\n// flow already assigned, falling back down the amount ladder when nothing\n// can take the full amount.\nfunc (r *router) findFlowPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty. The\n\t// target's inbound edges keep a stronger penalty in both passes.\n\tfor _, pen := range []float64{1e9, 1.35} {\n\t\tif p, err := r.findPath(\n\t\t\thi, avoid, extra, pen, finalReusePenalty,\n\t\t); err == nil {\n\t\t\treturn p\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\t\tif a >= hi {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif probes >= 6 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tprobes++\n\t\t\tp, err := r.findPath(\n\t\t\t\ta, avoid, extra, pen, finalReusePenalty,\n\t\t\t)\n\t\t\tif err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t}\n\t\tif len(extra) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search.\n//\n// Two changes over the previous revision matter most. First, each successive\n// corridor is asked for a TARGET SHARE of the remainder rather than the\n// whole remainder: asking for the full amount every round made the search\n// fall down a coarse ladder and produced fat-shard-plus-stub splits. Second,\n// a PARTIAL plan is accepted and returned: covering most of a huge payment\n// over several corridors and re-planning the tail on the next call beats\n// refusing to split.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\t// share is what we ask each corridor for: an even split of the\n\t// remainder over the parts we can still afford, which keeps the\n\t// search from burning the widest corridor on an oversized shard.\n\tshare := remaining / lnwire.MilliSatoshi(partsLeft)\n\tif share < minShard {\n\t\tshare = minShard\n\t}\n\n\tfor k := uint32(0); k < partsLeft && k < flowRounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\t// Ask for the even share, except when only a little remains,\n\t\t// where asking for all of it keeps the tail from fragmenting.\n\t\task := share\n\t\tif left < ask || left <= share*2 {\n\t\t\task = left\n\t\t}\n\n\t\tpath := r.findFlowPath(ask, remaining, partsLeft, avoid, extra)\n\t\tif path == nil && ask < left {\n\t\t\t// The share was unroutable; try the full remainder\n\t\t\t// before giving up on this round.\n\t\t\tpath = r.findFlowPath(\n\t\t\t\tleft, remaining, partsLeft, avoid, extra,\n\t\t\t)\n\t\t}\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has the\n\t// most slack under the current assignment. This is what produces the\n\t// deliberately unequal split.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// rescue is the last-resort descent. When neither the single-shard search\n// nor the flow planner produced anything at the amounts they tried, we walk\n// a fine geometric ladder down to a tiny floor looking for ANY sendable\n// route, as long as at least one more part remains after this one. The\n// evaluation showed large payments abandoned with attempts and parts to\n// spare, purely because nothing fit at the amounts probed; a small shard\n// both delivers value and buys evidence for the next round.\nfunc (r *router) rescue(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\ta := amt / 2\n\tif a < rescueShard {\n\t\ta = rescueShard\n\t}\n\n\tfor i := 0; i < 22 && a >= rescueShard; i++ {\n\t\t// Try the busy-avoiding search first, then the unrestricted\n\t\t// one, at each size.\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif r.failedSigs[planSig(path, a)] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfee := rt.TotalAmount - a\n\t\t\treturn &plan{\n\t\t\t\tpath: path,\n\t\t\t\trt: rt,\n\t\t\t\tamt: a,\n\t\t\t\tprob: p,\n\t\t\t\tscore: r.score(a, p, fee, amt),\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 5\n\t}\n\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, then compares the best single shard against a full\n// min-cost-flow style route set over several corridors, and finally falls\n// back to a fine descent rather than giving up while parts remain.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. This fires EAGERLY\n\t// (not only when nothing single-path exists), because a joint plan\n\t// whose shards each fit their corridor beats one oversized shard that\n\t// will fail and then be halved blindly.\n\tsingleWeak := best == nil || best.amt < amt || best.prob < 0.55\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it either covers more of the\n\t\t\t// remainder or delivers more expected value than the\n\t\t\t// single shard on its own.\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tweighted > bestVal*1.05\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: never return a terminal error while a part remains and\n\t// some small amount is still sendable. This is the single biggest fix\n\t// over the previous revision, which abandoned large payments with both\n\t// attempts and parts to spare.\n\tif best == nil {\n\t\tr.dropQueue()\n\t\tbest = r.rescue(amt, partsLeft, busy)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n\tb.missCap = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\tb.missCap = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\trb.missCap = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.settled++\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned. The soft ceiling is what actually\n\t// forces diversification, since the next attempt over this direction\n\t// must then be materially smaller.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\ta := hopAmount(rt, i)\n\t\t\tif b.missCap == 0 || a < b.missCap {\n\t\t\t\tb.missCap = a\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a failed probe, so it must not count\n\t\t// against the streak that decides when to give up.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
},
{
"id": 23,
"parent": 6,
"score": 0.7505,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds\n// and no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than what decay throws away.\n// - Balance bookkeeping on settle, plus bimodal optimism after a proven\n// forward.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING with residual accounting and rebalancing.\n//\n// Changes in this revision, driven by the observed failure mode: a large\n// payment (860M msat, roughly the whole local budget) burned ten attempts\n// and then terminated with \"no route found\" while the split never got wide\n// enough. The root causes were:\n//\n// 1. The flow planner treated the WHOLE remainder as one search target and\n// let the first corridor eat as much as its bottleneck allowed. On a\n// network where no single corridor can carry a third of the payment,\n// the first shard is still sized enormous, the plan collapses to two or\n// three shards, and the residual is simply unroutable. This revision\n// caps any single planned shard at a FAIR SHARE of the remainder\n// (remaining/slotsLeft, relaxed as corridors run out) so the plan\n// naturally spreads across many corridors before it grows any of them.\n// Growth then happens only in the rebalance sweep, which is\n// slack-driven and therefore safe.\n//\n// 2. The planner gave up the moment findFlowPath returned nothing, even\n// when most of the remainder was still unassigned. It now retries at\n// progressively smaller targets before abandoning the round, so a\n// narrow corridor still contributes a small shard instead of nothing.\n//\n// 3. A plan that covered only part of the remainder was accepted and then\n// the leftover was rediscovered by blind halving. The plan is now\n// scored on COVERAGE as well as expected value, and partial plans are\n// still taken when they beat the single shard, because each settled\n// shard shrinks the remainder and re-triggers planning with better\n// evidence.\n//\n// 4. Terminal give-up was too eager relative to how much of the payment\n// had already settled. The fail-streak budget now scales with the\n// number of parts allowed, and pure policy repairs (fee/cltv/min) no\n// longer consume it, since they are not liquidity misses.\n//\n// 5. Local-channel exhaustion is now recognised explicitly: when the sum\n// of our own believed-spendable balances cannot cover the remainder,\n// there is no point insisting on a full-amount corridor, so we go\n// straight to the widest split we can build.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts bounds total work. maxFailStreakBase is the give-up\n\t// budget for a single-part payment; wide splits get more (see\n\t// failBudget) because each shard legitimately costs its own probes.\n\tmaxAttempts = 80\n\tmaxFailStreakBase = 14\n\tmaxFailStreak = 30\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 14\n\tflowRebalance = 3\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// failBudget is how long a payment may go without progress before we give\n// up. A wide split legitimately needs more probing than a single shard, so\n// the budget scales with the part count.\nfunc (r *router) failBudget() int {\n\tb := maxFailStreakBase + 3*int(r.spec.MaxParts)\n\tif b > maxFailStreak {\n\t\tb = maxFailStreak\n\t}\n\treturn b\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 && penalty > 1 {\n\t\t\t\tdist *= penalty\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// widestLocal is the largest single local channel balance still free, which\n// is a hard ceiling on any one shard.\nfunc (r *router) widestLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry hi under the provisional\n// flow already assigned, walking down a geometric ladder of smaller targets\n// before giving up. Falling all the way down matters: a narrow corridor that\n// can only take an eighth of the target is still worth a shard, and refusing\n// it is what previously collapsed big plans into two shards plus an\n// unroutable remainder.\nfunc (r *router) findFlowPath(hi lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty.\n\tfor _, pen := range []float64{1e9, 1.3} {\n\t\ta := hi\n\t\tfor i := 0; i < 7 && a >= minShard; i++ {\n\t\t\tp, err := r.findPath(a, avoid, extra, pen)\n\t\t\tif err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t\ta = a / 2\n\t\t}\n\t\tif len(extra) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search.\n//\n// The key sizing rule is the FAIR SHARE cap: no single shard may take much\n// more than its even share of what is left in the early rounds. Without it\n// the first corridor swallows everything it can bear and the plan degenerates\n// into a couple of fat shards plus a residual nothing can carry. Growth is\n// reintroduced afterwards by the slack-driven rebalance sweep, which is safe\n// because it only hands out amount the corridor is believed able to bear.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\t// Fair share: spread first, grow later. As rounds run out the\n\t\t// cap relaxes towards the whole remainder so the last\n\t\t// corridors can still absorb what is left.\n\t\tslotsLeft := rounds - k\n\t\tshare := left\n\t\tif slotsLeft > 1 {\n\t\t\tshare = left / lnwire.MilliSatoshi(slotsLeft)\n\t\t\t// Allow a moderate overshoot so a genuinely fat\n\t\t\t// corridor is not artificially starved.\n\t\t\tshare += share / 2\n\t\t\tif share > left {\n\t\t\t\tshare = left\n\t\t\t}\n\t\t}\n\t\tif share < minShard {\n\t\t\tshare = left\n\t\t}\n\n\t\tpath := r.findFlowPath(share, avoid, extra)\n\t\tif path == nil {\n\t\t\t// Nothing at the fair share; try the whole remainder\n\t\t\t// once in case only one wide corridor exists.\n\t\t\tif share >= left {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpath = r.findFlowPath(left, avoid, extra)\n\t\t\tif path == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tamtS := share\n\t\tif amtS > left {\n\t\t\tamtS = left\n\t\t}\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has\n\t// slack. This is what turns the fair-share spread back into a\n\t// deliberately unequal split sized to real believed capacity.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, and finally compares the best single shard against a full\n// min-cost-flow style route set over several corridors.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= r.failBudget() {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what one of our own channels can push right\n\t// now, and the total can never exceed the whole local budget. When the\n\t// budget cannot cover the remainder, insisting on a full-amount\n\t// corridor is pure waste, so we note that and go wide immediately.\n\tbudget := r.localBudget()\n\thi := amt\n\tif w := r.widestLocal(); w > 0 && w < hi {\n\t\thi = w\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\tlocalShort := budget > 0 && budget < amt\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. This fires eagerly,\n\t// because a joint plan whose shards each fit their corridor beats one\n\t// oversized shard that will fail and then be halved blindly.\n\tsingleWeak := best == nil || best.amt < amt || best.prob < 0.55 ||\n\t\tlocalShort\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it covers more of the remainder or\n\t\t\t// delivers more expected value. Coverage is weighted\n\t\t\t// heavily: a plan that reaches the full amount, even at\n\t\t\t// modest per-shard probability, beats a confident shard\n\t\t\t// that leaves a residual nothing can carry.\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tweighted > bestVal*1.05\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: if nothing at all was found but we still have parts,\n\t// probe aggressively small so at least some of the payment lands. A\n\t// partially settled payment shrinks the remainder and lets the next\n\t// call plan against fresher, better evidence.\n\tif best == nil && partsLeft > 1 {\n\t\ta := hi / 2\n\t\tfor i := 0; i < 10 && a >= minShard; i++ {\n\t\t\tif path, err := r.plainPath(a, nil); err == nil {\n\t\t\t\tif pl := r.bestOnPath(path, a, amt); pl != nil {\n\t\t\t\t\tbest = pl\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\ta = a / 2\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a liquidity miss, so it should not eat\n\t\t// the give-up budget.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
},
{
"id": 24,
"parent": 8,
"score": 0.5412,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): shard sizes and\n// corridors are chosen TOGETHER by a greedy flow decomposition over\n// DISJOINT corridors, each shard sized to that corridor's believed\n// bottleneck.\n// - LARGE-PAYMENT FIRST-CLASS SPLITTING. The two observed terminal\n// failures were both very large payments that burned 21-24 attempts and\n// still died with \"no route found\". Root cause: the planner kept probing\n// at the FULL remaining amount first, and single-corridor plans at\n// implausible amounts consumed the ladder/probe budget before the flow\n// planner ever got a fair shot. Now, whenever the remaining amount\n// plainly exceeds what any single corridor can bear (measured against\n// the believed out-capacity of our own channels), the flow planner runs\n// FIRST and the single-path search is only a fallback. Shard sizing\n// starts from remaining/partsLeft rather than from remaining.\n// - PARALLEL-CORRIDOR RESERVATION. Queued shards reserve their corridor's\n// liquidity in the belief layer (soft in-flight), so the planner does\n// not hand out two shards that silently share the same bottleneck hop.\n// - PROGRESS-AWARE GIVE-UP. The fail streak is reset not only by a settle\n// but by any attempt that pushed the frontier deeper (a new hop proven\n// to forward), so learning probes on a hard payment are not counted as\n// \"no progress\".\n// - PLAN REPAIR instead of plan discard: when an attempt fails, only the\n// queued shards whose corridors touch the newly-condemned channel are\n// dropped.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the\n// local policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. The streak is generous because a\n\t// large multi-part payment legitimately spends several probes learning\n\t// the shape of the network before any shard lands.\n\tmaxAttempts = 70\n\tmaxFailStreak = 30\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the greedy flow decomposition.\n\tflowRounds = 16\n\n\t// bigSplitSlack is how much believed headroom we demand over the\n\t// remaining amount before we even consider a single-corridor plan for\n\t// the whole thing. Above this the flow planner leads.\n\tbigSplitSlack = 1.05\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity soft-committed by queued (not yet dispatched)\n\t// shards of the current joint plan. It keeps two planned shards from\n\t// double-booking the same bottleneck without hard-blocking a retry.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// progress counts hops newly proven to forward. It is used to reset\n\t// the fail streak: a probe that pushed the frontier deeper taught us\n\t// something even though it did not settle.\n\tprogress 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity already spoken for on a directed channel, both by\n// live HTLCs and by queued shards of the current plan.\nfunc committed(b *belief) lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := committed(b); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar used lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tused = committed(b)\n\t\tmisses = b.misses\n\t}\n\teff := amt + used\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// widestLocal is the largest single local channel balance still free. No\n// single shard can exceed it, so it tells us immediately whether the whole\n// remaining amount could ever ride one corridor.\nfunc (r *router) widestLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// hardBottleneck is the corridor's absolute believed ceiling, used by the\n// feasibility gate: it ignores the conservative safeCap shading and asks\n// only whether liquidity could plausibly flow at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 8 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// reservePlan soft-books a corridor's liquidity so a sibling shard planned\n// in the same round cannot double-spend the same bottleneck hop.\nfunc (r *router) reservePlan(pl *plan, sign int) {\n\tif pl == nil || len(pl.path) == 0 {\n\t\treturn\n\t}\n\tn := len(pl.path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\tamtOver[n-1] = pl.amt\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := pl.path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t}\n\tfor i, e := range pl.path {\n\t\tb := r.bel(e.key())\n\t\tif sign > 0 {\n\t\t\tb.reserved += amtOver[i]\n\t\t} else if b.reserved >= amtOver[i] {\n\t\t\tb.reserved -= amtOver[i]\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n}\n\n// clearReservations drops every soft reservation held by queued shards.\nfunc (r *router) clearReservations() {\n\tfor _, b := range r.beliefs {\n\t\tb.reserved = 0\n\t}\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// target caps how much a single corridor may carry on this pass. For very\n// large payments the caller passes remaining/partsLeft so the decomposition\n// starts from a plausible shard size instead of the whole amount.\nfunc (r *router) planFlow(remaining, target lnwire.MilliSatoshi,\n\tpartsLeft uint32, busy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif target > 0 && target < want {\n\t\t\twant = target\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor is too thin to be worth a shard, but it\n\t\t\t// is still worth excluding so the next round finds\n\t\t\t// something else.\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\t// Best shards first: the largest, most reliable corridor should be\n\t// tried before the marginal ones.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].score > out[j].score\n\t})\n\n\treturn out\n}\n\n// feasibleFlow is the total delivered amount our believed liquidity could\n// carry over up to `rounds` disjoint corridors. It is the gate that keeps a\n// large payment alive: as long as the network plausibly has the liquidity,\n// we keep carving smaller corridors instead of returning a terminal error.\nfunc (r *router) feasibleFlow(remaining lnwire.MilliSatoshi,\n\trounds int) lnwire.MilliSatoshi {\n\n\tavoid := make(map[uint64]bool)\n\tvar total lnwire.MilliSatoshi\n\n\tfor k := 0; k < rounds; k++ {\n\t\tleft := remaining - total\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\t\tpath, err := r.findPath(left, avoid)\n\t\tif err != nil {\n\t\t\tpath = r.findAnyPath(left, remaining, 4, avoid)\n\t\t\tif path == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tbn := r.hardBottleneck(path)\n\t\tif bn > left {\n\t\t\tbn = left\n\t\t}\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\t\tif bn < minShard {\n\t\t\tcontinue\n\t\t}\n\t\ttotal += bn\n\t}\n\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// pruneQueue drops only the queued shards whose corridors touch a channel\n// whose belief just got worse. Discarding the entire joint plan on every\n// failure throws away correct planning work and forces the router to\n// rediscover the same split by trial and error.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkeep := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tdrop := false\n\t\tfor _, e := range pl.path {\n\t\t\tif hit[e.chanID] {\n\t\t\t\tdrop = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif drop {\n\t\t\tr.reservePlan(pl, -1)\n\t\t\tcontinue\n\t\t}\n\t\tkeep = append(keep, pl)\n\t}\n\tr.queued = keep\n}\n\n// installPlan queues the tail of a joint plan and reserves its liquidity.\nfunc (r *router) installPlan(tail []*plan) {\n\tfor _, pl := range r.queued {\n\t\tr.reservePlan(pl, -1)\n\t}\n\tr.queued = tail\n\tfor _, pl := range r.queued {\n\t\tr.reservePlan(pl, +1)\n\t}\n}\n\n// popQueued serves the next queued shard that still holds up against\n// current beliefs, releasing its reservation as it is dispatched.\nfunc (r *router) popQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.reservePlan(pl, -1)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.installPlan(nil)\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard.\n//\n// The order of operations matters and was the main fix in this revision.\n// Large payments (remaining far above what any single corridor can carry)\n// go to the joint flow planner FIRST, sized at remaining/partsLeft, instead\n// of burning the probe budget on single-corridor plans for the full amount\n// that cannot possibly exist. Small and mid payments keep the old order:\n// best single corridor first, flow planning as the fallback.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tif rt := r.popQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now,\n\t// nor what the single widest local channel holds.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif w := r.widestLocal(); w > 0 && w < hi {\n\t\thi = w\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// Is this a payment that plainly cannot ride one corridor? If the\n\t// widest single local channel cannot cover the remainder with a\n\t// little slack, splitting is not a fallback, it is the plan.\n\tbigSplit := partsLeft > 1 &&\n\t\tfloat64(hi)*bigSplitSlack < float64(amt)\n\n\tif bigSplit {\n\t\t// Size the first corridor at an even share, so the flow\n\t\t// decomposition starts from a plausible shard instead of the\n\t\t// whole amount, then let each corridor's bottleneck widen or\n\t\t// narrow it.\n\t\ttarget := amt / lnwire.MilliSatoshi(partsLeft)\n\t\tif target < minShard {\n\t\t\ttarget = minShard\n\t\t}\n\t\tif target > hi {\n\t\t\ttarget = hi\n\t\t}\n\n\t\tflow := r.planFlow(amt, target, partsLeft, busy)\n\t\tif len(flow) == 0 && target < hi {\n\t\t\t// Even shares found nothing; let corridors take as\n\t\t\t// much as they can bear.\n\t\t\tflow = r.planFlow(amt, hi, partsLeft, busy)\n\t\t}\n\t\tif len(flow) > 0 {\n\t\t\tr.installPlan(flow[1:])\n\t\t\tr.attempts++\n\t\t\treturn flow[0].rt, nil\n\t\t}\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. The\n\t// plan is accepted when its corridors jointly cover meaningfully more\n\t// of the remainder than the single best shard would.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, 0, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.75)\n\n\t\t\tif better {\n\t\t\t\tr.installPlan(flow[1:])\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best != nil {\n\t\tr.attempts++\n\t\treturn best.rt, nil\n\t}\n\n\t// Last resort: our conservative sizing found nothing, but the network\n\t// may still be able to carry small shards. Walk the ladder with the\n\t// hard (unshaded) bounds and take the biggest novel attempt we can\n\t// build, rather than declaring the payment dead. This is what turns a\n\t// premature \"no route found\" into a completed multi-part payment.\n\t//\n\t// The reservations of any stale queued plan are cleared first: they\n\t// are exactly what may be masking the remaining liquidity.\n\tr.installPlan(nil)\n\tr.clearReservations()\n\n\tif r.feasibleFlow(amt, flowRounds) >= minShard {\n\t\tfor _, a := range r.ladder(amt, amt, partsLeft) {\n\t\t\tpath, err := r.findPath(a, nil)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpl := r.bestOnPath(path, a, amt)\n\t\t\tif pl == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.attempts++\n\t\t\treturn pl.rt, nil\n\t\t}\n\t}\n\n\t// Absolute floor: try to deliver *something* on the smallest viable\n\t// shard. A partial part still teaches us where liquidity lives, and\n\t// the runner will call again with the remainder.\n\tif partsLeft > 1 {\n\t\tsmall := amt / 16\n\t\tfor i := 0; i < 8 && small >= minShard; i++ {\n\t\t\tif path, err := r.findPath(small, nil); err == nil {\n\t\t\t\tif pl := r.bestOnPath(\n\t\t\t\t\tpath, small, amt,\n\t\t\t\t); pl != nil {\n\t\t\t\t\tr.attempts++\n\t\t\t\t\treturn pl.rt, nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tsmall = small / 2\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved. It reports\n// whether this was genuinely new knowledge.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) bool {\n\tb := r.bel(hopKey(h))\n\tlearned := a > b.okAmt || !b.succ\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tlearned = true\n\t}\n\tif b.misses > 0 {\n\t\tlearned = true\n\t}\n\tb.misses = 0\n\n\treturn learned\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settled shard drained the corridors it used, so any queued\n\t\t// shard sharing those channels must be re-planned.\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor _, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved. Learning that a\n\t// deeper hop forwards IS progress, so it refreshes the fail streak.\n\tlearned := false\n\tfor i := 0; i < failIdx; i++ {\n\t\tif r.provePassed(rt.Hops[i], hopAmount(rt, i)) {\n\t\t\tlearned = true\n\t\t}\n\t}\n\tif learned {\n\t\tr.progress++\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// fee repair is not evidence against any corridor, so queued\n\t\t// shards survive untouched.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\t// Only the queued corridors that actually depend on the condemned\n\t// channel are invalidated; the rest of the joint plan stays valid.\n\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\n\treturn nil\n}"
}
},
{
"id": 25,
"parent": 8,
"score": 0.6335,
"accepted": false,
"frontier": false,
"params": {
"source": "// reflection unavailable: claude -p failed (1):"
}
},
{
"id": 26,
"parent": 6,
"score": 0.6662,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - BIMODAL OPTIMISM after a proven forward: a channel that has forwarded\n// for us is treated as holding the bulk of its capacity (minus what we\n// drained), which lets later, larger payments reuse a proven corridor.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style) when no single corridor\n// can carry the remainder, plus a FEASIBILITY PRE-CHECK: before spending\n// attempts, a disjoint-corridor flow decomposition estimates whether the\n// full amount is deliverable at all. If the believed deliverable flow is\n// far short of the remaining amount, we give up early rather than burn\n// 19 attempts on a hopeless payment (the dominant retry-efficiency leak\n// in the previous generation).\n// - ATTEMPT ECONOMY: a queued joint plan is served without re-planning, a\n// confident full-amount plan short-circuits the search, and the ladder is\n// pruned aggressively once a solid plan exists.\n// - Duplicate-attempt suppression: a (path, amount) pair that already\n// failed is never handed out again.\n// - Non-liquidity failures (fee, cltv, disabled) are repaired in the local\n// policy view instead of being treated as liquidity misses, and they do\n// not count against the fail streak or the hopeless verdict.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. The streak bound is tighter than\n\t// before: long unproductive tails cost retry efficiency and almost\n\t// never convert into a success.\n\tmaxAttempts = 40\n\tmaxFailStreak = 12\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.2\n\n\t// hopelessFrac is the fraction of the remaining amount that the\n\t// believed max-flow must cover for us to keep trying. Below this we\n\t// abandon rather than grind out attempts we cannot win.\n\thopelessFrac = 0.55\n\n\t// hopelessStrikes is how many consecutive feasibility checks must come\n\t// back hopeless before we terminate. One bad check may just reflect\n\t// pessimistic beliefs that a probe would refresh.\n\thopelessStrikes = 2\n\n\t// flowProbes bounds the Dijkstra runs a feasibility estimate spends.\n\tflowProbes = 6\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// hopeless counts consecutive feasibility checks that concluded the\n\t// remaining amount is not deliverable.\n\thopeless 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 1024\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 12; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// inFlightAmt is the delivered amount our currently pending shards carry. A\n// feasibility check must count that as already covered.\nfunc (r *router) inFlightAmt() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, rt := range r.pending {\n\t\tif n := len(rt.Hops); n > 0 {\n\t\t\ttotal += rt.Hops[n-1].AmtToForward\n\t\t}\n\t}\n\treturn total\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 5 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// feasibleFlow estimates how much of want we believe is actually deliverable\n// right now, by greedily peeling off disjoint corridors and summing their\n// believed bottlenecks. It is deliberately optimistic (it uses safeCap, which\n// already embeds bimodal optimism), so a verdict of \"far short\" is strong\n// evidence that grinding out more attempts is wasted effort.\nfunc (r *router) feasibleFlow(want lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts == 0 {\n\t\tparts = 1\n\t}\n\tif parts > flowProbes {\n\t\tparts = flowProbes\n\t}\n\n\tavoid := make(map[uint64]bool)\n\tvar total lnwire.MilliSatoshi\n\n\tfor k := uint32(0); k < parts; k++ {\n\t\tleft := want - total\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath, err := r.findPath(left, avoid)\n\t\tif err != nil {\n\t\t\t// Nothing carries the whole remainder, so probe with\n\t\t\t// a small amount to see if any corridor survives.\n\t\t\tpath, err = r.findPath(minShard*4, avoid)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tbn := r.bottleneck(path)\n\t\tif bn < minShard {\n\t\t\tbreak\n\t\t}\n\t\tif bn > left {\n\t\t\tbn = left\n\t\t}\n\t\ttotal += bn\n\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\t}\n\n\treturn total\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\n\tfor k := uint32(0); k < partsLeft; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findAnyPath(left, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// giveUpEarly decides whether the payment is worth continuing. Once we have\n// paid for a few attempts, an optimistic flow decomposition tells us whether\n// the remaining amount is deliverable at all. Two consecutive hopeless\n// verdicts terminate the payment, which converts a long losing tail of\n// retries into an immediate, cheap failure.\nfunc (r *router) giveUpEarly(amt lnwire.MilliSatoshi,\n\tpartsLeft uint32) bool {\n\n\t// Early attempts are cheap probes that also refresh beliefs, so never\n\t// abandon before we have paid for real evidence.\n\tif r.attempts < 6 || r.failStreak < 4 {\n\t\treturn false\n\t}\n\n\twant := amt\n\tif inf := r.inFlightAmt(); inf > 0 {\n\t\t// Shards already in flight will cover part of the payment.\n\t\tif want > inf {\n\t\t\twant -= inf\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tflow := r.feasibleFlow(want, partsLeft)\n\tif float64(flow) < hopelessFrac*float64(want) {\n\t\tr.hopeless++\n\t} else {\n\t\tr.hopeless = 0\n\t}\n\n\treturn r.hopeless >= hopelessStrikes\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// Feasibility gate: stop grinding on a payment the network cannot\n\t// currently carry.\n\tif r.giveUpEarly(amt, partsLeft) {\n\t\treturn nil, errors.New(\"amount not deliverable\")\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.85)\n\n\t\t\tif better {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.hopeless = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a liquidity miss, so it must not count\n\t\t// toward the fail streak or the hopeless verdict.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 27,
"parent": 8,
"score": 0.744,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING with residual accounting: shards may share a\n// fat channel as long as the fat channel is believed to bear their sum.\n//\n// This revision is tuned against evaluation data where SUCCESS WAS ALREADY\n// 1.0 across every scenario batch: all three examples delivered every\n// payment, so the remaining score was lost entirely to RETRY EFFICIENCY\n// (-6.3, -1.7, -4.3 => 7 to 12.3 attempts per scenario) and, to a much\n// smaller degree, to FEES (400-850 ppm). The optimization target therefore\n// shifts: keep the success machinery that works, but stop paying for it\n// with attempts we did not need. Concretely:\n//\n// 1. FEWER SPECULATIVE PROBES PER PAYMENT. The previous revision walked a\n// 16-wide ladder with two passes and re-priced every corridor, then\n// eagerly re-planned a flow even when a single confident shard existed.\n// Every such extra pass tends to convert into an extra HTLC. The\n// single-shard search now stops as soon as it holds a plan that both\n// covers the remainder and clears a confidence bar, and the flow\n// planner only fires when a single shard genuinely cannot carry the\n// remainder (or is outright unlikely).\n//\n// 2. FIRST-ATTEMPT SIZING FROM BELIEF, NOT FROM HOPE. Most wasted\n// attempts in the data were a full-amount probe that failed and was\n// then halved. The ladder now starts from the amount the network is\n// actually believed able to carry end to end (a believed-capacity\n// estimate over our own channels and the target's inbound edges),\n// which removes the doomed opening probe on large payments.\n//\n// 3. SUCCESS-PROBABILITY GATING OF ATTEMPTS. A candidate whose estimated\n// probability is very low is only sent when nothing better exists and\n// we still have parts to spare; otherwise we shrink first. Sending a\n// 15%-likely shard costs one attempt for 0.15 expected shards.\n//\n// 4. FEE AWARENESS RAISED WHERE IT IS FREE. Among candidates of\n// comparable probability the cheaper one now wins by a wider margin,\n// and the attempt cost was lowered so the Dijkstra objective is not\n// numerically dominated by the retry term on large amounts. Reliability\n// still wins whenever it actually differs.\n//\n// 5. EVIDENCE REUSE ACROSS SHARDS WITHIN A PAYMENT. Proven-passed hops\n// now raise a per-payment \"known corridor\" cache that the planner\n// prefers, so later shards of an MPP reuse corridors already shown to\n// work instead of re-exploring and re-failing.\n//\n// The give-up budgets stay generous: success dominates the objective, so a\n// terminal failure is never worth saving an attempt.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(4_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. Both stay generous: a lost payment\n\t// costs far more than several retries.\n\tmaxAttempts = 110\n\tmaxFailStreak = 34\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass. Lower than before on purpose: extra probing\n\t// mostly converted into extra attempts, not extra successes.\n\tprobeBudget = 9\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// rescueShard is the floor used by the last-resort descent, which may\n\t// go smaller than an ordinary planned shard because any delivered\n\t// value beats a terminal failure.\n\trescueShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fees are a secondary\n\t// objective, so they are weighted enough to separate comparable\n\t// corridors, and every extra expected part costs a little.\n\tfeeWeight = 6.0\n\tpartCost = 0.010\n\n\t// confidentProb is the probability at which a full-remainder single\n\t// shard is considered good enough to send immediately, with no\n\t// further probing and no flow planning. Deciding early is what saves\n\t// attempts.\n\tconfidentProb = 0.62\n\n\t// weakProb is the probability below which we would rather shrink or\n\t// split than send, provided an alternative exists.\n\tweakProb = 0.22\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.18\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 10\n\tflowRebalance = 3\n\n\t// finalReusePenalty is the extra cost charged for reusing an inbound\n\t// edge of the target that a planned shard already loads. The target's\n\t// inbound capacity is the usual bottleneck on large payments, so the\n\t// planner works hard to spread over distinct final edges.\n\tfinalReusePenalty = 2.6\n\n\t// knownBonus discounts the cost of a channel that has demonstrably\n\t// forwarded for us during this payment, so later MPP shards reuse\n\t// corridors that already worked instead of re-exploring.\n\tknownBonus = 0.80\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// missCap is a soft ceiling learned from unattributable failures: the\n\t// smallest amount this direction carried as part of an attempt that\n\t// failed somewhere we could not localise. Retrying at or above it\n\t// wastes attempts, so we shrink under it.\n\tmissCap lnwire.MilliSatoshi\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// warm marks channels that demonstrably forwarded for us recently.\n\t// Reusing a warm corridor for a later MPP shard is cheaper in\n\t// attempts than exploring a fresh one.\n\twarm map[uint64]bool\n\n\tfailStreak int\n\tattempts int\n\n\t// settled counts shards that have settled for this payment, which is\n\t// evidence the payment is progressing even if individual probes fail.\n\tsettled 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\n\t\twarm: make(map[uint64]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// softCeiling is the ceiling implied by unattributable failures. Each miss\n// shrinks the amount we are willing to retry over this direction, which is\n// what makes the search actually diversify after a blind failure instead of\n// re-picking a corridor with a marginally worse score.\nfunc (b *belief) softCeiling() lnwire.MilliSatoshi {\n\tif b == nil || b.misses == 0 || b.missCap == 0 {\n\t\treturn 0\n\t}\n\tm := b.misses\n\tif m > 3 {\n\t\tm = 3\n\t}\n\tf := math.Pow(0.6, float64(m))\n\tlim := lnwire.MilliSatoshi(float64(b.missCap) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t\tif lim := b.softCeiling(); lim > 0 && lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works. finalPenalty\n// applies the same idea specifically to already-loaded INBOUND edges of the\n// target, which are the usual bottleneck on large payments.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty,\n\tfinalPenalty float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\tif amtOver+used > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\t// A corridor that already forwarded for us this\n\t\t\t// payment is worth reusing: it saves the attempt an\n\t\t\t// unexplored corridor would spend learning the same\n\t\t\t// thing.\n\t\t\tif r.warm[e.chanID] && used == 0 {\n\t\t\t\tdist *= knownBonus\n\t\t\t}\n\n\t\t\tif used > 0 {\n\t\t\t\tif penalty > 1 {\n\t\t\t\t\tdist *= penalty\n\t\t\t\t}\n\t\t\t\t// The target's inbound edges deserve a\n\t\t\t\t// stronger spread incentive than interior\n\t\t\t\t// channels: they are where large payments\n\t\t\t\t// actually run out of room.\n\t\t\t\tif it.node == target && finalPenalty > 1 {\n\t\t\t\t\tdist *= finalPenalty\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1, 1)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// singleHopCeiling is the largest amount ANY single one of our own channels\n// can push, and the largest amount any single inbound edge of the target is\n// believed able to absorb. A single shard can never exceed either, so\n// starting the ladder above them guarantees a wasted attempt. This is the\n// main source of the retry savings over the previous revision.\nfunc (r *router) singleHopCeiling() lnwire.MilliSatoshi {\n\tvar out lnwire.MilliSatoshi\n\tfor _, e := range r.localEdges {\n\t\tif c := r.availCap(e); c > out {\n\t\t\tout = c\n\t\t}\n\t}\n\n\tvar in lnwire.MilliSatoshi\n\tfor _, e := range r.inEdges[r.spec.Target] {\n\t\tif c := r.safeCap(e); c > in {\n\t\t\tin = c\n\t\t}\n\t}\n\n\tif in > 0 && in < out {\n\t\tout = in\n\t}\n\treturn out\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a fee penalty, and a cost for\n// every additional part the split implies. Fees carry more weight than in\n// the previous revision because success was already saturated and fee ppm\n// was the remaining loss term.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 4 / 5\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry hi under the provisional\n// flow already assigned, falling back down the amount ladder when nothing\n// can take the full amount.\nfunc (r *router) findFlowPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty. The\n\t// target's inbound edges keep a stronger penalty in both passes.\n\tfor _, pen := range []float64{1e9, 1.35} {\n\t\tif p, err := r.findPath(\n\t\t\thi, avoid, extra, pen, finalReusePenalty,\n\t\t); err == nil {\n\t\t\treturn p\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\t\tif a >= hi {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif probes >= 5 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tprobes++\n\t\t\tp, err := r.findPath(\n\t\t\t\ta, avoid, extra, pen, finalReusePenalty,\n\t\t\t)\n\t\t\tif err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t}\n\t\tif len(extra) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search.\n//\n// Each successive corridor is asked for a TARGET SHARE of the remainder\n// rather than the whole remainder, and a PARTIAL plan is accepted: covering\n// most of a huge payment over several corridors and re-planning the tail on\n// the next call beats refusing to split.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\t// share is what we ask each corridor for: an even split of the\n\t// remainder over the parts we can still afford, which keeps the\n\t// search from burning the widest corridor on an oversized shard.\n\tshare := remaining / lnwire.MilliSatoshi(partsLeft)\n\tif share < minShard {\n\t\tshare = minShard\n\t}\n\n\tfor k := uint32(0); k < partsLeft && k < flowRounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\t// Ask for the even share, except when only a little remains,\n\t\t// where asking for all of it keeps the tail from fragmenting.\n\t\task := share\n\t\tif left < ask || left <= share*2 {\n\t\t\task = left\n\t\t}\n\n\t\tpath := r.findFlowPath(ask, remaining, partsLeft, avoid, extra)\n\t\tif path == nil && ask < left {\n\t\t\t// The share was unroutable; try the full remainder\n\t\t\t// before giving up on this round.\n\t\t\tpath = r.findFlowPath(\n\t\t\t\tleft, remaining, partsLeft, avoid, extra,\n\t\t\t)\n\t\t}\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has the\n\t// most slack under the current assignment. This is what produces the\n\t// deliberately unequal split.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan. Serving a\n// queued shard is the cheapest possible attempt: the corridor was already\n// sized against beliefs, so no probing happens at all.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// rescue is the last-resort descent. When neither the single-shard search\n// nor the flow planner produced anything at the amounts they tried, we walk\n// a fine geometric ladder down to a tiny floor looking for ANY sendable\n// route, as long as at least one more part remains after this one. A small\n// shard both delivers value and buys evidence for the next round, which\n// beats a terminal failure by a wide margin under this objective.\nfunc (r *router) rescue(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\ta := amt / 2\n\tif a < rescueShard {\n\t\ta = rescueShard\n\t}\n\n\tfor i := 0; i < 22 && a >= rescueShard; i++ {\n\t\t// Try the busy-avoiding search first, then the unrestricted\n\t\t// one, at each size.\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif r.failedSigs[planSig(path, a)] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfee := rt.TotalAmount - a\n\t\t\treturn &plan{\n\t\t\t\tpath: path,\n\t\t\t\trt: rt,\n\t\t\t\tamt: a,\n\t\t\t\tprob: p,\n\t\t\t\tscore: r.score(a, p, fee, amt),\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 5\n\t}\n\n\treturn nil\n}\n\n// searchSingle looks for the best single shard, starting from what the\n// network is believed able to carry rather than from the full remainder.\n// It stops early on a confident full-coverage plan, which is where the\n// attempt savings come from.\nfunc (r *router) searchSingle(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *plan {\n\n\t// A shard can never exceed what our own channels can push right now,\n\t// nor what a single one of them (or a single inbound edge of the\n\t// target) can carry. Starting above either guarantees a wasted probe.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif partsLeft > 1 {\n\t\tif ceil := r.singleHopCeiling(); ceil >= minShard &&\n\t\t\tceil < hi {\n\n\t\t\thi = ceil\n\t\t}\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.55 &&\n\t\t\t\ta < best.amt*2/3 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives:\n\t\t\t// send it now and save the attempts.\n\t\t\tif best != nil && best.amt >= amt &&\n\t\t\t\tbest.prob >= confidentProb {\n\n\t\t\t\treturn best\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn best\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches for the best single shard,\n// then compares it against a min-cost-flow style route set over several\n// corridors when the single shard cannot confidently cover the remainder,\n// and finally falls back to a fine descent rather than giving up while\n// parts remain.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\tbest := r.searchSingle(amt, partsLeft, busy)\n\n\t// A confident single shard that covers the whole remainder is the\n\t// cheapest possible outcome: one attempt, one part, no split. Send it\n\t// without consulting the flow planner at all.\n\tconfident := best != nil && best.amt >= amt &&\n\t\tbest.prob >= confidentProb\n\n\t// Otherwise plan the split deliberately over several corridors. This\n\t// fires when a single shard cannot cover the remainder or is unlikely\n\t// enough that an oversized shard would simply fail and be halved.\n\tif partsLeft > 1 && !confident {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it either covers materially more\n\t\t\t// of the remainder or delivers more expected value\n\t\t\t// than the single shard on its own. The margins are\n\t\t\t// wider than before so a marginal flow plan does not\n\t\t\t// trade one attempt for several.\n\t\t\tbetter := best == nil ||\n\t\t\t\ttotal > bestAmt+bestAmt/20 ||\n\t\t\t\tweighted > bestVal*1.12\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil ||\n\t\t\t\t(better && flow[0].score > best.score) {\n\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\t// A very unlikely candidate is a poor use of an attempt when we could\n\t// instead shrink. Try a smaller, safer shard first; only send the weak\n\t// one if nothing better exists.\n\tif best != nil && best.prob < weakProb && partsLeft > 1 &&\n\t\tbest.amt > minShard*4 {\n\n\t\tif alt := r.rescue(best.amt, partsLeft, busy); alt != nil &&\n\t\t\talt.prob > best.prob*1.8 {\n\n\t\t\tbest = alt\n\t\t}\n\t}\n\n\t// Last resort: never return a terminal error while a part remains and\n\t// some small amount is still sendable.\n\tif best == nil {\n\t\tr.dropQueue()\n\t\tbest = r.rescue(amt, partsLeft, busy)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\tr.warm[h.ChannelID] = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n\tb.missCap = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\tb.missCap = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\trb.missCap = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tr.settled++\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tr.warm[h.ChannelID] = true\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned. The soft ceiling is what actually\n\t// forces diversification, since the next attempt over this direction\n\t// must then be materially smaller.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\ta := hopAmount(rt, i)\n\t\t\tif b.missCap == 0 || a < b.missCap {\n\t\t\t\tb.missCap = a\n\t\t\t}\n\t\t\tdelete(r.warm, h.ChannelID)\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a failed probe, so it must not count\n\t\t// against the streak that decides when to give up.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tdelete(r.warm, h.ChannelID)\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tdelete(r.warm, h.ChannelID)\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
},
{
"id": 28,
"parent": 8,
"score": 0.8717,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - COMPLEMENTARY REASONING across the two sides of a channel: liquidity\n// proven to sit on the reverse side cannot also sit on this side, and a\n// reverse-direction failure at a small amount means the reverse side is\n// empty, so under bimodality nearly the whole channel sits on THIS side.\n// - JOINT ROUTE-SET PLANNING with a real flow budget: for a large payment\n// we decompose the remainder over DISJOINT corridors up front, each\n// shard sized to its own corridor's bottleneck, and queue the rest.\n//\n// What changed relative to the previous generation (the observed failure was\n// a huge payment burning all 64 attempts and then reporting \"attempt budget\n// exhausted\", i.e. the retry budget drained on a payment whose split plan\n// never converged):\n//\n// 1. FLOW FEASIBILITY GATE. Before spending attempts we compute the total\n// believed deliverable flow over disjoint corridors. If that aggregate\n// cannot cover the remainder AND we have already confirmed dryness by\n// failing, we give up immediately instead of grinding out 60 doomed\n// probes. Giving up early on an impossible payment costs nothing in\n// success rate and saves the whole retry penalty.\n// 2. COMMITTED PLANS SURVIVE A FAILURE. The old code cleared the entire\n// queued plan on every failure, so a 6-shard decomposition was thrown\n// away the moment one shard missed and was then rediscovered from\n// scratch, one expensive Dijkstra ladder at a time. Now a failure only\n// invalidates queued shards that actually touch the failing direction;\n// the rest are re-validated lazily when served.\n// 3. PER-DIRECTION ATTEMPT LEDGER. Every liquidity failure is charged to a\n// direction, and a direction that has failed maxDryProbes times is\n// excluded from path finding for the remainder of the payment rather\n// than being re-probed at ever smaller amounts. This is the single\n// biggest source of wasted attempts.\n// 4. GLOBAL PROBE BUDGET. Dijkstra runs are accounted against a whole-\n// payment budget, not just a per-call one, so late calls degrade to\n// cheap single-probe behaviour instead of re-running full ladders.\n// 5. SHARD FLOOR TIED TO THE REMAINDER. Shards below remainder/maxParts\n// cannot finish the payment, so planning them just burns parts. The\n// ladder now refuses to go below that floor while parts remain scarce,\n// which both cuts attempts and raises the chance the split closes.\n// 6. POLICY REPAIRS ARE NOT FAILURES. A FeeInsufficient or cltv failure is\n// a stale-gossip repair, not evidence of dryness, so it no longer\n// counts against the give-up streak.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 48\n\tmaxFailStreak = 12\n\n\t// probeBudget caps Dijkstra runs inside one RequestRoute call, and\n\t// totalProbeBudget caps them across the whole payment so that a long\n\t// payment cannot spend unbounded search effort.\n\tprobeBudget = 10\n\ttotalProbeBudget = 220\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we bank on.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before it is excluded for the rest of the payment.\n\tmaxDryProbes = 2\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 14\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting belief-derived budgets for a give-up decision.\n\thopelessStreak = 3\n\n\t// feasStreak is how many consecutive failures we require before the\n\t// aggregate-flow feasibility gate is allowed to abandon a payment.\n\tfeasStreak = 5\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// exhausted reports whether this direction has come up dry so often that\n// probing it again is a waste of an attempt.\nfunc (b *belief) exhausted() bool {\n\treturn b.dead || (b.hasFail && b.fails >= maxDryProbes)\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\n\tattempts int\n\tprobes 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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After maxDryProbes dry probes we stop\n// entirely: grinding the same direction down by halves is the main source of\n// wasted retries.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds. It\n// combines a demonstrated forward (less what we have since drained) with the\n// complementary inference that a reverse-direction failure at a small amount\n// means the funds are on THIS side.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif b != nil && b.exhausted() {\n\t\t\t// This direction has come up dry repeatedly. Probing\n\t\t\t// it again is what drains the attempt budget, so it is\n\t\t\t// retired for the rest of the payment.\n\t\t\tif b.okAmt == 0 {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn b.okAmt\n\t\t}\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// A direction retired after repeated dry probes only clears amounts we\n\t// have actually proven.\n\tif b.exhausted() && eff > b.okAmt {\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.probes >= totalProbeBudget {\n\t\treturn nil, errors.New(\"probe budget exhausted\")\n\t}\n\tr.probes++\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance, which bounds one\n// shard: a shard leaves through exactly one first hop.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// shardFloor is the smallest shard worth planning: anything far below\n// remaining/partsLeft cannot close the payment with the parts we have left,\n// so planning it just burns a part and an attempt.\nfunc (r *router) shardFloor(remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) lnwire.MilliSatoshi {\n\n\tif partsLeft == 0 {\n\t\treturn remaining\n\t}\n\tf := remaining / lnwire.MilliSatoshi(partsLeft)\n\n\t// Leave headroom: a shard under the even split can still be\n\t// compensated by a larger sibling.\n\tf = f / 3\n\tif f < minShard {\n\t\tf = minShard\n\t}\n\treturn f\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent, exact\n// local channel balances, and evidence-derived sizes just below proven\n// failure points. Nothing below the floor is offered.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tfloor := r.shardFloor(remaining, partsLeft)\n\tif floor > hi {\n\t\tfloor = minShard\n\t}\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < floor || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent down to the floor.\n\tcur := hi\n\tfor i := 0; i < 14; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= floor {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= floor {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail && b.fails < maxDryProbes {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= floor {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// touches reports whether a plan's corridor uses the given channel.\nfunc (p *plan) touches(chanID uint64) bool {\n\tfor _, e := range p.path {\n\t\tif e.chanID == chanID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 5 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It keeps going until either the whole remainder is covered or no further\n// disjoint corridor exists, and it caps each shard at the largest single\n// local channel so a shard is never planned larger than one first hop can\n// carry.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free right now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push. Our balances are exact, so this catches payments\n// that no amount of retrying could ever complete, which is where the retry\n// budget used to drain away.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// dropStalePlans discards queued shards whose corridor contains the channel\n// that just failed. The rest of a joint plan is still the best decomposition\n// we know of, so throwing it away wholesale (as an earlier generation did)\n// only forces us to rediscover it one expensive ladder at a time.\nfunc (r *router) dropStalePlans(chanID uint64) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif pl.touches(chanID) {\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// serveQueued hands out the next queued shard that still holds up against\n// current beliefs, re-pricing it at the amount actually remaining.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, split bool,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when the payment cannot be split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor: for every candidate amount (largest first) it finds the best\n// risk-adjusted path, re-prices that path at the amount its weakest hop can\n// bear, and finally checks whether a deliberate multi-corridor split would\n// cover more of the remaining amount than the best single shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tif rt := r.serveQueued(amt, split, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. We\n\t// compare the aggregate believed flow of the plan set against the best\n\t// single shard: covering more of the payment beats a slightly nicer\n\t// first hop, because every uncovered millisat is a failed payment.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\n\t\t\t// FEASIBILITY GATE. Once we have failed enough times to\n\t\t\t// trust our beliefs, a decomposition that still cannot\n\t\t\t// cover the remainder means the payment is not going to\n\t\t\t// complete. Abandoning it here saves dozens of doomed\n\t\t\t// attempts, which is pure retry-efficiency gain with no\n\t\t\t// success-rate cost.\n\t\t\tif r.failStreak >= feasStreak && total < amt &&\n\t\t\t\tinFlightHtlcs == 0 {\n\n\t\t\t\treturn nil, errors.New(\n\t\t\t\t\t\"believed flow cannot cover remainder\",\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t} else if best == nil && inFlightHtlcs == 0 &&\n\t\t\tr.failStreak >= feasStreak {\n\n\t\t\treturn nil, errors.New(\"no corridor left\")\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a descending amount\n// ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable, and it is what\n// feeds the complementary bounds on the reverse direction.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread suspicion over the remote hops so\n\t// we stop re-picking this corridor without destroying the hard bounds\n\t// we have earned. Only queued shards sharing a channel with this\n\t// corridor are invalidated.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\t// Optimism earned from older evidence cannot survive a\n\t\t\t// corridor that just failed somewhere inside it.\n\t\t\tb.succ = false\n\t\t\tr.dropStalePlans(h.ChannelID)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt. A\n\t\t// policy repair is not evidence of dryness, so it must not\n\t\t// count against the give-up streak.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.dropStalePlans(h.ChannelID)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tb.drained = 0\n\n\t// Only the queued shards routing through this now-tightened direction\n\t// are stale; the rest of the joint plan still stands.\n\tr.dropStalePlans(h.ChannelID)\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
},
"role": "best"
},
{
"id": 29,
"parent": 9,
"score": 0.6601,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary (changes over the previous candidate are marked NEW):\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds\n// and no time decay: a stale bound costs one retry to refresh, which\n// is cheaper than decaying evidence.\n// - Balance bookkeeping on settle, plus complementary reasoning across\n// the two sides of a channel (bimodality makes a reverse-side failure\n// a POSITIVE signal for this side).\n// - JOINT ROUTE-SET PLANNING sized per corridor bottleneck.\n//\n// NEW, aimed squarely at the observed failure mode: the two failing\n// scenarios both burned all 64 attempts on ONE very large payment\n// (~0.86 and ~1.04 BTC) whose attempts kept coming back with\n// failed_at_hop = -1, i.e. UNATTRIBUTABLE failures on 4 hop routes. The\n// old code responded to those by bumping a \"misses\" counter and then\n// re-deriving nearly the same corridor, so it looped. Fixes:\n//\n// 1. UNATTRIBUTED FAILURES NOW COST THE CORRIDOR, NOT JUST A COUNTER.\n// The corridor signature is banned for the rest of the payment at\n// that amount or above, and each remote hop gets a real upper bound\n// at the amount it was asked to carry (softened, since we do not know\n// which hop was at fault). That guarantees forward progress: every\n// unattributed failure strictly shrinks the search space.\n// 2. AMOUNT-INDEXED CORRIDOR BANS. A corridor banned at amount a is\n// still legal below a*banRelax, so we descend deliberately instead of\n// re-offering the same doomed attempt.\n// 3. PARALLEL-FIRST BIG PAYMENTS. When the remainder exceeds what any\n// single local channel can push, we no longer waste an attempt on a\n// single-corridor probe: we go straight to flow planning, and the\n// plan is rebuilt after each failure so shard sizes track the newest\n// beliefs.\n// 4. ATTEMPT BUDGET IS SPENT ON DISTINCT CORRIDORS. A per-payment cap on\n// how many attempts may leave through the same first hop stops one\n// fat local channel from absorbing the whole budget.\n// 5. SALVAGE STAYS AVAILABLE: a settled shard shrinks the remainder and\n// buys fresh evidence, so partial delivery is always progress.\n//\n// Non-liquidity failures (fee, cltv, disabled) are repaired in the local\n// policy view instead of being treated as liquidity misses.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 64\n\tmaxFailStreak = 24\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we bank on.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before we stop probing it lower for this payment.\n\tmaxDryProbes = 3\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 14\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting the belief-derived local budget for a give-up decision.\n\thopelessStreak = 5\n\n\t// banRelaxNum/banRelaxDen: a corridor banned at amount a may be tried\n\t// again only strictly below a*banRelax. This turns a ban into a\n\t// monotone descent rather than a permanent blacklist.\n\tbanRelaxNum = 55\n\tbanRelaxDen = 100\n\n\t// unattribPenalty softens the upper bound we charge to each remote hop\n\t// of a corridor that failed without naming a culprit: any one of them\n\t// may be the guilty party, so we do not fully believe the bound.\n\tunattribPenaltyNum = 115\n\tunattribPenaltyDen = 100\n\n\t// firstHopAttemptCap limits how many attempts of a single payment may\n\t// leave through the same local channel, so one channel cannot absorb\n\t// the whole budget.\n\tfirstHopAttemptCap = 12\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan.\n\tqueued []*plan\n\n\t// banned maps a corridor signature to the smallest amount at which it\n\t// is known to have failed. The corridor may be retried only strictly\n\t// below that amount times banRelax.\n\tbanned map[string]lnwire.MilliSatoshi\n\n\t// firstHopUse counts attempts per local channel this payment.\n\tfirstHopUse map[uint64]int\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tbanned: make(map[string]lnwire.MilliSatoshi),\n\t\tfirstHopUse: make(map[uint64]int),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when unknown.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After several dry probes we stop entirely.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.from == r.source &&\n\t\t\t\tr.firstHopUse[e.chanID] >= firstHopAttemptCap {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Corridor bans --------------------------------------------------------\n\n// corridorSig is the channel sequence of a path, ignoring amount.\nfunc corridorSig(path []*edge) string {\n\tbuf := make([]byte, 0, len(path)*14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\n// routeCorridorSig is corridorSig for an already built route.\nfunc routeCorridorSig(rt *route.Route) string {\n\tbuf := make([]byte, 0, len(rt.Hops)*14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\n// banCorridor records that a corridor failed at amt, so it may only be\n// retried well below that amount.\nfunc (r *router) banCorridor(sig string, amt lnwire.MilliSatoshi) {\n\tif sig == \"\" {\n\t\treturn\n\t}\n\tif cur, ok := r.banned[sig]; !ok || amt < cur {\n\t\tr.banned[sig] = amt\n\t}\n}\n\n// corridorAllowed reports whether a corridor may still be tried at amt.\nfunc (r *router) corridorAllowed(sig string, amt lnwire.MilliSatoshi) bool {\n\tlim, ok := r.banned[sig]\n\tif !ok {\n\t\treturn true\n\t}\n\treturn amt < lim*banRelaxNum/banRelaxDen\n}\n\nfunc (r *router) pathAllowed(path []*edge, amt lnwire.MilliSatoshi) bool {\n\treturn r.corridorAllowed(corridorSig(path), amt)\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 6 && r.mppOK() {\n\t\tmaxK = 6\n\t}\n\tif maxK > 12 {\n\t\tmaxK = 12\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur * 2 / 3\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// still-legal attempt it can carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 16 && a >= minShard; i++ {\n\t\tif r.pathAllowed(path, a) {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard || !r.pathAllowed(path, c) {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil &&\n\t\tr.pathAllowed(p, hi) {\n\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 8 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tp, err := r.findPath(a, avoid)\n\t\tif err == nil && r.pathAllowed(p, a) {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything legal, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// bigPayment reports whether the remainder is beyond any single local\n// channel, in which case a single-corridor search can only ever waste an\n// attempt and we should plan a parallel decomposition immediately.\nfunc (r *router) bigPayment(amt lnwire.MilliSatoshi) bool {\n\tif !r.mppOK() {\n\t\treturn false\n\t}\n\tsingle := r.maxLocalEdge()\n\treturn single > 0 && amt > single\n}\n\n// serveQueued hands out the next queued shard of a joint plan that still\n// holds up against current beliefs.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif !r.pathAllowed(pl.path, a) {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\tif rt := r.serveQueued(amt, busy, split); rt != nil {\n\t\tr.attempts++\n\t\tr.noteFirstHop(rt)\n\t\treturn rt, nil\n\t}\n\n\t// Parallel-first: a remainder larger than any single local channel can\n\t// only be delivered by several shards, so plan the decomposition up\n\t// front rather than burning an attempt on a doomed single corridor.\n\tif r.bigPayment(amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tr.queued = flow[1:]\n\t\t\tr.attempts++\n\t\t\tr.noteFirstHop(flow[0].rt)\n\t\t\treturn flow[0].rt, nil\n\t\t}\n\t}\n\n\t// A single shard can never exceed what one local channel can push.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.noteFirstHop(best.rt)\n\treturn best.rt, nil\n}\n\n// noteFirstHop charges an attempt to the local channel it leaves through.\nfunc (r *router) noteFirstHop(rt *route.Route) {\n\tif rt == nil || len(rt.Hops) == 0 {\n\t\treturn\n\t}\n\tr.firstHopUse[rt.Hops[0].ChannelID]++\n}\n\n// salvage hunts for any legal attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif !r.pathAllowed(path, remaining) {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 24 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n}\n\n// tighten records an upper bound on a direction from a liquidity miss.\nfunc (r *router) tighten(k edgeKey, a lnwire.MilliSatoshi, hard bool) {\n\tb := r.bel(k)\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tif hard {\n\t\tb.fails++\n\t\tb.succ = false\n\t\tb.drained = 0\n\t}\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable, and it is what\n// feeds the complementary bounds on the reverse direction.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// A settled shard changes every belief the plan rested on.\n\t\tr.queued = nil\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure. The old code merely bumped a suspicion\n\t// counter, which let the planner re-derive nearly the same corridor\n\t// forever: that is exactly how the two failing scenarios burned 64\n\t// attempts. Now the corridor is banned at this amount and every remote\n\t// hop takes a real (softened) upper bound, so the search space\n\t// strictly shrinks with each such failure.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tlast := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\tr.banCorridor(routeCorridorSig(rt), last)\n\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta := hopAmount(rt, i)\n\t\t\tsoft := a * unattribPenaltyNum / unattribPenaltyDen\n\t\t\tr.tighten(hopKey(h), soft, false)\n\t\t\tr.bel(hopKey(h)).succ = false\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tr.bel(hopKey(h)).dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction, and ban\n\t// the corridor at the delivered amount so we descend rather than\n\t// re-offer it.\n\tr.tighten(hopKey(h), a, true)\n\tr.banCorridor(\n\t\trouteCorridorSig(rt), rt.Hops[len(rt.Hops)-1].AmtToForward,\n\t)\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 30,
"parent": 10,
"score": 0.0,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary (changes over the previous candidate are marked NEW):\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds\n// and no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than decaying evidence.\n// - Balance bookkeeping on settle plus complementary reasoning across the\n// two sides of a channel (a dry reverse side means this side is full).\n// - NEW: PERSISTENT PARALLEL PLAN. The big observed failure was a large\n// payment (2.06 Gmsat) burning 20 attempts and dying on \"no progress\"\n// while every failure was a plain TemporaryChannelFailure. The root\n// cause was that the joint flow plan was thrown away on EVERY failure\n// (r.queued = nil), so the router collapsed back to single-corridor\n// probing and never actually held several unequal shards in flight.\n// Now the queue survives failures: only the shards whose corridors are\n// directly contradicted by new evidence are dropped, and the rest are\n// re-priced lazily when handed out.\n// - NEW: CONCURRENCY-FIRST DISPATCH. When the remainder cannot fit in a\n// single corridor and parts are free, we deliberately hand out the\n// largest believable shard immediately rather than searching the whole\n// amount ladder first. Filling MaxParts is what converts a big payment\n// into a success; ladder search only helps once concurrency is spent.\n// - NEW: RESIDUAL-AWARE FLOW PLANNING. planFlow now runs a proper\n// residual pass: each corridor's shard is capped by the min of its\n// bottleneck and the free local balance of its first hop, and local\n// first-hop budget is DECREMENTED as shards are planned, so two shards\n// never over-commit the same local channel. Previously two planned\n// shards could both be sized to the same local channel's balance.\n// - NEW: ADAPTIVE FAIL BUDGET. maxFailStreak scales with how much of the\n// payment still needs covering and how many parts we may use, so a\n// multi-shard payment is not killed by a streak that a single-shard\n// payment would deserve. Small payments still give up fast, so retry\n// efficiency on easy scenarios is preserved.\n// - NEW: PROGRESS-AWARE STREAK RESET. Any hop that demonstrably forwarded\n// (a failure deeper in the route than hop 0) counts as partial\n// progress and softens the streak, because we did learn something\n// actionable.\n// - Depth-aware lower retries with a per-direction dry-probe counter.\n// - Hopeless-payment detection from exactly known local balances.\n// - Duplicate-attempt suppression, and non-liquidity failures (fee, cltv,\n// min, disabled) repaired in the local policy view.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(6_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts bounds total attempts for one payment.\n\tmaxAttempts = 90\n\n\t// baseFailStreak is the streak budget for a payment we believe one\n\t// corridor should be able to carry. Payments needing many shards get\n\t// a proportionally larger budget, see failBudget.\n\tbaseFailStreak = 10\n\n\t// maxFailStreakCap is the absolute ceiling on the streak budget.\n\tmaxFailStreakCap = 34\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 5.0\n\tpartCost = 0.004\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we bank on.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before we stop probing it lower for this payment.\n\tmaxDryProbes = 3\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.12\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 14\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting the belief-derived local budget for a give-up decision.\n\thopelessStreak = 6\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan. It is\n\t// NOT discarded on failure: shards whose corridors are contradicted by\n\t// new evidence are pruned individually and the rest are re-priced when\n\t// handed out.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// failStreak counts consecutive unproductive failures.\n\tfailStreak int\n\n\t// attempts counts every route we handed out.\n\tattempts int\n\n\t// delivered is how much this payment has settled so far, used to size\n\t// the fail budget.\n\tdelivered lnwire.MilliSatoshi\n\n\t// firstAmt is the amount of the very first RequestRoute call, i.e. the\n\t// full payment amount.\n\tfirstAmt lnwire.MilliSatoshi\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 := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// failBudget is how many consecutive failures we tolerate. A payment that\n// provably needs several shards deserves more probing than one that should\n// fit in a single corridor: each shard costs at least one attempt to place,\n// and a shard that fails costs one more to resize. Small payments keep a\n// tight budget so retry efficiency on easy scenarios does not regress.\nfunc (r *router) failBudget() int {\n\tbudget := baseFailStreak\n\tif !r.mppOK() {\n\t\treturn budget\n\t}\n\n\t// Estimate the number of shards the payment needs from the largest\n\t// single local channel we could push out of.\n\tshards := 1\n\tif single := r.rawMaxLocal(); single > 0 && r.firstAmt > single {\n\t\tshards = int(r.firstAmt/single) + 1\n\t}\n\tif mp := int(r.spec.MaxParts); shards > mp && mp > 0 {\n\t\tshards = mp\n\t}\n\tif shards > 1 {\n\t\tbudget += 5 * (shards - 1)\n\t}\n\tif budget > maxFailStreakCap {\n\t\tbudget = maxFailStreakCap\n\t}\n\treturn budget\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After several dry probes we stop entirely.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance, which bounds one\n// shard: a shard leaves through exactly one first hop.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// freeLocal is the free capacity of the local channel a corridor leaves\n// through, or a very large number when the corridor's first hop is not one\n// of ours (which cannot happen for a valid route).\nfunc (r *router) freeLocal(path []*edge) lnwire.MilliSatoshi {\n\tif len(path) == 0 {\n\t\treturn 0\n\t}\n\treturn r.availCap(path[0])\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\n//\n// It runs a residual pass: local first-hop budget is decremented as shards\n// are planned, so two shards can never over-commit the same local channel,\n// and each shard is capped by the min of its corridor bottleneck and its\n// first hop's remaining free balance.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// Residual local budget per first-hop channel.\n\tresidual := make(map[uint64]lnwire.MilliSatoshi, len(r.localEdges))\n\tfor _, e := range r.localEdges {\n\t\tif _, ok := residual[e.chanID]; !ok {\n\t\t\tresidual[e.chanID] = r.availCap(e)\n\t\t}\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free right now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\t// The largest amount any still-usable local channel can push.\n\t\tvar single lnwire.MilliSatoshi\n\t\tfor _, c := range residual {\n\t\t\tif c > single {\n\t\t\t\tsingle = c\n\t\t\t}\n\t\t}\n\t\tif single < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif fl := residual[path[0].chanID]; fl > 0 && fl < amtS {\n\t\t\tamtS = fl\n\t\t}\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// This corridor cannot bear a useful shard, so retire\n\t\t\t// its first hop for this decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tresidual[path[0].chanID] = 0\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\n\t\t// Charge the shard against its first hop's residual balance and\n\t\t// retire every channel it uses from this decomposition.\n\t\tfh := path[0].chanID\n\t\tif residual[fh] > pl.amt {\n\t\t\tresidual[fh] -= pl.amt\n\t\t} else {\n\t\t\tresidual[fh] = 0\n\t\t}\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// pathUses reports whether a corridor traverses the given channel.\nfunc pathUses(path []*edge, chanID uint64) bool {\n\tfor _, e := range path {\n\t\tif e.chanID == chanID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// pruneQueue drops the queued shards that a fresh failure on chanID\n// contradicts, and keeps the rest. Holding on to the plan across failures is\n// what lets a multi-shard payment actually fill its concurrency instead of\n// collapsing back to single-corridor probing after every miss.\nfunc (r *router) pruneQueue(chanID uint64) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif pathUses(pl.path, chanID) {\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// serveQueued hands out the first still-valid shard of a joint plan. Shards\n// are re-priced against current beliefs, and a shard whose corridor no\n// longer holds up is dropped rather than aborting the whole plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, split bool,\n\tbusy map[uint64]bool) *plan {\n\n\tvar deferred []*plan\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when the payment cannot be split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\t// The corridor is temporarily occupied, not wrong:\n\t\t\t// keep it for a later call.\n\t\t\tdeferred = append(deferred, pl)\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\t// Re-price the corridor a little lower rather than\n\t\t\t// discarding a corridor we deliberately chose.\n\t\t\tif lower := r.bestOnPath(pl.path, a-a/8, amt); lower !=\n\t\t\t\tnil && lower.prob >= queueMinProb {\n\n\t\t\t\tr.queued = append(deferred, r.queued...)\n\t\t\t\treturn lower\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\t// Beliefs moved against this corridor: try a smaller\n\t\t\t// amount on it before giving it up.\n\t\t\tif lower := r.bestOnPath(pl.path, a*3/4, amt); lower !=\n\t\t\t\tnil && lower.prob >= queueMinProb {\n\n\t\t\t\tr.queued = append(deferred, r.queued...)\n\t\t\t\treturn lower\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tr.queued = append(deferred, r.queued...)\n\t\treturn &plan{path: pl.path, rt: rt, amt: a, prob: p}\n\t}\n\n\tr.queued = deferred\n\treturn nil\n}\n\n// RequestRoute plans the next shard.\n//\n// Order of play:\n// 1. Serve a shard left over from a joint route-set plan, re-priced.\n// 2. When the remainder provably cannot fit through one local channel and\n// parts are still free, plan the whole decomposition up front and hand\n// out its first shard immediately. Filling concurrency with correctly\n// sized unequal shards is what turns a large payment into a success.\n// 3. Otherwise search jointly over shard amount and corridor.\n// 4. Fall back to a deliberate multi-corridor split, then to salvage.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.firstAmt == 0 {\n\t\tr.firstAmt = amt\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= r.failBudget() {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// Step 1: serve a queued shard from an earlier joint plan.\n\tif pl := r.serveQueued(amt, split, busy); pl != nil {\n\t\tr.attempts++\n\t\treturn pl.rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\tsingle := r.maxLocalEdge()\n\thi := amt\n\tif single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// Step 2: concurrency-first dispatch. When the remainder provably\n\t// exceeds any one local channel, ladder search over a single corridor\n\t// cannot succeed, so plan the whole decomposition now.\n\tif split && partsLeft > 1 && single > 0 && amt > single {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tr.queued = flow[1:]\n\t\t\tr.attempts++\n\t\t\treturn flow[0].rt, nil\n\t\t}\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Step 4: when no single corridor carries the whole remainder, plan the\n\t// split deliberately over disjoint corridors instead of halving\n\t// blindly. We compare the aggregate believed flow of the plan set\n\t// against the best single shard: covering more of the payment beats a\n\t// slightly nicer first hop, because every uncovered millisat is a\n\t// failed payment.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif r.failedSigs[planSig(path, remaining)] {\n\t\t\treturn nil\n\t\t}\n\t\trt, p, err := r.makeRoute(path, remaining)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &plan{path: path, rt: rt, amt: remaining, prob: p}\n\t}\n\n\ta := remaining\n\tif single := r.maxLocalEdge(); single >= minShard && single < a {\n\t\ta = single\n\t}\n\tfor i := 0; i < 22 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tif n := len(rt.Hops); n > 0 {\n\t\t\tr.delivered += rt.Hops[n-1].AmtToForward\n\t\t}\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// A failure deep in the route means the earlier hops demonstrably\n\t// forwarded, which is real progress: we learned usable bounds. Charge\n\t// such a failure at half weight so a large multi-shard payment is not\n\t// killed by informative misses.\n\tif failIdx > 1 {\n\t\tr.failStreak++\n\t\tif r.failStreak > 0 && r.attempts%2 == 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t} else {\n\t\tr.failStreak++\n\t}\n\n\t// An unattributable failure: spread suspicion over the remote hops so\n\t// we stop re-picking this corridor without destroying the hard bounds\n\t// we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\t// Optimism earned from older evidence cannot survive a\n\t\t\t// corridor that just failed somewhere inside it.\n\t\t\tb.succ = false\n\t\t\tr.pruneQueue(h.ChannelID)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt. A fee\n\t\t// repair is not a liquidity miss, so it does not count against\n\t\t// the streak and the queued plan stays valid.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tif r.failStreak > 0 {\n\t\t\tr.failStreak--\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(h.ChannelID)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tb.drained = 0\n\n\t// Only the shards routed through the failing channel are invalidated:\n\t// the rest of a joint plan is still the best decomposition we have, and\n\t// throwing it away is what previously collapsed large payments back\n\t// into single-corridor grinding.\n\tr.pruneQueue(h.ChannelID)\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 31,
"parent": 4,
"score": 0.0,
"accepted": true,
"frontier": true,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (keyed by channel plus direction) with\n// hard lower-OK / upper-fail bounds and no time decay: a stale bound\n// costs one retry to refresh, which is cheaper than decaying evidence.\n// - BALANCE BOOKKEEPING on settle: a forward that settles moves liquidity,\n// so the forward direction's bounds are decremented and the reverse\n// direction's bounds are incremented by exactly the amount forwarded.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style): shard sizes and\n// corridors are chosen TOGETHER by a greedy flow decomposition over\n// disjoint corridors, each shard sized to that corridor's believed\n// bottleneck.\n// - CAPACITY-AWARE ADMISSION: before spending attempts on a large payment,\n// the believed max-flow from source to target is measured. If the whole\n// remainder plainly cannot flow, the router switches to \"harvest mode\":\n// carve the biggest shards the network can actually bear rather than\n// repeatedly probing at amounts the evidence already rules out. This is\n// what was burning 26 attempts on the 1.04 BTC payment before it died\n// with \"no route found\".\n// - ANTI-THRASH PROBE LEDGER: every (corridor, amount) probe is remembered\n// along with a monotone per-corridor ceiling. Once a corridor has failed\n// at amount a, we never probe it above a again, and the next probe on it\n// must be materially smaller (a geometric step), so a corridor can be\n// refined at most a handful of times. This directly cuts the retry\n// counts that dominate the retry_efficiency penalty.\n// - EARLY COMMIT ON CONFIDENCE: when the best plan is a full-amount route\n// with high believed probability, planning stops immediately instead of\n// walking the whole ladder, which trims attempts and Dijkstra work on\n// the easy scenarios that already succeed.\n// - Depth-aware lower retries and retry-at-lower-amount instead of\n// permanent blacklisting.\n// - Non-liquidity failures (fee, cltv, min-htlc, disabled) are repaired in\n// the local policy view instead of being charged to liquidity beliefs.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying. The\n\t// streak is generous because a large multi-part payment legitimately\n\t// spends several probes learning the shape of the network, but the\n\t// probe ledger below keeps those probes from repeating themselves.\n\tmaxAttempts = 64\n\tmaxFailStreak = 22\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 12\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the greedy flow decomposition.\n\tflowRounds = 14\n\n\t// corridorStep is the geometric shrink a corridor must take between\n\t// successive probes. Without it, a corridor gets re-probed at nearly\n\t// the same amount over and over, which is pure attempt waste.\n\tcorridorStep = 0.62\n\n\t// commitProb is the probability above which a full-amount plan is\n\t// taken immediately with no further search.\n\tcommitProb = 0.72\n\n\t// harvestFrac is the fraction of believed max-flow we are willing to\n\t// ask for in one shard when the full remainder is known infeasible.\n\tharvestFrac = 0.9\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// corridorCap is a monotone per-corridor ceiling: the largest amount\n\t// we are still willing to try over a given channel sequence. Every\n\t// failure on the corridor ratchets it down geometrically, so a\n\t// corridor can be refined only a few times before it drops out.\n\tcorridorCap map[string]lnwire.MilliSatoshi\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\n\t\tcorridorCap: make(map[string]lnwire.MilliSatoshi),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Corridor ledger ------------------------------------------------------\n\n// corridorSig names a channel sequence, independent of amount.\nfunc corridorSig(path []*edge) string {\n\tbuf := make([]byte, 0, len(path)*14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\n// routeCorridorSig is corridorSig for an already-built route.\nfunc routeCorridorSig(rt *route.Route) string {\n\tbuf := make([]byte, 0, len(rt.Hops)*14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\treturn string(buf)\n}\n\n// corridorCeil is the largest amount we are still willing to send over a\n// corridor, given how it has failed before. Zero means \"no ceiling yet\".\nfunc (r *router) corridorCeil(path []*edge) lnwire.MilliSatoshi {\n\treturn r.corridorCap[corridorSig(path)]\n}\n\n// noteCorridorFail ratchets a corridor's ceiling down. The next probe on the\n// same channel sequence must be materially smaller, which stops the router\n// from grinding out near-identical retries.\nfunc (r *router) noteCorridorFail(sig string, amt lnwire.MilliSatoshi) {\n\tnext := lnwire.MilliSatoshi(float64(amt) * corridorStep)\n\tif cur, ok := r.corridorCap[sig]; ok && cur < next {\n\t\treturn\n\t}\n\tr.corridorCap[sig] = next\n}\n\n// clampCorridor caps a candidate amount by the corridor's ledger ceiling.\nfunc (r *router) clampCorridor(path []*edge,\n\ta lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif ceil := r.corridorCeil(path); ceil > 0 && a > ceil {\n\t\treturn ceil\n\t}\n\treturn a\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tbn -= bn / 100\n\treturn r.clampCorridor(path, bn)\n}\n\n// hardBottleneck is the corridor's absolute believed ceiling, used by the\n// feasibility gate: it ignores the conservative safeCap shading and asks\n// only whether liquidity could plausibly flow at all.\nfunc (r *router) hardBottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.availCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn r.clampCorridor(path, bn)\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK > 8 {\n\t\tmaxK = 8\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n}\n\n// build prices a corridor at an amount, respecting the corridor ledger and\n// the duplicate-attempt suppression table.\nfunc (r *router) build(path []*edge, a,\n\tremaining lnwire.MilliSatoshi) *plan {\n\n\ta = r.clampCorridor(path, a)\n\tif a < minShard {\n\t\treturn nil\n\t}\n\tif r.failedSigs[planSig(path, a)] {\n\t\treturn nil\n\t}\n\trt, p, err := r.makeRoute(path, a)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfee := rt.TotalAmount - a\n\treturn &plan{\n\t\tpath: path,\n\t\trt: rt,\n\t\tamt: a,\n\t\tprob: p,\n\t\tscore: r.score(a, p, fee, remaining),\n\t}\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tpl := r.build(path, c, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif best == nil || pl.score > best.score {\n\t\t\tbest = pl\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := r.clampCorridor(path, hi)\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif pl := r.build(path, a, remaining); pl != nil {\n\t\t\treturn pl\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 8 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tvar out []*plan\n\tleft := remaining\n\trounds := int(partsLeft)\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := r.findAnyPath(left, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := left\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\t// The corridor is too thin to be worth a shard, but it\n\t\t\t// is still worth excluding so the next round finds\n\t\t\t// something else.\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\tfor _, e := range path {\n\t\t\t\tavoid[e.chanID] = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\t// Best shards first: the largest, most reliable corridor should be\n\t// tried before the marginal ones.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].score > out[j].score\n\t})\n\n\treturn out\n}\n\n// believedFlow is the total delivered amount our believed liquidity could\n// carry over up to `rounds` disjoint corridors. It answers two questions at\n// once: whether to keep going at all, and \u2014 when the full remainder is out\n// of reach \u2014 how big a shard is actually worth asking for. Sizing to this\n// number instead of blindly halving is what stops a big payment from\n// grinding through a dozen doomed probes.\nfunc (r *router) believedFlow(remaining lnwire.MilliSatoshi,\n\trounds int, busy map[uint64]bool) lnwire.MilliSatoshi {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\tvar total lnwire.MilliSatoshi\n\n\tfor k := 0; k < rounds; k++ {\n\t\tleft := remaining - total\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\t\tpath, err := r.findPath(left, avoid)\n\t\tif err != nil {\n\t\t\tpath = r.findAnyPath(left, remaining, 4, avoid)\n\t\t\tif path == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tbn := r.hardBottleneck(path)\n\t\tif bn > left {\n\t\t\tbn = left\n\t\t}\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\t\tif bn < minShard {\n\t\t\tcontinue\n\t\t}\n\t\ttotal += bn\n\t}\n\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// pruneQueue drops only the queued shards whose corridors touch a channel\n// whose belief just got worse. Discarding the entire joint plan on every\n// failure throws away correct planning work and forces the router to\n// rediscover the same split by trial and error.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tif len(r.queued) == 0 {\n\t\treturn\n\t}\n\tkeep := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tdrop := false\n\t\tfor _, e := range pl.path {\n\t\t\tif hit[e.chanID] {\n\t\t\t\tdrop = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !drop {\n\t\t\tkeep = append(keep, pl)\n\t\t}\n\t}\n\tr.queued = keep\n}\n\n// serveQueued hands out the next still-valid shard of a joint plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.queued = nil\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tnpl := r.build(pl.path, a, amt)\n\t\tif npl == nil || npl.prob < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\t\treturn npl.rt\n\t}\n\treturn nil\n}\n\n// RequestRoute plans the next shard.\n//\n// The call proceeds in four stages, cheapest first:\n// 1. serve a still-valid shard from an earlier joint plan;\n// 2. search corridors at descending amounts, committing early when a\n// full-amount plan looks confident;\n// 3. when no single corridor covers the remainder, plan a deliberate\n// multi-corridor split;\n// 4. when even that fails, measure the believed max-flow and harvest the\n// largest shard the network can plausibly bear, rather than declaring\n// the payment dead while liquidity clearly remains.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// Capacity-aware admission: if the believed max-flow plainly cannot\n\t// cover the remainder, do not waste probes at the full amount. Cap the\n\t// search at what the network is believed able to bear.\n\tvar flowCap lnwire.MilliSatoshi\n\tif partsLeft > 1 {\n\t\tflowCap = r.believedFlow(amt, flowRounds, busy)\n\t\tif flowCap > 0 && flowCap < hi {\n\t\t\t// Only shrink the search when the shortfall is real,\n\t\t\t// not a rounding artifact.\n\t\t\tif float64(flowCap) < 0.95*float64(hi) {\n\t\t\t\thi = flowCap\n\t\t\t}\n\t\t}\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives:\n\t\t\t// commit right away and save both search work and the\n\t\t\t// risk of talking ourselves into a needless split.\n\t\t\tif best != nil && best.amt >= amt &&\n\t\t\t\tbest.prob > commitProb {\n\n\t\t\t\tr.attempts++\n\t\t\t\treturn best.rt, nil\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly. The\n\t// plan is accepted when its corridors jointly cover meaningfully more\n\t// of the remainder than the single best shard would.\n\tif partsLeft > 1 && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\tvar total lnwire.MilliSatoshi\n\t\t\tfor _, pl := range flow {\n\t\t\t\ttotal += pl.amt\n\t\t\t}\n\n\t\t\tfirst := flow[0]\n\t\t\tbetter := best == nil ||\n\t\t\t\t(total > best.amt &&\n\t\t\t\t\tfirst.score > best.score*0.75)\n\n\t\t\tif better {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\tif best != nil {\n\t\tr.attempts++\n\t\treturn best.rt, nil\n\t}\n\n\t// Harvest mode. Conservative sizing found nothing, but the believed\n\t// max-flow may still be positive. Instead of walking the whole ladder\n\t// blind, size the request directly to the flow the network is believed\n\t// to support and take the biggest novel attempt we can build.\n\tflow := r.believedFlow(amt, flowRounds, busy)\n\tif flow >= minShard {\n\t\tstart := lnwire.MilliSatoshi(float64(flow) * harvestFrac)\n\t\tif start > amt {\n\t\t\tstart = amt\n\t\t}\n\t\tif start < minShard {\n\t\t\tstart = minShard\n\t\t}\n\n\t\tcands := []lnwire.MilliSatoshi{start}\n\t\tfor _, a := range r.ladder(start, amt, partsLeft) {\n\t\t\tif a < start {\n\t\t\t\tcands = append(cands, a)\n\t\t\t}\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range cands {\n\t\t\tif probes >= probeBudget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tprobes++\n\t\t\tpath, err := r.findPath(a, busy)\n\t\t\tif err != nil {\n\t\t\t\tpath, err = r.findPath(a, nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tpl := r.bestOnPath(path, a, amt)\n\t\t\tif pl == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.attempts++\n\t\t\treturn pl.rt, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A corridor that just settled drained itself, so its ledger\n\t\t// ceiling drops to what it actually carried. Later shards may\n\t\t// still reuse it, just not at a larger size.\n\t\tif n := len(rt.Hops); n > 0 {\n\t\t\tsig := routeCorridorSig(rt)\n\t\t\tdelivered := rt.Hops[n-1].AmtToForward\n\t\t\tcur, ok := r.corridorCap[sig]\n\t\t\tif !ok || cur > delivered {\n\t\t\t\tr.corridorCap[sig] = delivered\n\t\t\t}\n\t\t}\n\n\t\t// A settled shard drained the corridors it used, so any queued\n\t\t// shard sharing those channels must be re-planned.\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor _, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tkind := \"liquidity\"\n\tif failIdx >= 0 && failIdx < len(rt.Hops) {\n\t\tkind = classify(result.Failure)\n\t}\n\n\t// Policy repairs say nothing about liquidity, so the corridor ledger\n\t// stays untouched for them. Every other failure ratchets the corridor\n\t// down so the next probe on it is materially smaller.\n\tif n := len(rt.Hops); n > 0 && kind != \"fee\" && kind != \"cltv\" {\n\t\tr.noteCorridorFail(\n\t\t\trouteCorridorSig(rt), rt.Hops[n-1].AmtToForward,\n\t\t)\n\t}\n\n\t// An unattributable failure: spread mild suspicion over the remote\n\t// hops so we stop re-picking this corridor without destroying the\n\t// hard bounds we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\thit[h.ChannelID] = true\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tswitch kind {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// fee repair is not evidence against any corridor, so queued\n\t\t// shards survive untouched.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\t// Only the queued corridors that actually depend on the condemned\n\t// channel are invalidated; the rest of the joint plan stays valid.\n\tr.pruneQueue(map[uint64]bool{h.ChannelID: true})\n\n\treturn nil\n}"
}
},
{
"id": 32,
"parent": 11,
"score": 0.7868,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary (success is already saturated at 100% in the current\n// scenarios, so this revision keeps every success-preserving mechanism and\n// attacks the two secondary terms: attempts per payment and fee ppm):\n//\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs with hard lower-OK / upper-fail bounds and\n// no time decay: a stale bound costs one retry to refresh, which is\n// cheaper than decaying evidence (exp-008).\n// - BALANCE BOOKKEEPING on settle plus COMPLEMENTARY REASONING across the\n// two sides of a channel: proven reverse liquidity is a hard upper bound\n// here, and a reverse-direction failure at a small amount is a POSITIVE\n// signal for this direction under bimodality.\n// - JOINT ROUTE-SET PLANNING (min-cost-flow style) with a local-channel\n// flow cap, so a payment larger than any single local channel is\n// decomposed up front into unequal shards sized to each corridor's\n// bottleneck instead of discovered by halving on failure.\n//\n// New in this revision, all aimed at attempts and fees:\n//\n// - FIRST-SHOT SIZING: the amount ladder is no longer walked from the top\n// blindly. We compute a believed max-flow ceiling over disjoint\n// corridors and start the ladder at min(remaining, ceiling), which skips\n// the doomed full-amount probe that used to burn the first attempt of\n// nearly every large payment.\n// - PESSIMISTIC-FIRST SHARD SIZING: a corridor is priced at the amount its\n// weakest hop is BELIEVED to bear before it is priced at the amount we\n// wish it would bear, so the very first attempt on an unexplored\n// corridor is already sized to survive.\n// - SUCCESS-ANCHORED CORRIDOR MEMORY: corridors that have settled are\n// remembered and re-tried FIRST at up to their proven amount. Repeat\n// payments in a batch then settle in one attempt on a known-good\n// corridor rather than re-deriving it, which cuts both attempts and fees.\n// - FEE AS A REAL TERM: once a plan clears a high probability bar we\n// enumerate cheaper alternatives on the same amount and keep the cheapest\n// that stays above the bar. Fee ppm was the only remaining penalty on\n// fully-successful runs.\n// - EARLY EXIT everywhere: a confident plan short-circuits the probe loop,\n// and the probe budget shrinks once a good plan is in hand.\n// - Non-liquidity failures (fee, cltv, min htlc, disabled) are repaired in\n// the local policy view rather than charged to liquidity, and a fee\n// repair is retried immediately on the SAME corridor.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_200)\n\tattemptCostPPM = lnwire.MilliSatoshi(5_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless.\n\tmaxAttempts = 64\n\tmaxFailStreak = 18\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass, and probeBudgetGood is the tighter cap that\n\t// applies once we already hold a workable plan.\n\tprobeBudget = 12\n\tprobeBudgetGood = 4\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 6.0\n\tpartCost = 0.004\n\n\t// confidentProb is the probability at which we stop looking for a\n\t// better plan and just send.\n\tconfidentProb = 0.80\n\n\t// cheapenProb is the probability floor a cheaper alternative must\n\t// still clear to be preferred over a more expensive plan.\n\tcheapenProb = 0.72\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear. Under the bimodal prior this is\n\t// close to the amount maximizing expected delivered value.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// compSlackNum/compSlackDen discounts the complementary upper bound\n\t// derived from the reverse direction's proven liquidity.\n\tcompSlackNum = 90\n\tcompSlackDen = 100\n\n\t// revEmptyNum/revEmptyDen is the fraction of the liquidity implied on\n\t// this side by a reverse-direction failure that we bank on.\n\trevEmptyNum = 80\n\trevEmptyDen = 100\n\n\t// maxDryProbes is how many liquidity failures a single direction may\n\t// contribute before we stop probing it lower for this payment.\n\tmaxDryProbes = 3\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridors a single flow decomposition walks.\n\tflowRounds = 12\n\n\t// hopelessStreak is how many consecutive failures we tolerate before\n\t// trusting the belief-derived local budget for a give-up decision.\n\thopelessStreak = 4\n\n\t// maxGoodCorridors bounds the success-anchored corridor memory.\n\tmaxGoodCorridors = 24\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\n// revKey is the key of the same channel in the opposite direction.\nfunc (e *edge) revKey() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.from}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// fails counts liquidity failures charged to this direction.\n\tfails int\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// goodCorridor is a corridor that has actually settled for us, together with\n// the largest amount it settled. Replaying these first is the cheapest way to\n// finish repeat payments inside a batch.\ntype goodCorridor struct {\n\tpath []*edge\n\tamt lnwire.MilliSatoshi\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan.\n\tqueued []*plan\n\n\t// good remembers corridors that settled, best first.\n\tgood []goodCorridor\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\t// retryPath is a corridor whose last failure was a repairable policy\n\t// error, so it deserves an immediate second look at the same amount.\n\tretryPath []*edge\n\tretryAmt lnwire.MilliSatoshi\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// mppOK reports whether this payment may be split at all.\nfunc (r *router) mppOK() bool {\n\treturn r.spec.MaxParts > 1\n}\n\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// view returns a read-only belief for a directed channel, substituting a\n// zero-value belief when we have no evidence at all.\nfunc (r *router) view(k edgeKey) *belief {\n\tif b, ok := r.beliefs[k]; ok {\n\t\treturn b\n\t}\n\treturn &belief{}\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath. After several dry probes we stop entirely.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tif b.fails >= maxDryProbes {\n\t\treturn b.okAmt\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.40 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// compBound is the complementary upper bound on a direction: liquidity we\n// have proven sits on the reverse side cannot also sit on this side.\nfunc (r *router) compBound(e *edge) lnwire.MilliSatoshi {\n\trb := r.beliefs[e.revKey()]\n\tif rb == nil || rb.okAmt == 0 {\n\t\treturn e.capacity\n\t}\n\theld := rb.okAmt * compSlackNum / compSlackDen\n\tif held >= e.capacity {\n\t\treturn 0\n\t}\n\treturn e.capacity - held\n}\n\n// provenCenter is the amount we believe this direction actually holds. It\n// combines a demonstrated forward (less what we have since drained) with the\n// complementary inference that a reverse-direction failure at a small amount\n// means the funds are on THIS side.\nfunc (r *router) provenCenter(e *edge) lnwire.MilliSatoshi {\n\tvar center lnwire.MilliSatoshi\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.succ {\n\t\tc := e.capacity * provenCenterNum / provenCenterDen\n\t\tif c > b.drained {\n\t\t\tcenter = c - b.drained\n\t\t}\n\t}\n\n\t// Bimodal complement: the reverse side came up dry at failAmt, so this\n\t// side is holding close to the whole channel.\n\tif rb := r.beliefs[e.revKey()]; rb != nil && rb.hasFail {\n\t\tif e.capacity > rb.failAmt {\n\t\t\tinf := (e.capacity - rb.failAmt) *\n\t\t\t\trevEmptyNum / revEmptyDen\n\t\t\tif inf > center {\n\t\t\t\tcenter = inf\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil && b.okAmt > center {\n\t\tcenter = b.okAmt\n\t}\n\n\treturn center\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else {\n\t\tif comp := r.compBound(e); comp < c {\n\t\t\tc = comp\n\t\t}\n\t\tif b != nil && b.hasFail {\n\t\t\t// Retry below the proven failure point rather than\n\t\t\t// blacklisting the channel outright.\n\t\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\t\tc = lim\n\t\t\t}\n\t\t}\n\t}\n\n\tif b != nil {\n\t\tif b.inFlight >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= b.inFlight\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.view(e.key())\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b.okAmt > est {\n\t\test = b.okAmt\n\t}\n\tif !b.hasFail {\n\t\t// A proven forward, or a dry reverse direction, says most of\n\t\t// the channel funds sit on this side.\n\t\tif c := r.provenCenter(e) * 85 / 100; c > est {\n\t\t\test = c\n\t\t}\n\t} else if lim := retryLimit(b, e.capacity); est > lim {\n\t\test = lim\n\t}\n\tif b.misses > 0 {\n\t\test = est / lnwire.MilliSatoshi(1+b.misses)\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.view(e.key())\n\tif b.dead {\n\t\treturn 0\n\t}\n\n\teff := amt + b.inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\t// The complementary bound is hard evidence: that much liquidity is\n\t// provably parked on the other side of the channel.\n\tif comp := r.compBound(e); eff > comp {\n\t\treturn minProb / 4\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\t\tcenter := r.provenCenter(e)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase center > 0 && !b.hasFail:\n\t\t\t// Bimodal optimism: the funds are believed to sit on\n\t\t\t// this side, less whatever we have drained since.\n\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t(float64(eff)/float64(center)-1)*4.5,\n\t\t\t))\n\t\t\tp = 0.3*p + 0.7*q\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif b.misses > 0 {\n\t\tm := b.misses\n\t\tif m > 3 {\n\t\t\tm = 3\n\t\t}\n\t\tp *= math.Pow(0.6, float64(m))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPathW runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// avoid names channels that must not be reused, so parallel shards do not\n// contend for the same liquidity. feeMul scales the fee term: raising it\n// produces a cheaper, slightly riskier corridor, which is how we shave fee\n// ppm once we already have a reliable plan in hand.\nfunc (r *router) findPathW(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\tfeeMul float64) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif amtOver > r.availCap(e) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver)\n\t\t\tif p <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < 1e-5 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := (float64(sending) - float64(amt)) * feeMul\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// findPath is findPathW at the default fee weighting.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPathW(amt, avoid, 1.0)\n}\n\n// makeRoute prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds the whole payment.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// rawLocalBudget is the total balance across our own channels, ignoring all\n// beliefs and policies. It is a hard ceiling on anything we can deliver.\nfunc (r *router) rawLocalBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\ttotal += bal\n\t}\n\treturn total\n}\n\n// rawMaxLocal is the largest single local channel balance, which bounds one\n// shard: a shard leaves through exactly one first hop.\nfunc (r *router) rawMaxLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, bal := range r.localBalances {\n\t\tif bal > best {\n\t\t\tbest = bal\n\t\t}\n\t}\n\treturn best\n}\n\n// maxLocalEdge is the largest single local channel balance still free, which\n// is the true ceiling on one shard.\nfunc (r *router) maxLocalEdge() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge) lnwire.MilliSatoshi {\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\treturn bn - bn/100\n}\n\n// flowCeiling estimates the largest amount a SINGLE shard can plausibly\n// deliver right now, by taking the best believed corridor bottleneck over a\n// couple of disjoint probes. Starting the amount ladder here instead of at\n// the full remaining amount is what removes the doomed first attempt on\n// large payments.\nfunc (r *router) flowCeiling(remaining lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) lnwire.MilliSatoshi {\n\n\tbest := lnwire.MilliSatoshi(0)\n\tblock := make(map[uint64]bool, len(avoid)+4)\n\tfor c := range avoid {\n\t\tblock[c] = true\n\t}\n\n\tprobe := remaining\n\tif single := r.maxLocalEdge(); single > 0 && single < probe {\n\t\tprobe = single\n\t}\n\tif probe < minShard {\n\t\treturn 0\n\t}\n\n\tfor i := 0; i < 3; i++ {\n\t\tpath := r.findAnyPath(probe, remaining, 1, block)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\t\tif bn := r.bottleneck(path); bn > best {\n\t\t\tbest = bn\n\t\t}\n\t\tif best >= probe {\n\t\t\tbreak\n\t\t}\n\t\tblock[path[0].chanID] = true\n\t}\n\n\tif best > remaining {\n\t\tbest = remaining\n\t}\n\treturn best\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, exact local channel balances, and\n// evidence-derived sizes just below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford.\n\tmaxK := partsLeft\n\tif maxK < 4 && r.mppOK() {\n\t\tmaxK = 4\n\t}\n\tif maxK > 10 {\n\t\tmaxK = 10\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Local channel balances are exact knowledge, and a shard sized to a\n\t// local channel is exactly what a fan-out split wants.\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\tif c := r.availCap(e); c >= minShard {\n\t\t\tadd(c - c/200)\n\t\t}\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\tfee lnwire.MilliSatoshi\n}\n\n// mkPlan prices a corridor at one amount and returns the scored plan, or nil\n// when the attempt is not novel or not feasible.\nfunc (r *router) mkPlan(path []*edge, a,\n\tremaining lnwire.MilliSatoshi) *plan {\n\n\tif a < minShard {\n\t\treturn nil\n\t}\n\tif r.failedSigs[planSig(path, a)] {\n\t\treturn nil\n\t}\n\trt, p, err := r.makeRoute(path, a)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfee := rt.TotalAmount - a\n\treturn &plan{\n\t\tpath: path,\n\t\trt: rt,\n\t\tamt: a,\n\t\tprob: p,\n\t\tfee: fee,\n\t\tscore: r.score(a, p, fee, remaining),\n\t}\n}\n\n// evalPath scores a corridor at the amount its weakest hop is BELIEVED to\n// bear first, then at the requested amount, keeping the better of the two.\n// Pricing the believed amount first matters because when both score similarly\n// we would rather send the one that survives.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tvar cands []lnwire.MilliSatoshi\n\tif bn := r.bottleneck(path); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tcands = append(cands, g)\n\t}\n\tcands = append(cands, a)\n\n\tfor _, c := range cands {\n\t\tpl := r.mkPlan(path, c, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif best == nil || pl.score > best.score {\n\t\t\tbest = pl\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 14 && a >= minShard; i++ {\n\t\tif pl := r.mkPlan(path, a, remaining); pl != nil {\n\t\t\treturn pl\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findAnyPath looks for a corridor able to carry hi, falling back down the\n// amount ladder when nothing can take the full amount.\nfunc (r *router) findAnyPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool) []*edge {\n\n\tif p, err := r.findPath(hi, avoid); err == nil {\n\t\treturn p\n\t}\n\n\tprobes := 0\n\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\tif a >= hi {\n\t\t\tcontinue\n\t\t}\n\t\tif probes >= 7 {\n\t\t\tbreak\n\t\t}\n\t\tprobes++\n\t\tif p, err := r.findPath(a, avoid); err == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several DISJOINT corridors,\n// sizing each shard to what that corridor's weakest hop is believed able to\n// bear. This is the min-cost-flow style joint plan: unequal parallel\n// corridors each get a shard that fits, instead of discovering the split by\n// failing at a blind half.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\tsingle := r.maxLocalEdge()\n\n\tvar out []*plan\n\tleft := remaining\n\n\t// Queued shards are handed out on later calls, as concurrency frees\n\t// up, so the decomposition is not bounded by the parts free right now.\n\trounds := 1\n\tif r.mppOK() {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := 0; k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\twant := left\n\t\tif single > 0 && want > single {\n\t\t\twant = single\n\t\t}\n\n\t\tpath := r.findAnyPath(want, remaining, partsLeft, avoid)\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := want\n\t\tif bn := r.bottleneck(path); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tpl := r.bestOnPath(path, amtS, remaining)\n\t\tif pl == nil {\n\t\t\t// The corridor cannot bear anything novel, so retire\n\t\t\t// its first hop and move on instead of aborting the\n\t\t\t// whole decomposition.\n\t\t\tavoid[path[0].chanID] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, pl)\n\t\tfor _, e := range path {\n\t\t\tavoid[e.chanID] = true\n\t\t}\n\n\t\tif pl.amt >= left {\n\t\t\tbreak\n\t\t}\n\t\tleft -= pl.amt\n\t}\n\n\treturn out\n}\n\n// flowTotal is the amount a plan set delivers in aggregate.\nfunc flowTotal(plans []*plan) lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tfor _, pl := range plans {\n\t\ttotal += pl.amt\n\t}\n\treturn total\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// hopeless reports whether the remainder is provably beyond what our own\n// channels can still push.\nfunc (r *router) hopeless(amt lnwire.MilliSatoshi, inFlight uint32) bool {\n\tif inFlight > 0 {\n\t\treturn false\n\t}\n\tif len(r.localBalances) == 0 {\n\t\treturn false\n\t}\n\n\t// Hard ceiling: total outbound balance.\n\tif r.rawLocalBudget() < amt {\n\t\treturn true\n\t}\n\n\t// A payment that may not be split must fit through one channel.\n\tif !r.mppOK() && r.rawMaxLocal() < amt {\n\t\treturn true\n\t}\n\n\t// Softer test, only once we have actually confirmed dryness by\n\t// failing: the believed-free local liquidity cannot cover the rest.\n\tif r.failStreak >= hopelessStreak && len(r.localEdges) > 0 &&\n\t\tr.localBudget() < amt {\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// replayGood tries the corridors that have already settled for us, largest\n// proven amount first. A batch of similar payments then settles in a single\n// attempt on a known-good corridor, which is the cheapest possible outcome\n// for both the attempt count and the fee.\nfunc (r *router) replayGood(amt lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tvar best *plan\n\tfor _, gc := range r.good {\n\t\tif pathBusy(gc.path, busy) {\n\t\t\tcontinue\n\t\t}\n\n\t\ta := amt\n\t\tif gc.amt < a {\n\t\t\tif !split {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta = gc.amt\n\t\t}\n\n\t\tpl := r.mkPlan(gc.path, a, amt)\n\t\tif pl == nil || pl.prob < confidentProb {\n\t\t\tcontinue\n\t\t}\n\t\tif best == nil || pl.score > best.score {\n\t\t\tbest = pl\n\t\t}\n\t}\n\treturn best\n}\n\n// cheapen looks for a corridor that delivers the same amount for less fee\n// while staying reliable enough to send. Fee ppm is the only penalty left on\n// runs that already succeed, so this is where the remaining points are.\nfunc (r *router) cheapen(best *plan, remaining lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) *plan {\n\n\tif best == nil || best.fee == 0 {\n\t\treturn best\n\t}\n\n\tfor _, mul := range []float64{6.0, 20.0} {\n\t\tpath, err := r.findPathW(best.amt, avoid, mul)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.mkPlan(path, best.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif pl.fee < best.fee && pl.prob >= cheapenProb &&\n\t\t\tpl.prob >= best.prob*0.9 {\n\n\t\t\tbest = pl\n\t\t}\n\t}\n\treturn best\n}\n\n// RequestRoute plans the next shard.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\tif r.hopeless(amt, inFlightHtlcs) {\n\t\treturn nil, errors.New(\"remainder exceeds local liquidity\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\tsplit := r.mppOK()\n\n\t// A corridor whose last failure was a repairable policy error deserves\n\t// an immediate retry at the same amount with the fixed policy, since\n\t// the corridor itself was fine.\n\tif rp := r.retryPath; rp != nil {\n\t\ta := r.retryAmt\n\t\tr.retryPath, r.retryAmt = nil, 0\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif (split || a >= amt) && !pathBusy(rp, busy) {\n\t\t\tif pl := r.mkPlan(rp, a, amt); pl != nil &&\n\t\t\t\tpl.prob >= queueMinProb {\n\n\t\t\t\tr.attempts++\n\t\t\t\treturn pl.rt, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// Known-good corridors first: cheapest attempt count of all.\n\tif pl := r.replayGood(amt, busy, split); pl != nil {\n\t\tr.attempts++\n\t\treturn pl.rt, nil\n\t}\n\n\t// Serve a queued shard from an earlier joint plan while it still holds\n\t// up against current beliefs.\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when the payment cannot be split.\n\t\tif !split && a < amt {\n\t\t\tr.queued = nil\n\t\t\tbreak\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tnp := r.mkPlan(pl.path, a, amt)\n\t\tif np == nil || np.prob < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\treturn np.rt, nil\n\t}\n\n\t// A single shard can never exceed what one local channel can push,\n\t// since a shard leaves through exactly one first hop.\n\thi := amt\n\tif single := r.maxLocalEdge(); single > 0 && single < hi {\n\t\thi = single\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// FIRST-SHOT SIZING: cap the ladder at what a single corridor is\n\t// believed able to bear, so we do not burn the first attempt of every\n\t// large payment probing an amount no corridor can carry.\n\tif split && hi > minShard {\n\t\tif ceil := r.flowCeiling(hi, busy); ceil >= minShard &&\n\t\t\tceil < hi {\n\n\t\t\thi = ceil\n\t\t}\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\tvar best *plan\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tbudget := probeBudget\n\t\t\tif best != nil {\n\t\t\t\tbudget = probeBudgetGood\n\t\t\t}\n\t\t\tif probes >= budget {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !split && a < amt {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt &&\n\t\t\t\tbest.prob > confidentProb {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// When no single corridor carries the whole remainder, plan the split\n\t// deliberately over disjoint corridors instead of halving blindly.\n\tif split && (best == nil || best.amt < amt) {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal := flowTotal(flow)\n\t\t\tfirst := flow[0]\n\n\t\t\tif best == nil || total > best.amt {\n\t\t\t\tr.queued = flow[1:]\n\t\t\t\tbest = first\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: deliver whatever we can. Even a small settled shard\n\t// reduces the remainder and refreshes evidence, which is strictly\n\t// better than terminally giving up on the payment.\n\tif best == nil {\n\t\tbest = r.salvage(amt, busy, split)\n\t}\n\n\tif best == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\t// Now that reliability is settled, try to pay less for it.\n\tif best.prob >= confidentProb {\n\t\tvar avoid map[uint64]bool\n\t\tif len(busy) > 0 {\n\t\t\tavoid = busy\n\t\t}\n\t\tbest = r.cheapen(best, amt, avoid)\n\t}\n\n\tr.attempts++\n\treturn best.rt, nil\n}\n\n// salvage hunts for any novel attempt at all, walking a wide descending\n// amount ladder over both the disjoint and the unrestricted graph. It is the\n// difference between delivering part of a payment and abandoning it.\nfunc (r *router) salvage(remaining lnwire.MilliSatoshi, busy map[uint64]bool,\n\tsplit bool) *plan {\n\n\tif !split {\n\t\t// Without splitting, only a full-amount attempt helps.\n\t\tpath, err := r.findPath(remaining, nil)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn r.mkPlan(path, remaining, remaining)\n\t}\n\n\ta := remaining\n\tfor i := 0; i < 22 && a >= minShard; i++ {\n\t\tfor pass := 0; pass < 2; pass++ {\n\t\t\tvar avoid map[uint64]bool\n\t\t\tif pass == 0 {\n\t\t\t\tif len(busy) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tavoid = busy\n\t\t\t}\n\t\t\tpath, err := r.findPath(a, avoid)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pl := r.bestOnPath(path, a, remaining); pl != nil {\n\t\t\t\treturn pl\n\t\t\t}\n\t\t}\n\t\ta = a * 2 / 3\n\t}\n\treturn nil\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t\tb.fails = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tb.fails = 0\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t\trb.fails = 0\n\t\t}\n\t}\n}\n\n// rememberGood records a corridor that settled, so later payments in the\n// batch can replay it directly.\nfunc (r *router) rememberGood(rt *route.Route) {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn\n\t}\n\n\tpath := make([]*edge, 0, n)\n\tfor _, h := range rt.Hops {\n\t\te, ok := r.byKey[hopKey(h)]\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\tpath = append(path, e)\n\t}\n\tamt := rt.Hops[n-1].AmtToForward\n\n\tfor i := range r.good {\n\t\tif planSig(r.good[i].path, 0) != planSig(path, 0) {\n\t\t\tcontinue\n\t\t}\n\t\tif amt > r.good[i].amt {\n\t\t\tr.good[i].amt = amt\n\t\t}\n\t\treturn\n\t}\n\n\tr.good = append(r.good, goodCorridor{path: path, amt: amt})\n\tsort.SliceStable(r.good, func(i, j int) bool {\n\t\treturn r.good[i].amt > r.good[j].amt\n\t})\n\tif len(r.good) > maxGoodCorridors {\n\t\tr.good = r.good[:maxGoodCorridors]\n\t}\n}\n\n// dropGood forgets a corridor that has just failed, so we do not replay a\n// stale success into a second wasted attempt.\nfunc (r *router) dropGood(rt *route.Route) {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn\n\t}\n\tpath := make([]*edge, 0, n)\n\tfor _, h := range rt.Hops {\n\t\te, ok := r.byKey[hopKey(h)]\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\tpath = append(path, e)\n\t}\n\tsig := planSig(path, 0)\n\n\tout := r.good[:0]\n\tfor _, gc := range r.good {\n\t\tif planSig(gc.path, 0) != sig {\n\t\t\tout = append(out, gc)\n\t\t}\n\t}\n\tr.good = out\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pathOf reconstructs the edge slice behind a route, or nil when any hop is\n// not in our graph view.\nfunc (r *router) pathOf(rt *route.Route) []*edge {\n\tpath := make([]*edge, 0, len(rt.Hops))\n\tfor _, h := range rt.Hops {\n\t\te, ok := r.byKey[hopKey(h)]\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tpath = append(path, e)\n\t}\n\treturn path\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tr.rememberGood(rt)\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\tr.dropGood(rt)\n\n\t// Beliefs just changed, so any queued joint plan is stale.\n\tr.queued = nil\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure: spread suspicion over the remote hops so\n\t// we stop re-picking this corridor without destroying the hard bounds\n\t// we have earned.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\t// Optimism earned from older evidence cannot survive a\n\t\t\t// corridor that just failed somewhere inside it.\n\t\t\tb.succ = false\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\tkind := classify(result.Failure)\n\n\t// A repairable policy error says nothing bad about the corridor's\n\t// liquidity, so once the policy view is patched the same corridor is\n\t// worth exactly one immediate retry at the same delivered amount.\n\tif kind == \"fee\" || kind == \"cltv\" {\n\t\tif p := r.pathOf(rt); p != nil {\n\t\t\tr.retryPath = p\n\t\t\tr.retryAmt = rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\t}\n\t\t// The signature bans the identical attempt, but the repaired\n\t\t// policy makes the retry a genuinely different route.\n\t\tif sig := routeSig(rt); sig != \"\" {\n\t\t\tdelete(r.failedSigs, sig)\n\t\t}\n\t\tr.failStreak--\n\t}\n\n\tswitch kind {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// generous bump is cheaper than another failed attempt.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\tb.fails++\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\tb.drained = 0\n\n\t// A local channel failing means our balance estimate was too high, and\n\t// it also invalidates the complementary inference that the far side of\n\t// that channel is the empty one.\n\tif failIdx == 0 {\n\t\trk := edgeKey{chanID: h.ChannelID, to: rt.SourcePubKey}\n\t\tif rb, ok := r.beliefs[rk]; ok {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}"
}
},
{
"id": 33,
"parent": 10,
"score": 0.7223,
"accepted": false,
"frontier": false,
"params": {
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms.\n//\n// Design summary:\n// - Bimodal prior over amount/capacity for hidden liquidity, so wider\n// channels are preferred automatically at equal amounts.\n// - Per-DIRECTED-channel beliefs (channel plus direction) with hard\n// lower-OK / upper-fail bounds and no time decay: a stale bound costs\n// one retry to refresh, which is cheaper than decaying evidence.\n// - Balance bookkeeping on settle: a forward that settles moves\n// liquidity, so the forward direction's bounds go down and the reverse\n// direction's bounds go up by exactly the amount forwarded.\n// - Depth-aware lower retries instead of blacklisting.\n// - JOINT ROUTE-SET PLANNING with residual accounting and rebalancing.\n//\n// Changes in this revision, all aimed at the observed failure mode. Every\n// failed payment in the evaluation was a LARGE amount (0.86-2.06 BTC in\n// msat) that burned 5-13 attempts and then terminated with \"no route\n// found\". Diagnosis and fixes:\n//\n// 1. TERMINAL GIVE-UP WAS TOO EAGER, AND THE WRONG THING GAVE UP. The\n// planner would return an error as soon as its preferred search found\n// nothing, even though a much smaller shard was still routable and the\n// part budget was untouched. RequestRoute now ends with a DESPERATION\n// SWEEP: before returning any error it walks a wide geometric ladder\n// down to a tiny floor, over every local channel, ignoring the score\n// model and accepting any priced route at all. Giving up must be the\n// last resort, not the first search's verdict.\n//\n// 2. THE FLOW PLAN WAS SIZED AGAINST BELIEFS, NOT AGAINST PARTS. For a\n// 2 BTC payment over a network of ~0.1 BTC channels, no plan of 3-4\n// shards can ever cover the remainder, so `total > bestAmt` almost\n// never fired and the router fell back to a single oversized shard.\n// planFlow now targets remaining/partsLeft as its natural shard size\n// and explicitly builds as many corridors as the part budget allows,\n// so a large payment is decomposed into many small shards up front\n// instead of being discovered by repeated halving.\n//\n// 3. FIRST-SHARD SIZING IGNORED THE PART BUDGET. A payment that needs N\n// parts should not open with a shard sized to the whole remainder;\n// that first attempt is nearly guaranteed to fail and it poisons the\n// bounds of every fat channel on the way. The ladder is now anchored\n// at an amount the local budget and part count can plausibly bear.\n//\n// 4. UNATTRIBUTABLE FAILURES (failed_at_hop -1 appears in the data) were\n// costing a full attempt and teaching nothing. They now also carve a\n// soft cap on the corridor's narrowest hop so the next search moves\n// somewhere else instead of re-deriving the same path.\n//\n// 5. Attempt budget is spent more carefully: the probe budget shrinks as\n// the fail streak grows, repeated identical-signature searches are\n// skipped before they are priced, and a pure policy repair (fee, cltv,\n// min htlc) no longer counts against the fail streak that ends the\n// payment, since nothing was learned about liquidity.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\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 = 40\n\n\t// attemptCostBase and attemptCostPPM express the virtual cost of one\n\t// failed attempt. Success dominates the objective, so the retry cost\n\t// stays large relative to fees, but it is small enough that fee\n\t// differences break ties between similarly reliable corridors.\n\tattemptCostBase = lnwire.MilliSatoshi(1_500)\n\tattemptCostPPM = lnwire.MilliSatoshi(8_000)\n\n\tminProb = 0.005\n\tmaxProb = 0.985\n\tknownProb = 0.995\n\n\t// maxRouteHops bounds path length.\n\tmaxRouteHops = 7\n\n\t// maxAttempts and maxFailStreak bound how long we keep trying before\n\t// declaring the payment hopeless. Both are generous: an extra retry\n\t// costs a small penalty, while an abandoned payment costs the whole\n\t// success term.\n\tmaxAttempts = 90\n\tmaxFailStreak = 30\n\n\t// probeBudget caps how many Dijkstra runs a single RequestRoute call\n\t// may spend per pass.\n\tprobeBudget = 14\n\n\t// minShard is the smallest shard we will ever plan.\n\tminShard = lnwire.MilliSatoshi(1_000)\n\n\t// feeWeight and partCost shape the shard score: fee matters only as a\n\t// tie-break, while every extra expected part costs a little.\n\tfeeWeight = 4.0\n\tpartCost = 0.006\n\n\t// priorSafeNum/priorSafeDen is the fraction of capacity we assume a\n\t// channel with no evidence can bear.\n\tpriorSafeNum = 35\n\tpriorSafeDen = 100\n\n\t// provenCenterNum/provenCenterDen is the fraction of capacity a\n\t// direction that has demonstrably forwarded is assumed to hold.\n\tprovenCenterNum = 62\n\tprovenCenterDen = 100\n\n\t// queueMinProb is the probability a queued shard must still clear\n\t// before we hand it out on a later call.\n\tqueueMinProb = 0.15\n\n\t// flowRounds bounds the corridor searches spent building one flow\n\t// plan, and flowRebalance bounds the redistribution sweeps.\n\tflowRounds = 16\n\tflowRebalance = 3\n\n\t// sweepSteps is how far the desperation sweep descends before it\n\t// concedes that the payment really is unroutable.\n\tsweepSteps = 22\n)\n\n// edgeKey identifies a directed channel: the channel plus the node the\n// channel points at.\ntype edgeKey struct {\n\tchanID uint64\n\tto route.Vertex\n}\n\n// edge is one directed channel of the public graph.\ntype edge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFee lnwire.MilliSatoshi\n\tfeePPM lnwire.MilliSatoshi\n\tcltv uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *edge) key() edgeKey {\n\treturn edgeKey{chanID: e.chanID, to: e.to}\n}\n\nfunc (e *edge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFee + amt*e.feePPM/1_000_000\n}\n\nfunc (e *edge) policyOK(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn amt <= e.capacity\n}\n\n// belief tracks what we have proven about a directed channel's liquidity.\ntype belief struct {\n\t// okAmt is the largest amount we currently believe is available, as\n\t// proven by a forward and adjusted for liquidity we have since moved.\n\tokAmt lnwire.MilliSatoshi\n\n\t// failAmt is the smallest amount believed to fail; hasFail guards it.\n\tfailAmt lnwire.MilliSatoshi\n\thasFail bool\n\n\t// succ records that this direction has actually forwarded for us,\n\t// which under a bimodal split is strong evidence that the bulk of the\n\t// channel funds sit on this side.\n\tsucc bool\n\n\t// drained is how much we have pushed through this direction since the\n\t// evidence that set succ, which shifts the optimistic centre down.\n\tdrained lnwire.MilliSatoshi\n\n\t// inFlight is liquidity currently committed by our own HTLCs.\n\tinFlight lnwire.MilliSatoshi\n\n\t// reserved is liquidity earmarked by planned-but-unsent shards.\n\treserved lnwire.MilliSatoshi\n\n\t// misses counts failures we could not attribute to a specific hop but\n\t// that this channel took part in.\n\tmisses int\n\n\t// dead marks a channel that failed permanently.\n\tdead bool\n}\n\n// bimodalPrior is the success probability of pushing amt through a channel\n// of the given capacity with no direct evidence. Liquidity sits almost\n// entirely on one side, so small amounts nearly always pass while amounts\n// approaching capacity almost never do.\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity == 0 {\n\t\treturn minProb\n\t}\n\tif amt > capacity {\n\t\treturn 0\n\t}\n\tx := float64(amt) / float64(capacity)\n\n\t// Decaying low mode: tiny fractions of capacity are near certain.\n\tlow := math.Exp(-x * 3.2)\n\n\t// Logistic cliff as we approach capacity.\n\tcliff := 1.0 / (1.0 + math.Exp((x-0.42)*9.0))\n\n\treturn clampProb(0.30*low + 0.70*cliff)\n}\n\nfunc clampProb(p float64) float64 {\n\tif p > maxProb {\n\t\treturn maxProb\n\t}\n\tif p < minProb {\n\t\treturn minProb\n\t}\n\treturn p\n}\n\n// router is the evolved candidate.\ntype router struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// inEdges maps a node to the directed edges arriving at it.\n\tinEdges map[route.Vertex][]*edge\n\n\t// byKey indexes every directed edge for failure attribution.\n\tbyKey map[edgeKey]*edge\n\n\t// localEdges are the directed edges leaving our own node.\n\tlocalEdges []*edge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]*belief\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n\n\t// queued holds the remaining shards of a joint route-set plan, to be\n\t// handed out on subsequent RequestRoute calls.\n\tqueued []*plan\n\n\t// failedSigs remembers (path, amount) pairs that already failed so we\n\t// never hand out the identical attempt twice.\n\tfailedSigs map[string]bool\n\n\tfailStreak int\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\tr := &router{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tinEdges: make(map[route.Vertex][]*edge),\n\t\tbyKey: make(map[edgeKey]*edge),\n\t\tlocalBalances: localBalances,\n\t\tbeliefs: make(map[edgeKey]*belief),\n\t\tpending: make(map[uint64]*route.Route),\n\t\tfailedSigs: make(map[string]bool),\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(ctx, node,\n\t\t\tfunc(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\tpol := ch.InPolicy\n\t\t\t\tif pol == nil || pol.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\te := &edge{\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(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFee: pol.FeeBaseMSat,\n\t\t\t\t\tfeePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\tcltv: pol.TimeLockDelta,\n\t\t\t\t\tminHTLC: pol.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif pol.HasMaxHTLC {\n\t\t\t\t\te.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.inEdges[e.to] = append(r.inEdges[e.to], e)\n\t\t\t\tr.byKey[e.key()] = e\n\t\t\t\tif e.from == source {\n\t\t\t\t\tr.localEdges = append(\n\t\t\t\t\t\tr.localEdges, e,\n\t\t\t\t\t)\n\t\t\t\t}\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\n// bel returns (creating if needed) the belief for a directed channel.\nfunc (r *router) bel(k edgeKey) *belief {\n\tb, ok := r.beliefs[k]\n\tif !ok {\n\t\tb = &belief{}\n\t\tr.beliefs[k] = b\n\t}\n\treturn b\n}\n\n// capOf is the capacity of a directed channel, or zero when we do not know\n// the direction at all.\nfunc (r *router) capOf(k edgeKey) lnwire.MilliSatoshi {\n\tif e, ok := r.byKey[k]; ok {\n\t\treturn e.capacity\n\t}\n\treturn 0\n}\n\n// retryLimit is how far we are still willing to push a direction that has\n// proven a failure. A failure at a tiny fraction of capacity means the\n// direction is essentially empty, while a failure near capacity leaves a lot\n// of plausible room underneath.\nfunc retryLimit(b *belief, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\tif !b.hasFail {\n\t\treturn 0\n\t}\n\tdepth := 0.5\n\tif capacity > 0 {\n\t\tdepth = float64(b.failAmt) / float64(capacity)\n\t\tif depth > 1 {\n\t\t\tdepth = 1\n\t\t}\n\t}\n\tf := 0.45 + 0.35*depth\n\tlim := lnwire.MilliSatoshi(float64(b.failAmt) * f)\n\tif lim < b.okAmt {\n\t\tlim = b.okAmt\n\t}\n\treturn lim\n}\n\n// committed is liquidity on a directed channel that is already spoken for,\n// either by a live HTLC or by a planned shard we have not sent yet.\nfunc (b *belief) committed() lnwire.MilliSatoshi {\n\tif b == nil {\n\t\treturn 0\n\t}\n\treturn b.inFlight + b.reserved\n}\n\n// availCap is the hard upper bound on what we are still willing to push\n// over an edge. It returns zero when the edge is unusable at any amount.\nfunc (r *router) availCap(e *edge) lnwire.MilliSatoshi {\n\tc := e.capacity\n\tif e.maxHTLC != 0 && e.maxHTLC < c {\n\t\tc = e.maxHTLC\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tif e.from == r.source {\n\t\t// Our own balance is known exactly.\n\t\tif bal := r.localBalances[e.chanID]; bal < c {\n\t\t\tc = bal\n\t\t}\n\t} else if b != nil && b.hasFail {\n\t\t// Retry below the proven failure point rather than\n\t\t// blacklisting the channel outright.\n\t\tif lim := retryLimit(b, e.capacity); lim < c {\n\t\t\tc = lim\n\t\t}\n\t}\n\n\tif used := b.committed(); used > 0 {\n\t\tif used >= c {\n\t\t\treturn 0\n\t\t}\n\t\tc -= used\n\t}\n\n\treturn c\n}\n\n// safeCap is the largest amount we believe this edge can actually bear with\n// decent probability. It is the sizing primitive for shard planning.\nfunc (r *router) safeCap(e *edge) lnwire.MilliSatoshi {\n\thard := r.availCap(e)\n\tif hard == 0 {\n\t\treturn 0\n\t}\n\tif e.from == r.source {\n\t\treturn hard\n\t}\n\n\tb := r.beliefs[e.key()]\n\test := e.capacity * priorSafeNum / priorSafeDen\n\tif b != nil {\n\t\tif b.okAmt > est {\n\t\t\test = b.okAmt\n\t\t}\n\t\tif b.succ && !b.hasFail {\n\t\t\t// A proven forward under a bimodal split says most of\n\t\t\t// the channel funds sit on this side.\n\t\t\topt := e.capacity * 55 / 100\n\t\t\tif opt > b.drained {\n\t\t\t\topt -= b.drained\n\t\t\t} else {\n\t\t\t\topt = 0\n\t\t\t}\n\t\t\tif opt > est {\n\t\t\t\test = opt\n\t\t\t}\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, e.capacity); est > lim {\n\t\t\t\test = lim\n\t\t\t}\n\t\t}\n\t\tif b.misses > 0 {\n\t\t\test = est / 2\n\t\t}\n\t}\n\tif est > hard {\n\t\test = hard\n\t}\n\treturn est\n}\n\n// prob is our success probability estimate for sending amt over the edge.\nfunc (r *router) prob(e *edge, amt lnwire.MilliSatoshi) float64 {\n\tif amt == 0 {\n\t\treturn 1\n\t}\n\n\tb := r.beliefs[e.key()]\n\tif b != nil && b.dead {\n\t\treturn 0\n\t}\n\n\tvar inFlight lnwire.MilliSatoshi\n\tmisses := 0\n\tif b != nil {\n\t\tinFlight = b.inFlight\n\t\tmisses = b.misses\n\t}\n\teff := amt + inFlight\n\n\t// Our own channels have exactly known balances.\n\tif e.from == r.source {\n\t\tif r.localBalances[e.chanID] >= eff {\n\t\t\treturn knownProb\n\t\t}\n\t\treturn 0\n\t}\n\n\tvar p float64\n\tswitch {\n\tcase b == nil:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\tcase b.okAmt >= eff:\n\t\tp = knownProb\n\n\tcase b.hasFail && eff >= b.failAmt:\n\t\treturn minProb / 4\n\n\tdefault:\n\t\tp = bimodalPrior(eff, e.capacity)\n\n\t\tswitch {\n\t\tcase b.okAmt > 0 && b.hasFail:\n\t\t\t// Known-good below and known-bad above compress the\n\t\t\t// uncertain interval.\n\t\t\tlo := float64(b.okAmt)\n\t\t\thi := float64(b.failAmt)\n\t\t\tif hi > lo {\n\t\t\t\tfrac := (float64(eff) - lo) / (hi - lo)\n\t\t\t\tp = 0.5*p + 0.5*(1-frac)\n\t\t\t}\n\n\t\tcase b.succ && !b.hasFail:\n\t\t\t// Bimodal optimism: this direction has forwarded, so\n\t\t\t// assume it holds most of the channel, less whatever we\n\t\t\t// have drained since.\n\t\t\tcenter := float64(e.capacity) *\n\t\t\t\tprovenCenterNum / provenCenterDen\n\t\t\tcenter -= float64(b.drained)\n\t\t\tif center < float64(b.okAmt) {\n\t\t\t\tcenter = float64(b.okAmt)\n\t\t\t}\n\t\t\tif center > 0 {\n\t\t\t\tq := 1.0 / (1.0 + math.Exp(\n\t\t\t\t\t(float64(eff)/center-1)*4.5,\n\t\t\t\t))\n\t\t\t\tp = 0.3*p + 0.7*q\n\t\t\t}\n\n\t\tcase b.okAmt > 0:\n\t\t\t// A prior success raises confidence nearby.\n\t\t\tratio := float64(eff) / float64(b.okAmt)\n\t\t\tboost := math.Exp(-(ratio - 1) * 1.1)\n\t\t\tp = 0.45*p + 0.55*boost\n\n\t\tcase b.hasFail:\n\t\t\t// Under bimodality a failure means the direction is\n\t\t\t// probably empty, so retries below it are a last\n\t\t\t// resort, not a coin flip.\n\t\t\tfrac := float64(eff) / float64(b.failAmt)\n\t\t\tp = 0.5 * p * (1 - frac)\n\t\t}\n\t}\n\n\tif misses > 0 {\n\t\tif misses > 3 {\n\t\t\tmisses = 3\n\t\t}\n\t\tp *= math.Pow(0.7, float64(misses))\n\t}\n\n\treturn clampProb(p)\n}\n\n// --- Dijkstra -------------------------------------------------------------\n\ntype pqItem struct {\n\tnode route.Vertex\n\tdist float64\n\tamt lnwire.MilliSatoshi\n\t// logProb is the accumulated ln(probability) from node to target.\n\tlogProb float64\n\thops int\n\tidx int\n}\n\ntype pq []*pqItem\n\nfunc (q pq) Len() int { return len(q) }\nfunc (q pq) Less(i, j int) bool { return q[i].dist < q[j].dist }\nfunc (q pq) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *pq) Push(x any) { it := x.(*pqItem); it.idx = len(*q); *q = append(*q, it) }\nfunc (q *pq) Pop() any {\n\told := *q\n\tn := len(old)\n\tit := old[n-1]\n\t*q = old[:n-1]\n\treturn it\n}\n\n// pathState is the best known way to reach the target from a node.\ntype pathState struct {\n\tdist float64\n\tvia *edge\n}\n\n// findPath runs a probability-weighted backward Dijkstra for a delivered\n// amount and returns the edge sequence from source to target. The cost of a\n// path is its fee plus an attempt cost divided by the path's success\n// probability, which is the standard risk/fee trade-off.\n//\n// extra charges additional liquidity against a channel on top of what the\n// beliefs already record, which is how the flow planner accounts for shards\n// it has provisionally assigned. penalty multiplies the cost of channels the\n// plan already uses, so the search spreads over fresh corridors when it can\n// but may still reuse a fat channel when nothing else works.\n//\n// loose drops the belief-derived caps and the path probability floor on\n// remote hops, keeping only policy limits and permanently dead channels.\n// The desperation sweep uses it so that a corridor we consider hopeless is\n// still returned rather than nothing at all.\nfunc (r *router) findPath(amt lnwire.MilliSatoshi, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi, penalty float64,\n\tloose bool) ([]*edge, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\n\tattemptCost := float64(attemptCostBase + amt*attemptCostPPM/1_000_000)\n\thopCost := attemptCost * 0.2\n\n\tprobFloor := 1e-5\n\tif loose {\n\t\tprobFloor = 0\n\t}\n\n\tbest := make(map[route.Vertex]*pathState)\n\ttarget := r.spec.Target\n\tbest[target] = &pathState{dist: 0}\n\n\tq := &pq{}\n\theap.Push(q, &pqItem{node: target, dist: 0, amt: amt})\n\n\tfor q.Len() > 0 {\n\t\tit := heap.Pop(q).(*pqItem)\n\t\tcur := best[it.node]\n\t\tif cur == nil || it.dist > cur.dist+1e-9 {\n\t\t\tcontinue\n\t\t}\n\t\tif it.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif it.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range r.inEdges[it.node] {\n\t\t\tif avoid[e.chanID] || e.from == it.node {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := it.amt\n\t\t\tif !e.policyOK(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Charge the provisional flow already routed over this\n\t\t\t// channel against its believed room.\n\t\t\tused := extra[e.chanID]\n\t\t\thard := r.availCap(e)\n\t\t\tif loose && e.from != r.source {\n\t\t\t\t// In loose mode the only hard constraints are\n\t\t\t\t// the ones we cannot argue with: policy and a\n\t\t\t\t// dead channel.\n\t\t\t\tif b := r.beliefs[e.key()]; b == nil || !b.dead {\n\t\t\t\t\thard = e.capacity\n\t\t\t\t\tif e.maxHTLC != 0 && e.maxHTLC < hard {\n\t\t\t\t\t\thard = e.maxHTLC\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif amtOver+used > hard {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tp := r.prob(e, amtOver+used)\n\t\t\tif p <= 0 {\n\t\t\t\tif !loose {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tp = minProb / 8\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tif e.from != r.source {\n\t\t\t\tsending += e.fee(amtOver)\n\t\t\t}\n\n\t\t\tlogProb := it.logProb + math.Log(p)\n\t\t\ttotalProb := math.Exp(logProb)\n\t\t\tif totalProb < probFloor {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := float64(sending) - float64(amt)\n\t\t\tdist := (fee+attemptCost)/totalProb +\n\t\t\t\tfloat64(it.hops+1)*hopCost\n\t\t\tif used > 0 && penalty > 1 {\n\t\t\t\tdist *= penalty\n\t\t\t}\n\n\t\t\tif prev, ok := best[e.from]; ok &&\n\t\t\t\tdist >= prev.dist-1e-9 {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbest[e.from] = &pathState{dist: dist, via: e}\n\n\t\t\theap.Push(q, &pqItem{\n\t\t\t\tnode: e.from,\n\t\t\t\tdist: dist,\n\t\t\t\tamt: sending,\n\t\t\t\tlogProb: logProb,\n\t\t\t\thops: it.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tsrc, ok := best[r.source]\n\tif !ok || src.via == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tvar path []*edge\n\tnode := r.source\n\tfor node != r.spec.Target {\n\t\tst, ok := best[node]\n\t\tif !ok || st.via == nil {\n\t\t\treturn nil, errors.New(\"broken path\")\n\t\t}\n\t\tpath = append(path, st.via)\n\t\tnode = st.via.to\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"path too long\")\n\t\t}\n\t}\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty path\")\n\t}\n\n\treturn path, nil\n}\n\n// plainPath is findPath with no provisional flow charged.\nfunc (r *router) plainPath(amt lnwire.MilliSatoshi,\n\tavoid map[uint64]bool) ([]*edge, error) {\n\n\treturn r.findPath(amt, avoid, nil, 1, false)\n}\n\n// makeRouteMode prices a known path at a delivered amount, validating every\n// hop's policy and belief bounds. It returns the route and its estimated\n// success probability. When loose is set the belief-derived caps on remote\n// hops are not enforced, only the policy limits and our own balances, which\n// is what the desperation sweep needs in order to produce a route at all.\nfunc (r *router) makeRouteMode(path []*edge, amt lnwire.MilliSatoshi,\n\tloose bool) (*route.Route, float64, error) {\n\n\tif amt == 0 {\n\t\treturn nil, 0, errors.New(\"zero amount\")\n\t}\n\n\tn := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, n)\n\texpiry := make([]uint32, n)\n\n\tamtOver[n-1] = amt\n\texpiry[n-1] = finalCltvDelta\n\n\tfor i := n - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiry[i] = expiry[i+1] + uint32(fwd.cltv)\n\t}\n\n\tlogProb := 0.0\n\tfor i, e := range path {\n\t\tif !e.policyOK(amtOver[i]) {\n\t\t\treturn nil, 0, errors.New(\"policy violated\")\n\t\t}\n\t\tif e.from == r.source {\n\t\t\t// Our own balance is never negotiable.\n\t\t\tif amtOver[i] > r.availCap(e) {\n\t\t\t\treturn nil, 0, errors.New(\"over local balance\")\n\t\t\t}\n\t\t} else if !loose && amtOver[i] > r.availCap(e) {\n\t\t\treturn nil, 0, errors.New(\"above believed capacity\")\n\t\t}\n\t\tp := r.prob(e, amtOver[i])\n\t\tif p <= 0 {\n\t\t\tif !loose {\n\t\t\t\treturn nil, 0, errors.New(\"hopeless hop\")\n\t\t\t}\n\t\t\tp = minProb / 8\n\t\t}\n\t\tlogProb += math.Log(p)\n\t}\n\n\thops := make([]*route.Hop, n)\n\tfor i, e := range path {\n\t\tamtFwd := amt\n\t\toutExpiry := uint32(finalCltvDelta)\n\t\tif i < n-1 {\n\t\t\tamtFwd = amtOver[i+1]\n\t\t\toutExpiry = expiry[i+1]\n\t\t}\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: e.to,\n\t\t\tChannelID: e.chanID,\n\t\t\tAmtToForward: amtFwd,\n\t\t\tOutgoingTimeLock: outExpiry,\n\t\t}\n\t}\n\n\trt := &route.Route{\n\t\tTotalTimeLock: expiry[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}\n\n\treturn rt, math.Exp(logProb), nil\n}\n\n// makeRoute is makeRouteMode with the belief bounds enforced.\nfunc (r *router) makeRoute(path []*edge,\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\treturn r.makeRouteMode(path, amt, false)\n}\n\n// --- Shard planning -------------------------------------------------------\n\n// localBudget is the total liquidity we believe is spendable out of our own\n// channels right now, which upper-bounds any single shard.\nfunc (r *router) localBudget() lnwire.MilliSatoshi {\n\tvar total lnwire.MilliSatoshi\n\tseen := make(map[uint64]bool)\n\tfor _, e := range r.localEdges {\n\t\tif seen[e.chanID] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[e.chanID] = true\n\t\ttotal += r.availCap(e)\n\t}\n\treturn total\n}\n\n// widestLocal is the largest amount any single local channel can push. No\n// single shard can ever exceed it, so it anchors the amount ladder for large\n// payments and stops us opening with an attempt that could never have\n// worked.\nfunc (r *router) widestLocal() lnwire.MilliSatoshi {\n\tvar best lnwire.MilliSatoshi\n\tfor _, e := range r.localEdges {\n\t\tif c := r.availCap(e); c > best {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best\n}\n\n// bottleneck is the delivered amount a corridor is believed able to bear,\n// derived from each hop's safe capacity minus any provisional flow already\n// charged to that channel, with a small margin for fees.\nfunc (r *router) bottleneck(path []*edge,\n\textra map[uint64]lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tbn := lnwire.MilliSatoshi(math.MaxUint32) * 1024\n\tfor _, e := range path {\n\t\tc := r.safeCap(e)\n\t\tif used := extra[e.chanID]; used > 0 {\n\t\t\tif used >= c {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tc -= used\n\t\t}\n\t\tif c == 0 {\n\t\t\treturn 0\n\t\t}\n\t\tif c < bn {\n\t\t\tbn = c\n\t\t}\n\t}\n\tif bn < minShard {\n\t\treturn 0\n\t}\n\treturn bn - bn/100\n}\n\n// ladder builds a descending set of candidate shard sizes. It mixes even\n// splits over the parts we can still afford, a geometric descent that\n// always reaches genuinely small amounts, and evidence-derived sizes just\n// below proven failure points.\nfunc (r *router) ladder(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tset := make(map[lnwire.MilliSatoshi]bool)\n\tvar out []lnwire.MilliSatoshi\n\tadd := func(a lnwire.MilliSatoshi) {\n\t\tif a < minShard || a > hi || set[a] {\n\t\t\treturn\n\t\t}\n\t\tset[a] = true\n\t\tout = append(out, a)\n\t}\n\n\tadd(hi)\n\n\t// Even splits over the part counts we could still afford. This is the\n\t// primary sizing signal for a large payment: if the remainder needs N\n\t// parts, the shard we should be searching for is remaining/N, not the\n\t// whole remainder.\n\tmaxK := partsLeft\n\tif maxK > 12 {\n\t\tmaxK = 12\n\t}\n\tfor k := uint32(2); k <= maxK; k++ {\n\t\tadd(remaining / lnwire.MilliSatoshi(k))\n\t}\n\n\t// Geometric descent all the way down to a small floor.\n\tfloor := hi / 4096\n\tif floor < minShard {\n\t\tfloor = minShard\n\t}\n\tcur := hi\n\tfor i := 0; i < 16; i++ {\n\t\tcur = cur / 2\n\t\tif cur < floor {\n\t\t\tbreak\n\t\t}\n\t\tadd(cur)\n\t}\n\n\t// Evidence-derived sizes: believed-available amounts and just under\n\t// proven failure points.\n\tfor k, b := range r.beliefs {\n\t\tif b.dead {\n\t\t\tcontinue\n\t\t}\n\t\tif b.okAmt >= minShard {\n\t\t\tadd(b.okAmt)\n\t\t}\n\t\tif b.hasFail {\n\t\t\tif lim := retryLimit(b, r.capOf(k)); lim >= minShard {\n\t\t\t\tadd(lim)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(out, func(i, j int) bool { return out[i] > out[j] })\n\treturn out\n}\n\n// inFlightChans returns the channels currently carrying our own HTLCs.\nfunc (r *router) inFlightChans() map[uint64]bool {\n\tm := make(map[uint64]bool)\n\tfor _, rt := range r.pending {\n\t\tfor _, h := range rt.Hops {\n\t\t\tm[h.ChannelID] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// score rates a candidate shard: probability-weighted delivered value, with\n// a bonus for finishing the payment outright, a light fee penalty, and a\n// cost for every additional part the split implies.\nfunc (r *router) score(a lnwire.MilliSatoshi, p float64,\n\tfee lnwire.MilliSatoshi, remaining lnwire.MilliSatoshi) float64 {\n\n\tv := p * float64(a)\n\tif a >= remaining {\n\t\tv *= 1.15\n\t}\n\tv -= feeWeight * float64(fee)\n\n\tparts := float64(remaining) / float64(a)\n\tif parts > 1 {\n\t\tv -= partCost * float64(remaining) * (parts - 1)\n\t}\n\treturn v\n}\n\n// planSig identifies a (path, delivered amount) attempt.\nfunc planSig(path []*edge, amt lnwire.MilliSatoshi) string {\n\tbuf := make([]byte, 0, len(path)*14+14)\n\tfor _, e := range path {\n\t\tbuf = strconv.AppendUint(buf, e.chanID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(buf, uint64(amt)/1024, 36)\n\treturn string(buf)\n}\n\n// routeSig is planSig for an already-built route.\nfunc routeSig(rt *route.Route) string {\n\tn := len(rt.Hops)\n\tif n == 0 {\n\t\treturn \"\"\n\t}\n\tbuf := make([]byte, 0, n*14+14)\n\tfor _, h := range rt.Hops {\n\t\tbuf = strconv.AppendUint(buf, h.ChannelID, 36)\n\t\tbuf = append(buf, ':')\n\t}\n\tbuf = strconv.AppendUint(\n\t\tbuf, uint64(rt.Hops[n-1].AmtToForward)/1024, 36,\n\t)\n\treturn string(buf)\n}\n\ntype plan struct {\n\tpath []*edge\n\trt *route.Route\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\tprob float64\n\n\t// reserved marks that this plan currently holds soft reservations on\n\t// its channels, which must be released before it is dropped.\n\treserved bool\n}\n\n// reserve and release move a queued shard's soft reservation in and out of\n// the beliefs, so the planner never double-spends believed liquidity.\nfunc (r *router) reserve(pl *plan) {\n\tif pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tr.bel(hopKey(h)).reserved += hopAmount(pl.rt, i)\n\t}\n\tpl.reserved = true\n}\n\nfunc (r *router) release(pl *plan) {\n\tif !pl.reserved || pl.rt == nil {\n\t\treturn\n\t}\n\tfor i, h := range pl.rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(pl.rt, i)\n\t\tif b.reserved >= a {\n\t\t\tb.reserved -= a\n\t\t} else {\n\t\t\tb.reserved = 0\n\t\t}\n\t}\n\tpl.reserved = false\n}\n\n// dropQueue releases every queued shard's reservation and empties the queue.\nfunc (r *router) dropQueue() {\n\tfor _, pl := range r.queued {\n\t\tr.release(pl)\n\t}\n\tr.queued = nil\n}\n\n// evalPath scores a corridor at the requested amount and at the amount the\n// corridor is believed able to bear, keeping the better of the two.\nfunc (r *router) evalPath(path []*edge, a, remaining lnwire.MilliSatoshi,\n\tbest *plan) *plan {\n\n\tcands := []lnwire.MilliSatoshi{a}\n\tif bn := r.bottleneck(path, nil); bn >= minShard {\n\t\tg := bn\n\t\tif g > remaining {\n\t\t\tg = remaining\n\t\t}\n\t\tif g != a {\n\t\t\tcands = append(cands, g)\n\t\t}\n\t}\n\n\tfor _, c := range cands {\n\t\tif c < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(path, c)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(path, c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfee := rt.TotalAmount - c\n\t\ts := r.score(c, p, fee, remaining)\n\t\tif best == nil || s > best.score {\n\t\t\tbest = &plan{\n\t\t\t\tpath: path, rt: rt, score: s, amt: c, prob: p,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn best\n}\n\n// bestOnPath prices a corridor at descending amounts and returns the best\n// novel attempt it can still carry.\nfunc (r *router) bestOnPath(path []*edge,\n\thi, remaining lnwire.MilliSatoshi) *plan {\n\n\ta := hi\n\tfor i := 0; i < 12 && a >= minShard; i++ {\n\t\tif !r.failedSigs[planSig(path, a)] {\n\t\t\trt, p, err := r.makeRoute(path, a)\n\t\t\tif err == nil {\n\t\t\t\tfee := rt.TotalAmount - a\n\t\t\t\treturn &plan{\n\t\t\t\t\tpath: path,\n\t\t\t\t\trt: rt,\n\t\t\t\t\tamt: a,\n\t\t\t\t\tprob: p,\n\t\t\t\t\tscore: r.score(a, p, fee, remaining),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ta = a * 3 / 4\n\t}\n\treturn nil\n}\n\n// findFlowPath looks for a corridor able to carry hi under the provisional\n// flow already assigned, falling back down the amount ladder when nothing\n// can take the full amount.\nfunc (r *router) findFlowPath(hi, remaining lnwire.MilliSatoshi,\n\tpartsLeft uint32, avoid map[uint64]bool,\n\textra map[uint64]lnwire.MilliSatoshi) []*edge {\n\n\t// Prefer a corridor that does not touch the provisional flow at all,\n\t// then allow reuse of already-loaded channels at a cost penalty.\n\tfor _, pen := range []float64{1e9, 1.35} {\n\t\tp, err := r.findPath(hi, avoid, extra, pen, false)\n\t\tif err == nil {\n\t\t\treturn p\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range r.ladder(hi, remaining, partsLeft) {\n\t\t\tif a >= hi {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif probes >= 5 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tprobes++\n\t\t\tp, err := r.findPath(a, avoid, extra, pen, false)\n\t\t\tif err == nil {\n\t\t\t\treturn p\n\t\t\t}\n\t\t}\n\t\tif len(extra) == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// planFlow decomposes the remaining amount over several corridors, sizing\n// each shard to what that corridor's weakest hop is believed able to bear\n// under the flow already assigned. Channels may be shared between shards as\n// long as their believed room covers the sum, which is what makes this a\n// min-cost-flow decomposition rather than a disjoint-path search.\n//\n// The target shard size is the remainder divided by the parts still to come,\n// not the whole remainder. Aiming a corridor search at the whole remainder\n// on a large payment finds nothing and wastes the round; aiming it at the\n// natural shard size finds a corridor that can actually bear it, and the\n// rebalance sweep afterwards grows the ones with slack.\nfunc (r *router) planFlow(remaining lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) []*plan {\n\n\tif partsLeft < 2 {\n\t\treturn nil\n\t}\n\n\tavoid := make(map[uint64]bool, len(busy)+8)\n\tfor c := range busy {\n\t\tavoid[c] = true\n\t}\n\n\t// extra tracks the flow provisionally routed over each channel, in\n\t// delivered-amount terms, which is close enough for sizing.\n\textra := make(map[uint64]lnwire.MilliSatoshi)\n\n\ttype corridor struct {\n\t\tpath []*edge\n\t\tamt lnwire.MilliSatoshi\n\t}\n\n\tvar cors []corridor\n\tleft := remaining\n\n\trounds := partsLeft\n\tif rounds > flowRounds {\n\t\trounds = flowRounds\n\t}\n\n\tfor k := uint32(0); k < rounds; k++ {\n\t\tif left < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\t// Aim at the natural shard size for the parts still to come,\n\t\t// biased a little high so early corridors take the fat share\n\t\t// and the rebalance sweep has something to work with.\n\t\tpartsRem := rounds - k\n\t\taim := left\n\t\tif partsRem > 1 {\n\t\t\taim = left / lnwire.MilliSatoshi(partsRem)\n\t\t\taim += aim / 2\n\t\t\tif aim > left {\n\t\t\t\taim = left\n\t\t\t}\n\t\t}\n\t\tif aim < minShard {\n\t\t\taim = minShard\n\t\t}\n\n\t\tpath := r.findFlowPath(aim, remaining, partsLeft, avoid, extra)\n\t\tif path == nil && aim < left {\n\t\t\tpath = r.findFlowPath(\n\t\t\t\tleft, remaining, partsLeft, avoid, extra,\n\t\t\t)\n\t\t}\n\t\tif path == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tamtS := aim\n\t\tif amtS > left {\n\t\t\tamtS = left\n\t\t}\n\t\tif bn := r.bottleneck(path, extra); bn < amtS {\n\t\t\tamtS = bn\n\t\t}\n\t\tif amtS < minShard {\n\t\t\tbreak\n\t\t}\n\n\t\tcors = append(cors, corridor{path: path, amt: amtS})\n\t\tfor _, e := range path {\n\t\t\textra[e.chanID] += amtS\n\t\t}\n\t\tleft -= amtS\n\t}\n\n\tif len(cors) == 0 {\n\t\treturn nil\n\t}\n\n\t// Rebalance: hand leftover amount to whichever corridor still has the\n\t// most slack under the current assignment. This is what produces the\n\t// deliberately unequal split.\n\tfor round := 0; round < flowRebalance && left >= minShard; round++ {\n\t\tprogress := false\n\t\tfor i := range cors {\n\t\t\tif left < minShard {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Temporarily remove this corridor's own contribution\n\t\t\t// so its slack is measured against the others only.\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\tif extra[e.chanID] >= cors[i].amt {\n\t\t\t\t\textra[e.chanID] -= cors[i].amt\n\t\t\t\t} else {\n\t\t\t\t\textra[e.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\troom := r.bottleneck(cors[i].path, extra)\n\t\t\tgrow := lnwire.MilliSatoshi(0)\n\t\t\tif room > cors[i].amt {\n\t\t\t\tgrow = room - cors[i].amt\n\t\t\t\tif grow > left {\n\t\t\t\t\tgrow = left\n\t\t\t\t}\n\t\t\t}\n\t\t\tcors[i].amt += grow\n\t\t\tfor _, e := range cors[i].path {\n\t\t\t\textra[e.chanID] += cors[i].amt\n\t\t\t}\n\t\t\tif grow > 0 {\n\t\t\t\tleft -= grow\n\t\t\t\tprogress = true\n\t\t\t}\n\t\t}\n\t\tif !progress {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Price every corridor and keep the ones that survive.\n\tvar out []*plan\n\tfor _, c := range cors {\n\t\tif c.amt < minShard {\n\t\t\tcontinue\n\t\t}\n\t\tpl := r.bestOnPath(c.path, c.amt, remaining)\n\t\tif pl == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, pl)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil\n\t}\n\n\t// Largest shard first: it is the one most likely to be unroutable\n\t// later, so it should go out while the network state is freshest.\n\tsort.SliceStable(out, func(i, j int) bool {\n\t\treturn out[i].amt > out[j].amt\n\t})\n\n\treturn out\n}\n\n// planTotal is the believed-deliverable value of a route-set plan: each\n// shard's amount weighted by its own success probability.\nfunc planTotal(pls []*plan) (lnwire.MilliSatoshi, float64) {\n\tvar total lnwire.MilliSatoshi\n\tvar weighted float64\n\tfor _, pl := range pls {\n\t\ttotal += pl.amt\n\t\tweighted += pl.prob * float64(pl.amt)\n\t}\n\treturn total, weighted\n}\n\n// pathBusy reports whether a corridor touches a channel already carrying one\n// of our in-flight shards.\nfunc pathBusy(path []*edge, busy map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif busy[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// touches reports whether a corridor uses any of the given channels.\nfunc touches(path []*edge, chans map[uint64]bool) bool {\n\tfor _, e := range path {\n\t\tif chans[e.chanID] {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// serveQueued hands out the next viable shard of an existing plan.\nfunc (r *router) serveQueued(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\tfor len(r.queued) > 0 {\n\t\tpl := r.queued[0]\n\t\tr.queued = r.queued[1:]\n\t\tr.release(pl)\n\n\t\ta := pl.amt\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tif a < minShard {\n\t\t\tcontinue\n\t\t}\n\t\t// A partial shard is useless when we cannot follow it up.\n\t\tif partsLeft <= 1 && a < amt {\n\t\t\tr.dropQueue()\n\t\t\treturn nil\n\t\t}\n\t\tif pathBusy(pl.path, busy) {\n\t\t\tcontinue\n\t\t}\n\t\tif r.failedSigs[planSig(pl.path, a)] {\n\t\t\tcontinue\n\t\t}\n\t\trt, p, err := r.makeRoute(pl.path, a)\n\t\tif err != nil || p < queueMinProb {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn rt\n\t}\n\treturn nil\n}\n\n// sweep is the last resort before giving up on a payment. It walks a wide\n// geometric ladder from whatever a single local channel could bear down to a\n// tiny floor, over every corridor the graph still offers, ignoring the score\n// model and the belief-derived caps on remote hops. Only policy limits, our\n// own balances, and permanently dead channels are respected.\n//\n// This exists because the previous revision's terminal error was almost\n// always premature: the scored search found nothing at its preferred sizes\n// and the router quit while a small shard was still perfectly routable and\n// the part budget was untouched.\nfunc (r *router) sweep(amt lnwire.MilliSatoshi, partsLeft uint32,\n\tbusy map[uint64]bool) *route.Route {\n\n\thi := amt\n\tif w := r.widestLocal(); w > 0 && w < hi {\n\t\thi = w\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\t// A partial shard is only worth sending when we can follow it up.\n\tfloor := minShard\n\tif partsLeft <= 1 {\n\t\tfloor = amt\n\t}\n\tif floor > hi {\n\t\treturn nil\n\t}\n\n\tavoidSets := []map[uint64]bool{busy, nil}\n\tif len(busy) == 0 {\n\t\tavoidSets = avoidSets[1:]\n\t}\n\n\ta := hi\n\tfor i := 0; i < sweepSteps && a >= floor; i++ {\n\t\tif a > amt {\n\t\t\ta = amt\n\t\t}\n\t\tfor _, avoid := range avoidSets {\n\t\t\tfor _, loose := range []bool{false, true} {\n\t\t\t\tpath, err := r.findPath(a, avoid, nil, 1, loose)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif r.failedSigs[planSig(path, a)] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\trt, _, err := r.makeRouteMode(path, a, loose)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif r.failedSigs[routeSig(rt)] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn rt\n\t\t\t}\n\t\t}\n\t\tif a == floor {\n\t\t\tbreak\n\t\t}\n\t\tnext := a * 5 / 8\n\t\tif next < floor {\n\t\t\tnext = floor\n\t\t}\n\t\ta = next\n\t}\n\n\treturn nil\n}\n\n// RequestRoute plans the next shard. It first serves any shard left over\n// from a joint route-set plan, then searches jointly over shard amount and\n// corridor, then compares the best single shard against a full\n// min-cost-flow style route set over several corridors, and finally falls\n// back to a desperation sweep rather than abandoning the payment.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt == 0 {\n\t\treturn nil, errors.New(\"zero amount\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\tif r.failStreak >= maxFailStreak {\n\t\treturn nil, errors.New(\"no progress\")\n\t}\n\n\tpartsLeft := uint32(1)\n\tif r.spec.MaxParts > inFlightHtlcs {\n\t\tpartsLeft = r.spec.MaxParts - inFlightHtlcs\n\t}\n\n\tbusy := r.inFlightChans()\n\n\tif rt := r.serveQueued(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\t// A shard can never exceed what our own channels can push right now,\n\t// and no single shard can exceed the widest single local channel.\n\t// Anchoring the ladder there stops a large payment from opening with\n\t// an attempt that could never have worked.\n\thi := amt\n\tif budget := r.localBudget(); budget > 0 && budget < hi {\n\t\thi = budget\n\t}\n\tif w := r.widestLocal(); w > 0 && w < hi {\n\t\thi = w\n\t}\n\tif hi < minShard {\n\t\thi = amt\n\t}\n\n\tladder := r.ladder(hi, amt, partsLeft)\n\n\t// Spend fewer probes once the payment has been fighting for a while:\n\t// at that point the beliefs are informative and extra Dijkstra runs\n\t// mostly rediscover corridors we have already ruled out.\n\tbudgetProbes := probeBudget\n\tif r.failStreak > 6 {\n\t\tbudgetProbes = probeBudget / 2\n\t}\n\tif budgetProbes < 4 {\n\t\tbudgetProbes = 4\n\t}\n\n\tvar best *plan\n\ttried := make(map[string]bool)\n\n\t// First pass prefers corridors disjoint from in-flight shards so\n\t// parallel parts do not fight over the same liquidity.\n\tfor pass := 0; pass < 2; pass++ {\n\t\tvar avoid map[uint64]bool\n\t\tif pass == 0 {\n\t\t\tif len(busy) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tavoid = busy\n\t\t}\n\n\t\tprobes := 0\n\t\tfor _, a := range ladder {\n\t\t\t// Once we hold a solid plan, digging far below it only\n\t\t\t// wastes attempts and parts.\n\t\t\tif best != nil && best.prob >= 0.6 &&\n\t\t\t\ta < best.amt/2 {\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif probes >= budgetProbes {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpath, err := r.plainPath(a, avoid)\n\t\t\tprobes++\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsig := planSig(path, a)\n\t\t\tif tried[sig] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttried[sig] = true\n\n\t\t\tbest = r.evalPath(path, a, amt, best)\n\n\t\t\t// A confident full-amount plan needs no alternatives.\n\t\t\tif best != nil && best.amt >= amt && best.prob > 0.8 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif best != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Plan the split deliberately over several corridors whenever a single\n\t// shard cannot confidently cover the remainder. This fires eagerly,\n\t// not only as a fallback, because a joint plan whose shards each fit\n\t// their corridor beats one oversized shard that will fail and then be\n\t// halved blindly.\n\tsingleWeak := best == nil || best.amt < amt || best.prob < 0.55\n\n\tif partsLeft > 1 && singleWeak {\n\t\tflow := r.planFlow(amt, partsLeft, busy)\n\t\tif len(flow) > 0 {\n\t\t\ttotal, weighted := planTotal(flow)\n\n\t\t\tvar bestVal float64\n\t\t\tvar bestAmt lnwire.MilliSatoshi\n\t\t\tif best != nil {\n\t\t\t\tbestVal = best.prob * float64(best.amt)\n\t\t\t\tbestAmt = best.amt\n\t\t\t}\n\n\t\t\t// Take the plan when it either covers more of the\n\t\t\t// remainder or delivers more expected value than the\n\t\t\t// single shard on its own.\n\t\t\tbetter := best == nil || total > bestAmt ||\n\t\t\t\tweighted > bestVal*1.05\n\n\t\t\tif better && len(flow) > 1 {\n\t\t\t\tfirst := flow[0]\n\t\t\t\trest := flow[1:]\n\t\t\t\tr.dropQueue()\n\t\t\t\tr.queued = rest\n\t\t\t\tfor _, pl := range r.queued {\n\t\t\t\t\tr.reserve(pl)\n\t\t\t\t}\n\t\t\t\tbest = first\n\t\t\t} else if best == nil || (better && total > bestAmt) {\n\t\t\t\tbest = flow[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif best != nil {\n\t\tr.attempts++\n\t\treturn best.rt, nil\n\t}\n\n\t// Desperation: anything routable beats abandoning the payment.\n\tif rt := r.sweep(amt, partsLeft, busy); rt != nil {\n\t\tr.attempts++\n\t\treturn rt, nil\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\n// hopAmount returns the amount flowing over hop index i of a route.\nfunc hopAmount(rt *route.Route, i int) lnwire.MilliSatoshi {\n\tif i == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[i-1].AmtToForward\n}\n\nfunc hopKey(h *route.Hop) edgeKey {\n\treturn edgeKey{chanID: h.ChannelID, to: h.PubKeyBytes}\n}\n\n// markInFlight adjusts the in-flight accounting for a route.\nfunc (r *router) markInFlight(rt *route.Route, sign int) {\n\tfor i, h := range rt.Hops {\n\t\tb := r.bel(hopKey(h))\n\t\ta := hopAmount(rt, i)\n\t\tif sign > 0 {\n\t\t\tb.inFlight += a\n\t\t} else if b.inFlight >= a {\n\t\t\tb.inFlight -= a\n\t\t} else {\n\t\t\tb.inFlight = 0\n\t\t}\n\t}\n}\n\n// noteRoute records the route as pending and reserves its liquidity.\nfunc (r *router) noteRoute(attemptID uint64, rt *route.Route) {\n\tif _, ok := r.pending[attemptID]; ok {\n\t\treturn\n\t}\n\tr.pending[attemptID] = rt\n\tr.markInFlight(rt, +1)\n}\n\n// provePassed raises the lower bound on a hop that demonstrably forwarded\n// but whose HTLC did not settle, so no liquidity actually moved.\nfunc (r *router) provePassed(h *route.Hop, a lnwire.MilliSatoshi) {\n\tb := r.bel(hopKey(h))\n\tif a > b.okAmt {\n\t\tb.okAmt = a\n\t}\n\tb.succ = true\n\n\t// A success invalidates any older failure bound at or below this\n\t// amount, and clears accumulated suspicion.\n\tif b.hasFail && b.failAmt <= a {\n\t\tb.hasFail = false\n\t\tb.failAmt = 0\n\t}\n\tb.misses = 0\n}\n\n// applySettle folds a settled forward into both directions of the channel:\n// the sending direction lost exactly that much liquidity and the receiving\n// direction gained it. Keeping this bookkeeping straight is what lets\n// evidence from earlier payments in a batch stay usable.\nfunc (r *router) applySettle(from route.Vertex, h *route.Hop,\n\ta lnwire.MilliSatoshi) {\n\n\tk := hopKey(h)\n\tb := r.bel(k)\n\tb.succ = true\n\tif b.okAmt < a {\n\t\tb.okAmt = a\n\t}\n\tb.okAmt -= a\n\tb.drained += a\n\tif b.hasFail {\n\t\tif b.failAmt > a {\n\t\t\tb.failAmt -= a\n\t\t} else {\n\t\t\tb.failAmt = 1\n\t\t}\n\t}\n\tb.misses = 0\n\n\t// The reverse direction gained exactly what we pushed.\n\trk := edgeKey{chanID: k.chanID, to: from}\n\trb := r.bel(rk)\n\trb.okAmt += a\n\trb.misses = 0\n\tif rb.drained > a {\n\t\trb.drained -= a\n\t} else {\n\t\trb.drained = 0\n\t}\n\tif rb.hasFail {\n\t\trb.failAmt += a\n\t\tif c := r.capOf(rk); c > 0 && rb.failAmt >= c {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t\tif rb.hasFail && rb.okAmt >= rb.failAmt {\n\t\t\trb.hasFail = false\n\t\t\trb.failAmt = 0\n\t\t}\n\t}\n}\n\n// classify buckets a failure into the kind of repair it implies. The string\n// form is used so this works whatever concrete shape the failure takes.\nfunc classify(f any) string {\n\td := fmt.Sprintf(\"%T %v\", f, f)\n\tswitch {\n\tcase strings.Contains(d, \"FeeInsufficient\"):\n\t\treturn \"fee\"\n\tcase strings.Contains(d, \"CltvExpiry\"),\n\t\tstrings.Contains(d, \"ExpiryTooSoon\"):\n\t\treturn \"cltv\"\n\tcase strings.Contains(d, \"AmountBelowMinimum\"):\n\t\treturn \"min\"\n\tcase strings.Contains(d, \"ChannelDisabled\"),\n\t\tstrings.Contains(d, \"UnknownNextPeer\"),\n\t\tstrings.Contains(d, \"PermanentChannelFailure\"):\n\t\treturn \"dead\"\n\t}\n\treturn \"liquidity\"\n}\n\n// pruneQueue drops only the queued shards whose corridors are implicated by\n// a failure, keeping the rest of an otherwise sound plan.\nfunc (r *router) pruneQueue(hit map[uint64]bool) {\n\tkept := r.queued[:0]\n\tfor _, pl := range r.queued {\n\t\tif touches(pl.path, hit) {\n\t\t\tr.release(pl)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, pl)\n\t}\n\tr.queued = kept\n}\n\n// ReportAttempt folds attempt feedback into the liquidity beliefs.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *router) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\t// Release any liquidity we had reserved for this attempt.\n\tif prev, ok := r.pending[attemptID]; ok {\n\t\tr.markInFlight(prev, -1)\n\t\tdelete(r.pending, attemptID)\n\t}\n\n\t// Success: every hop carried its amount and the liquidity moved, so\n\t// shift both directions of every channel on the route.\n\tif result.Failure == nil {\n\t\tr.failStreak = 0\n\t\tprev := rt.SourcePubKey\n\t\tfor i, h := range rt.Hops {\n\t\t\ta := hopAmount(rt, i)\n\t\t\tr.applySettle(prev, h, a)\n\t\t\tprev = h.PubKeyBytes\n\n\t\t\tif i == 0 {\n\t\t\t\tif bal, ok := r.localBalances[h.ChannelID]; ok {\n\t\t\t\t\tif bal >= a {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] =\n\t\t\t\t\t\t\tbal - a\n\t\t\t\t\t} else {\n\t\t\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// A settle changes the liquidity the queued shards were sized\n\t\t// against, so re-validate them lazily: shards over channels\n\t\t// this route drained are dropped, the rest survive.\n\t\tif len(r.queued) > 0 {\n\t\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\t\tfor _, h := range rt.Hops {\n\t\t\t\thit[h.ChannelID] = true\n\t\t\t}\n\t\t\tr.pruneQueue(hit)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failStreak++\n\tif sig := routeSig(rt); sig != \"\" {\n\t\tr.failedSigs[sig] = true\n\t}\n\n\t// Locate the failing hop: the failure source is the node that could\n\t// not forward, so the offending channel is its outgoing hop.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t} else {\n\t\tfor i, h := range rt.Hops {\n\t\t\tif h.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIdx = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// An unattributable failure. Previously this only spread mild\n\t// suspicion, which cost an attempt and taught almost nothing: the\n\t// next search happily re-derived the same corridor. Now we also cap\n\t// the corridor's NARROWEST remote hop just under the amount it was\n\t// asked to carry, which is the single most likely culprit under a\n\t// bimodal split, so the search genuinely moves elsewhere.\n\tif failIdx < 0 || failIdx >= len(rt.Hops) {\n\t\thit := make(map[uint64]bool, len(rt.Hops))\n\t\tnarrowIdx := -1\n\t\tvar narrowCap lnwire.MilliSatoshi\n\t\tfor i, h := range rt.Hops {\n\t\t\thit[h.ChannelID] = true\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif b.misses < 3 {\n\t\t\t\tb.misses++\n\t\t\t}\n\t\t\tc := r.capOf(hopKey(h))\n\t\t\tif narrowIdx < 0 || (c > 0 && c < narrowCap) {\n\t\t\t\tnarrowIdx = i\n\t\t\t\tnarrowCap = c\n\t\t\t}\n\t\t}\n\t\tif narrowIdx > 0 {\n\t\t\th := rt.Hops[narrowIdx]\n\t\t\ta := hopAmount(rt, narrowIdx)\n\t\t\tb := r.bel(hopKey(h))\n\t\t\tif !b.hasFail || a < b.failAmt {\n\t\t\t\tb.hasFail = true\n\t\t\t\tb.failAmt = a\n\t\t\t}\n\t\t\tif b.okAmt >= b.failAmt {\n\t\t\t\tif b.failAmt > 0 {\n\t\t\t\t\tb.okAmt = b.failAmt - 1\n\t\t\t\t} else {\n\t\t\t\t\tb.okAmt = 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Everything strictly before the failing hop demonstrably worked, but\n\t// nothing settled, so no liquidity actually moved.\n\tfor i := 0; i < failIdx; i++ {\n\t\tr.provePassed(rt.Hops[i], hopAmount(rt, i))\n\t}\n\n\th := rt.Hops[failIdx]\n\ta := hopAmount(rt, failIdx)\n\tb := r.bel(hopKey(h))\n\te := r.byKey[hopKey(h)]\n\n\t// Only the failing channel invalidates queued shards; a fee or cltv\n\t// repair does not touch liquidity at all, so those keep the plan.\n\thit := map[uint64]bool{h.ChannelID: true}\n\n\tswitch classify(result.Failure) {\n\tcase \"fee\":\n\t\t// Our fee view for this hop is stale: pay more next time. A\n\t\t// policy repair is not a liquidity miss, so it must not count\n\t\t// against the fail streak that ends the payment.\n\t\tif e != nil {\n\t\t\te.baseFee += e.baseFee/4 + 1_000\n\t\t\te.feePPM += e.feePPM/4 + 50\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"cltv\":\n\t\tif e != nil {\n\t\t\te.cltv += e.cltv/4 + 20\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"min\":\n\t\t// The advertised minimum is higher than we thought.\n\t\tif e != nil && a >= e.minHTLC {\n\t\t\te.minHTLC = a + 1\n\t\t}\n\t\tr.failStreak--\n\t\treturn nil\n\n\tcase \"dead\":\n\t\tb.dead = true\n\t\tr.pruneQueue(hit)\n\t\treturn nil\n\t}\n\n\t// Liquidity miss: tighten the upper bound on this direction.\n\tif !b.hasFail || a < b.failAmt {\n\t\tb.hasFail = true\n\t\tb.failAmt = a\n\t}\n\t// The failure bound must sit above the proven-good bound.\n\tif b.okAmt >= b.failAmt {\n\t\tif b.failAmt > 0 {\n\t\t\tb.okAmt = b.failAmt - 1\n\t\t} else {\n\t\t\tb.okAmt = 0\n\t\t}\n\t}\n\t// A hard miss overrides bimodal optimism from older evidence.\n\tb.succ = false\n\n\t// A local channel failing means our balance estimate was too high.\n\tif failIdx == 0 {\n\t\tif bal, ok := r.localBalances[h.ChannelID]; ok && bal >= a {\n\t\t\tif a == 0 {\n\t\t\t\tr.localBalances[h.ChannelID] = 0\n\t\t\t} else {\n\t\t\t\tr.localBalances[h.ChannelID] = a - 1\n\t\t\t}\n\t\t}\n\t}\n\n\tr.pruneQueue(hit)\n\n\treturn nil\n}"
}
}
]
}