mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-18 13:07:58 +02:00
840 lines
No EOL
624 KiB
JSON
840 lines
No EOL
624 KiB
JSON
{
|
|
"run_id": "code_drift1",
|
|
"reflection_lm": "codex:gpt-5.6-sol",
|
|
"mode": "generalization",
|
|
"status": "complete",
|
|
"seed_score": 0.3388,
|
|
"best_score": 0.9209,
|
|
"iterations": [
|
|
{
|
|
"i": 0,
|
|
"candidate_score": 0.3388,
|
|
"best_score": 0.3388,
|
|
"note": "seed"
|
|
},
|
|
{
|
|
"i": 1,
|
|
"candidate_score": 0.0284,
|
|
"best_score": 0.3388,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 2,
|
|
"candidate_score": 0.3368,
|
|
"best_score": 0.3388,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 3,
|
|
"candidate_score": -0.0137,
|
|
"best_score": 0.3388,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 4,
|
|
"candidate_score": 0.2391,
|
|
"best_score": 0.3388,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 5,
|
|
"candidate_score": 0.5106,
|
|
"best_score": 0.3388,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 6,
|
|
"candidate_score": 0.3699,
|
|
"best_score": 0.3699,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 7,
|
|
"candidate_score": 0.6151,
|
|
"best_score": 0.6151,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 8,
|
|
"candidate_score": 0.3522,
|
|
"best_score": 0.6151,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 9,
|
|
"candidate_score": 0.389,
|
|
"best_score": 0.6151,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 10,
|
|
"candidate_score": 0.1104,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 11,
|
|
"candidate_score": 0.5178,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 12,
|
|
"candidate_score": 0.349,
|
|
"best_score": 0.6151,
|
|
"note": "accepted"
|
|
},
|
|
{
|
|
"i": 13,
|
|
"candidate_score": 0.2423,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 14,
|
|
"candidate_score": 0.288,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 15,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 16,
|
|
"candidate_score": 0.4425,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 17,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 18,
|
|
"candidate_score": 0.3058,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 19,
|
|
"candidate_score": 0.5128,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 20,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 21,
|
|
"candidate_score": -0.0611,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 22,
|
|
"candidate_score": 0.2742,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 23,
|
|
"candidate_score": 0.3232,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 24,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 25,
|
|
"candidate_score": 0.2124,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 26,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 27,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 28,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 29,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 30,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 31,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 32,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 33,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 34,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 35,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 36,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 37,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 38,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 39,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 40,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 41,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 42,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 43,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 44,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 45,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 46,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 47,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 48,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 49,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
},
|
|
{
|
|
"i": 50,
|
|
"candidate_score": 0.0,
|
|
"best_score": 0.6151,
|
|
"note": "rejected"
|
|
}
|
|
],
|
|
"seed_params": {
|
|
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms. During\n// optimization, the entire file is replaced (via go build -overlay) with a\n// generated implementation. The contract is a single constructor:\n//\n//\tnewCandidateRouter(view, source, localBalances, spec)\n//\n// returning a routing.SimRouter. The router sees only the public gossip\n// graph, its own channel balances and per-attempt feedback \u2014 the same\n// information a real Lightning sender has. The in-tree implementation below\n// is the seed algorithm: a deliberately simple fee-optimizing Dijkstra with\n// failure blacklisting and halving-based MPP splitting.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\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\n// candidateEdge is one directed edge of the public graph: a channel from\n// one node to another, with the policy the sending node announced.\ntype candidateEdge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\n// fee returns the fee the sending node charges to forward amt over this\n// edge.\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\n// usable reports whether the edge can carry the given amount per its\n// announced policy.\nfunc (e *candidateEdge) usable(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\t// The public capacity is a hard upper bound on what can flow.\n\treturn amt <= e.capacity\n}\n\n// candidateRouter is the seed algorithm: cheapest-path routing with a\n// failure blacklist and amount halving when no route is found.\ntype candidateRouter struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// incomingEdges maps a node to the directed edges arriving at it,\n\t// the natural shape for backward Dijkstra.\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\n\t// localBalances is the exact outbound liquidity of our own channels.\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// failedAmt records, per directed channel, the lowest amount that\n\t// failed with a liquidity error; routes are built to stay below it.\n\tfailedAmt map[uint64]lnwire.MilliSatoshi\n\n\t// shardAmt is the current shard size for MPP splitting.\n\tshardAmt lnwire.MilliSatoshi\n\n\t// partsUsed counts the successful shards so far.\n\tpartsUsed uint32\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n}\n\n// newCandidateRouter builds the router for one payment. This signature is\n// the stable contract between the harness and generated candidates.\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\trouter := &candidateRouter{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\tfailedAmt: make(map[uint64]lnwire.MilliSatoshi),\n\t\tshardAmt: spec.Amount,\n\t\tpending: make(map[uint64]*route.Route),\n\t}\n\n\t// Build the adjacency list from gossip. Iterating a node's channels\n\t// yields, per channel, the policy the OTHER node announced toward us\n\t// (InPolicy). That is exactly the policy governing the directed edge\n\t// other -> node, so we record the reversed edge at each visit.\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\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\tedge := &candidateEdge{\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\tbaseFeeMsat: pol.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: 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\tedge.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.incomingEdges[edge.to] = append(\n\t\t\t\t\trouter.incomingEdges[edge.to], edge,\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 router, nil\n}\n\n// dijkstraItem is a priority queue entry.\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tcost lnwire.MilliSatoshi\n\tidx int\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int { return len(q) }\nfunc (q dijkstraQueue) Less(i, j int) bool { return q[i].cost < q[j].cost }\nfunc (q dijkstraQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *dijkstraQueue) Push(x any) {\n\titem := x.(*dijkstraItem)\n\titem.idx = len(*q)\n\t*q = append(*q, item)\n}\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\n// findRoute computes the cheapest usable path delivering amt to the target,\n// walking backward from the target so fees accumulate correctly.\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi) (*route.Route,\n\terror) {\n\n\t// dist[node] = amount that must arrive at node to deliver amt.\n\tdist := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tdist[r.spec.Target] = amt\n\tpq := &dijkstraQueue{}\n\theap.Push(pq, &dijkstraItem{node: r.spec.Target, cost: amt})\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*dijkstraItem)\n\t\tnode, arriving := item.node, item.cost\n\n\t\tif arriving > dist[node] {\n\t\t\tcontinue\n\t\t}\n\t\tif node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\t// Consider all edges INTO node: for edge u->node, u must\n\t\t// send arriving plus u's fee.\n\t\tfor _, edge := range r.incomingEdges[node] {\n\t\t\tamtOver := arriving\n\n\t\t\tif !edge.usable(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Skip channels whose liquidity failure bound says\n\t\t\t// this amount cannot pass.\n\t\t\tif bound, ok := r.failedAmt[edge.chanID]; ok &&\n\t\t\t\tamtOver >= bound {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Our own channels: check exact local balance.\n\t\t\tif edge.from == r.source {\n\t\t\t\tif r.localBalances[edge.chanID] < amtOver {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif edge.from == r.source {\n\t\t\t\t// We pay no fee to ourselves.\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + edge.fee(amtOver)\n\t\t\t}\n\n\t\t\tbest, ok := dist[edge.from]\n\t\t\tif !ok || sending < best {\n\t\t\t\tdist[edge.from] = sending\n\t\t\t\tnext[edge.from] = edge\n\t\t\t\theap.Push(pq, &dijkstraItem{\n\t\t\t\t\tnode: edge.from,\n\t\t\t\t\tcost: sending,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, ok := dist[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\n// buildRoute walks the next-pointers from source to target and constructs a\n// route with correctly accumulated fees and cltv deltas.\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tconst finalCltvDelta = 40\n\n\t// Collect the path edges source -> target.\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\t// Amounts and expiries per channel, computed backward.\n\tnumHops := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, numHops)\n\texpiryOver := make([]uint32, numHops)\n\n\tamtOver[numHops-1] = amt\n\texpiryOver[numHops-1] = finalCltvDelta\n\n\tfor i := numHops - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiryOver[i] = expiryOver[i+1] +\n\t\t\tuint32(fwd.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, numHops)\n\tfor i, edge := range path {\n\t\tamtToFwd := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\t\tif i < numHops-1 {\n\t\t\tamtToFwd = amtOver[i+1]\n\t\t\toutgoingExpiry = expiryOver[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amtToFwd,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiryOver[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// RequestRoute returns the next route to try: the cheapest path for the\n// current shard size, halving the shard when no route exists.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif r.shardAmt > amt {\n\t\tr.shardAmt = amt\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(r.shardAmt)\n\t\tif err == nil {\n\t\t\treturn rt, nil\n\t\t}\n\n\t\t// No route at this shard size: split if we're allowed more\n\t\t// parts and the shard is still meaningfully large.\n\t\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\t\tif partsLeft <= 1 || r.shardAmt < 10_000_000 {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.shardAmt /= 2\n\t}\n}\n\n// ReportAttempt learns from an attempt: liquidity failures set an upper\n// bound on the failing channel.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif result.Failure == nil {\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop and record the amount bound on its\n\t// outgoing channel.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t}\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\tfailIdx = i + 1\n\t\t}\n\t}\n\n\t// The failing node could not forward over its outgoing channel,\n\t// which is rt.Hops[failIdx].\n\tif failIdx >= 0 && failIdx < len(rt.Hops) {\n\t\thop := rt.Hops[failIdx]\n\t\tamtOver := rt.TotalAmount\n\t\tif failIdx > 0 {\n\t\t\tamtOver = rt.Hops[failIdx-1].AmtToForward\n\t\t}\n\n\t\tbound, ok := r.failedAmt[hop.ChannelID]\n\t\tif !ok || amtOver < bound {\n\t\t\tr.failedAmt[hop.ChannelID] = amtOver\n\t\t}\n\t}\n\n\treturn nil\n}\n"
|
|
},
|
|
"best_candidate": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateMinShard = lnwire.MilliSatoshi(5_000_000)\n\tcandidateRiskCost = 2_000_000.0\n\tcandidateHopCost = 25_000.0\n\tcandidateFailureCost = 8_000_000.0\n\tcandidateReservedCost = 2_000_000.0\n\tcandidateEvidenceLife = 10 * time.Minute\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n}\n\ntype candidateGlobalBelief struct {\n\tquality float64\n\tupdated time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.Mutex\n\n\tbeliefs map[candidateEdgeKey]candidateGlobalBelief\n\tlastViewNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateGlobalBelief),\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tnow time.Time\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\tlocalReserved map[uint64]lnwire.MilliSatoshi\n\n\t// lowerOK records amounts recently proven to traverse a channel.\n\tlowerOK map[candidateEdgeKey]lnwire.MilliSatoshi\n\n\t// upperFail records amounts that failed during the current payment.\n\t// Amounts at or above this bound are excluded, while smaller retries\n\t// remain available.\n\tupperFail map[candidateEdgeKey]lnwire.MilliSatoshi\n\n\tquality map[candidateEdgeKey]float64\n\tfailures map[candidateEdgeKey]uint32\n\tpolicyBad map[candidateEdgeKey]bool\n\tinUse map[candidateEdgeKey]uint32\n\tcurrentShard 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\tnow := view.Now()\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tnow: now,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalReserved: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlowerOK: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tupperFail: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tquality: make(map[candidateEdgeKey]float64),\n\t\tfailures: make(map[candidateEdgeKey]uint32),\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t\tinUse: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tr.loadBeliefs(now)\n\n\tparts := spec.MaxParts\n\tif parts == 0 {\n\t\tparts = 1\n\t}\n\tr.currentShard = divideCandidateAmount(spec.Amount, parts)\n\tif r.currentShard < candidateMinShard {\n\t\tr.currentShard = candidateMinShard\n\t}\n\tif r.currentShard > spec.Amount {\n\t\tr.currentShard = spec.Amount\n\t}\n\n\treturn r, nil\n}\n\nfunc divideCandidateAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc (r *candidateRouter) loadBeliefs(now time.Time) {\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tif !candidateKnowledge.lastViewNow.IsZero() &&\n\t\tnow.Before(candidateKnowledge.lastViewNow) {\n\n\t\tcandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]candidateGlobalBelief)\n\t}\n\tcandidateKnowledge.lastViewNow = now\n\n\tfor key := range r.edges {\n\t\tbelief, ok := candidateKnowledge.beliefs[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tage := now.Sub(belief.updated)\n\t\tif age < 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdecay := math.Exp(\n\t\t\t-float64(age) / float64(candidateEvidenceLife),\n\t\t)\n\t\tr.quality[key] = belief.quality * decay\n\t}\n}\n\nfunc (r *candidateRouter) updateQuality(key candidateEdgeKey,\n\tdelta float64) {\n\n\tvalue := r.quality[key] + delta\n\tif value > 6 {\n\t\tvalue = 6\n\t}\n\tif value < -6 {\n\t\tvalue = -6\n\t}\n\tr.quality[key] = value\n\n\tnow := r.view.Now()\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tif !candidateKnowledge.lastViewNow.IsZero() &&\n\t\tnow.Before(candidateKnowledge.lastViewNow) {\n\n\t\tcandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]candidateGlobalBelief)\n\t}\n\tcandidateKnowledge.lastViewNow = now\n\n\told := candidateKnowledge.beliefs[key]\n\tif !old.updated.IsZero() {\n\t\tage := now.Sub(old.updated)\n\t\tif age >= 0 {\n\t\t\told.quality *= math.Exp(\n\t\t\t\t-float64(age) /\n\t\t\t\t\tfloat64(candidateEvidenceLife),\n\t\t\t)\n\t\t} else {\n\t\t\told.quality = 0\n\t\t}\n\t}\n\n\told.quality += delta\n\tif old.quality > 6 {\n\t\told.quality = 6\n\t}\n\tif old.quality < -6 {\n\t\told.quality = -6\n\t}\n\told.updated = now\n\tcandidateKnowledge.beliefs[key] = old\n}\n\nfunc candidatePrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.001\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\t// The first term models the nearly empty mode. The second models the\n\t// nearly full mode and its capacity cliff.\n\tlowMode := 0.48 * math.Exp(-x/0.018)\n\thighMode := 0.50 / (1 + math.Exp((x-0.92)/0.035))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif lower := r.lowerOK[edge.key]; lower >= amt {\n\t\treturn 0.995\n\t}\n\n\tp := candidatePrior(amt, edge.capacity)\n\tlogOdds := math.Log(p/(1-p)) + r.quality[edge.key]\n\tp = 1 / (1 + math.Exp(-logOdds))\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\ntype candidatePathState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n}\n\ntype candidatePathQueue []*candidatePathState\n\nfunc (q candidatePathQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidatePathQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q candidatePathQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidatePathQueue) Push(value any) {\n\t*q = append(*q, value.(*candidatePathState))\n}\n\nfunc (q *candidatePathQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\tvalue := old[last]\n\t*q = old[:last]\n\n\treturn value\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tbestScore := map[route.Vertex]float64{\n\t\tr.spec.Target: 0,\n\t}\n\tbestAmount := map[route.Vertex]lnwire.MilliSatoshi{\n\t\tr.spec.Target: amt,\n\t}\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tpq := &candidatePathQueue{}\n\theap.Push(pq, &candidatePathState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t})\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*candidatePathState)\n\t\tknownScore, ok := bestScore[item.node]\n\t\tif !ok || item.score > knownScore+0.0001 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tamountOver := item.amount\n\n\t\t\tif !edge.usable(amountOver) ||\n\t\t\t\tr.policyBad[edge.key] {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif upper := r.upperFail[edge.key]; upper != 0 &&\n\t\t\t\tamountOver >= upper {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tavailable := r.localBalances[edge.chanID] -\n\t\t\t\t\tr.localReserved[edge.chanID]\n\t\t\t\tif available < amountOver {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := amountOver\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.from != r.source {\n\t\t\t\tfee = edge.fee(amountOver)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, amountOver)\n\t\t\tedgeScore := float64(fee) + candidateHopCost -\n\t\t\t\tcandidateRiskCost*math.Log(probability)\n\n\t\t\tedgeScore += float64(r.failures[edge.key]) *\n\t\t\t\tcandidateFailureCost\n\t\t\tedgeScore += float64(r.inUse[edge.key]) *\n\t\t\t\tcandidateReservedCost\n\n\t\t\tscore := item.score + edgeScore\n\t\t\toldScore, ok := bestScore[edge.from]\n\t\t\tif ok && score >= oldScore {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.from] = score\n\t\t\tbestAmount[edge.from] = sending\n\t\t\tnext[edge.from] = edge\n\t\t\theap.Push(pq, &candidatePathState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: score,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := bestAmount[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tforwardingEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateRouteEdges(rt *route.Route) []candidateEdgeKey {\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t}\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys\n}\n\nfunc candidateRouteAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc (r *candidateRouter) reserveRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\tfor _, key := range keys {\n\t\tr.inUse[key]++\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tr.localReserved[rt.Hops[0].ChannelID] += rt.TotalAmount\n\t}\n}\n\nfunc (r *candidateRouter) releaseRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\tfor _, key := range keys {\n\t\tif r.inUse[key] > 0 {\n\t\t\tr.inUse[key]--\n\t\t}\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tchanID := rt.Hops[0].ChannelID\n\t\treserved := r.localReserved[chanID]\n\t\tif reserved <= rt.TotalAmount {\n\t\t\tdelete(r.localReserved, chanID)\n\t\t} else {\n\t\t\tr.localReserved[chanID] = reserved - rt.TotalAmount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\n\tshard := r.currentShard\n\tif shard == 0 || shard > amt {\n\t\tshard = amt\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tr.reserveRoute(rt)\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif shard <= candidateMinShard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnextShard := shard * 3 / 5\n\t\tif nextShard < candidateMinShard {\n\t\t\tnextShard = candidateMinShard\n\t\t}\n\t\tif nextShard >= shard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tshard = nextShard\n\t\tr.currentShard = shard\n\t}\n}\n\nfunc (r *candidateRouter) failureIndex(rt *route.Route,\n\tfailureSource route.Vertex) int {\n\n\tif failureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == failureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordPassingPrefix(rt *route.Route,\n\tcount int) {\n\n\tkeys := candidateRouteEdges(rt)\n\tif count > len(keys) {\n\t\tcount = len(keys)\n\t}\n\n\tfor i := 0; i < count; i++ {\n\t\tamount := candidateRouteAmount(rt, i)\n\t\tif amount > r.lowerOK[keys[i]] {\n\t\t\tr.lowerOK[keys[i]] = amount\n\t\t}\n\n\t\tr.updateQuality(keys[i], 0.35)\n\t}\n}\n\nfunc (r *candidateRouter) recordSettledRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\n\tfor i, key := range keys {\n\t\tamount := candidateRouteAmount(rt, i)\n\n\t\tif lower := r.lowerOK[key]; lower != 0 {\n\t\t\tif lower <= amount {\n\t\t\t\tdelete(r.lowerOK, key)\n\t\t\t} else {\n\t\t\t\tr.lowerOK[key] = lower - amount\n\t\t\t}\n\t\t}\n\n\t\tif upper := r.upperFail[key]; upper != 0 {\n\t\t\tif upper <= amount {\n\t\t\t\tr.upperFail[key] = 1\n\t\t\t} else {\n\t\t\t\tr.upperFail[key] = upper - amount\n\t\t\t}\n\t\t}\n\n\t\tedge := r.edges[key]\n\t\tdelta := 0.8\n\t\tif edge != nil && edge.capacity > 0 {\n\t\t\tfraction := float64(amount) /\n\t\t\t\tfloat64(edge.capacity)\n\t\t\tdelta = 1.3 - 2.5*fraction\n\t\t\tif delta < -0.5 {\n\t\t\t\tdelta = -0.5\n\t\t\t}\n\t\t}\n\t\tr.updateQuality(key, delta)\n\n\t\treverse := candidateEdgeKey{\n\t\t\tchanID: key.chanID,\n\t\t\tfrom: rt.Hops[i].PubKeyBytes,\n\t\t}\n\t\tif _, ok := r.edges[reverse]; ok {\n\t\t\tr.updateQuality(reverse, 0.7)\n\t\t}\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tchanID := rt.Hops[0].ChannelID\n\t\tbalance := r.localBalances[chanID]\n\t\tif balance <= rt.TotalAmount {\n\t\t\tr.localBalances[chanID] = 0\n\t\t} else {\n\t\t\tr.localBalances[chanID] =\n\t\t\t\tbalance - rt.TotalAmount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tr.releaseRoute(rt)\n\n\tif result.Failure == nil {\n\t\tr.recordSettledRoute(rt)\n\t\treturn nil\n\t}\n\n\tfailIndex := r.failureIndex(rt, result.FailureSource)\n\tkeys := candidateRouteEdges(rt)\n\n\tif failIndex < 0 {\n\t\tfor _, key := range keys {\n\t\t\tr.failures[key]++\n\t\t\tr.updateQuality(key, -0.15)\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.recordPassingPrefix(rt, failIndex)\n\n\tif failIndex >= len(keys) {\n\t\treturn nil\n\t}\n\n\tkey := keys[failIndex]\n\tamount := candidateRouteAmount(rt, failIndex)\n\n\tswitch result.Failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\tupper, ok := r.upperFail[key]\n\t\tif !ok || amount < upper {\n\t\t\tr.upperFail[key] = amount\n\t\t}\n\t\tr.failures[key]++\n\t\tr.updateQuality(key, -2.4)\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\tr.policyBad[key] = true\n\t\tr.failures[key] += 2\n\t\tr.updateQuality(key, -1.0)\n\n\tdefault:\n\t\tr.failures[key]++\n\t\tr.updateQuality(key, -0.5)\n\t}\n\n\treturn nil\n}"
|
|
},
|
|
"stats": {
|
|
"evals_done": 400,
|
|
"distinct_candidates": 51
|
|
},
|
|
"candidates": [
|
|
{
|
|
"id": 0,
|
|
"parent": null,
|
|
"score": 0.3388,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"role": "seed",
|
|
"params": {
|
|
"source": "package main\n\n// This file is the CANDIDATE SLOT for evolved routing algorithms. During\n// optimization, the entire file is replaced (via go build -overlay) with a\n// generated implementation. The contract is a single constructor:\n//\n//\tnewCandidateRouter(view, source, localBalances, spec)\n//\n// returning a routing.SimRouter. The router sees only the public gossip\n// graph, its own channel balances and per-attempt feedback \u2014 the same\n// information a real Lightning sender has. The in-tree implementation below\n// is the seed algorithm: a deliberately simple fee-optimizing Dijkstra with\n// failure blacklisting and halving-based MPP splitting.\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\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\n// candidateEdge is one directed edge of the public graph: a channel from\n// one node to another, with the policy the sending node announced.\ntype candidateEdge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\n// fee returns the fee the sending node charges to forward amt over this\n// edge.\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\n// usable reports whether the edge can carry the given amount per its\n// announced policy.\nfunc (e *candidateEdge) usable(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\t// The public capacity is a hard upper bound on what can flow.\n\treturn amt <= e.capacity\n}\n\n// candidateRouter is the seed algorithm: cheapest-path routing with a\n// failure blacklist and amount halving when no route is found.\ntype candidateRouter struct {\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\t// incomingEdges maps a node to the directed edges arriving at it,\n\t// the natural shape for backward Dijkstra.\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\n\t// localBalances is the exact outbound liquidity of our own channels.\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\t// failedAmt records, per directed channel, the lowest amount that\n\t// failed with a liquidity error; routes are built to stay below it.\n\tfailedAmt map[uint64]lnwire.MilliSatoshi\n\n\t// shardAmt is the current shard size for MPP splitting.\n\tshardAmt lnwire.MilliSatoshi\n\n\t// partsUsed counts the successful shards so far.\n\tpartsUsed uint32\n\n\t// pending maps in-flight attempt ids to their routes.\n\tpending map[uint64]*route.Route\n}\n\n// newCandidateRouter builds the router for one payment. This signature is\n// the stable contract between the harness and generated candidates.\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\trouter := &candidateRouter{\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\tfailedAmt: make(map[uint64]lnwire.MilliSatoshi),\n\t\tshardAmt: spec.Amount,\n\t\tpending: make(map[uint64]*route.Route),\n\t}\n\n\t// Build the adjacency list from gossip. Iterating a node's channels\n\t// yields, per channel, the policy the OTHER node announced toward us\n\t// (InPolicy). That is exactly the policy governing the directed edge\n\t// other -> node, so we record the reversed edge at each visit.\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\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\tedge := &candidateEdge{\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\tbaseFeeMsat: pol.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: pol.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: 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\tedge.maxHTLC = pol.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.incomingEdges[edge.to] = append(\n\t\t\t\t\trouter.incomingEdges[edge.to], edge,\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 router, nil\n}\n\n// dijkstraItem is a priority queue entry.\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tcost lnwire.MilliSatoshi\n\tidx int\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int { return len(q) }\nfunc (q dijkstraQueue) Less(i, j int) bool { return q[i].cost < q[j].cost }\nfunc (q dijkstraQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i]; q[i].idx = i; q[j].idx = j }\nfunc (q *dijkstraQueue) Push(x any) {\n\titem := x.(*dijkstraItem)\n\titem.idx = len(*q)\n\t*q = append(*q, item)\n}\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tn := len(old)\n\titem := old[n-1]\n\t*q = old[:n-1]\n\treturn item\n}\n\n// findRoute computes the cheapest usable path delivering amt to the target,\n// walking backward from the target so fees accumulate correctly.\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi) (*route.Route,\n\terror) {\n\n\t// dist[node] = amount that must arrive at node to deliver amt.\n\tdist := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tdist[r.spec.Target] = amt\n\tpq := &dijkstraQueue{}\n\theap.Push(pq, &dijkstraItem{node: r.spec.Target, cost: amt})\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*dijkstraItem)\n\t\tnode, arriving := item.node, item.cost\n\n\t\tif arriving > dist[node] {\n\t\t\tcontinue\n\t\t}\n\t\tif node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\t// Consider all edges INTO node: for edge u->node, u must\n\t\t// send arriving plus u's fee.\n\t\tfor _, edge := range r.incomingEdges[node] {\n\t\t\tamtOver := arriving\n\n\t\t\tif !edge.usable(amtOver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Skip channels whose liquidity failure bound says\n\t\t\t// this amount cannot pass.\n\t\t\tif bound, ok := r.failedAmt[edge.chanID]; ok &&\n\t\t\t\tamtOver >= bound {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Our own channels: check exact local balance.\n\t\t\tif edge.from == r.source {\n\t\t\t\tif r.localBalances[edge.chanID] < amtOver {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar sending lnwire.MilliSatoshi\n\t\t\tif edge.from == r.source {\n\t\t\t\t// We pay no fee to ourselves.\n\t\t\t\tsending = amtOver\n\t\t\t} else {\n\t\t\t\tsending = amtOver + edge.fee(amtOver)\n\t\t\t}\n\n\t\t\tbest, ok := dist[edge.from]\n\t\t\tif !ok || sending < best {\n\t\t\t\tdist[edge.from] = sending\n\t\t\t\tnext[edge.from] = edge\n\t\t\t\theap.Push(pq, &dijkstraItem{\n\t\t\t\t\tnode: edge.from,\n\t\t\t\t\tcost: sending,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, ok := dist[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\n// buildRoute walks the next-pointers from source to target and constructs a\n// route with correctly accumulated fees and cltv deltas.\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tconst finalCltvDelta = 40\n\n\t// Collect the path edges source -> target.\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\t// Amounts and expiries per channel, computed backward.\n\tnumHops := len(path)\n\tamtOver := make([]lnwire.MilliSatoshi, numHops)\n\texpiryOver := make([]uint32, numHops)\n\n\tamtOver[numHops-1] = amt\n\texpiryOver[numHops-1] = finalCltvDelta\n\n\tfor i := numHops - 2; i >= 0; i-- {\n\t\tfwd := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] + fwd.fee(amtOver[i+1])\n\t\texpiryOver[i] = expiryOver[i+1] +\n\t\t\tuint32(fwd.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, numHops)\n\tfor i, edge := range path {\n\t\tamtToFwd := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\t\tif i < numHops-1 {\n\t\t\tamtToFwd = amtOver[i+1]\n\t\t\toutgoingExpiry = expiryOver[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amtToFwd,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiryOver[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\n// RequestRoute returns the next route to try: the cheapest path for the\n// current shard size, halving the shard when no route exists.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif r.shardAmt > amt {\n\t\tr.shardAmt = amt\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(r.shardAmt)\n\t\tif err == nil {\n\t\t\treturn rt, nil\n\t\t}\n\n\t\t// No route at this shard size: split if we're allowed more\n\t\t// parts and the shard is still meaningfully large.\n\t\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\t\tif partsLeft <= 1 || r.shardAmt < 10_000_000 {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.shardAmt /= 2\n\t}\n}\n\n// ReportAttempt learns from an attempt: liquidity failures set an upper\n// bound on the failing channel.\n//\n// NOTE: Part of the routing.SimRouter interface.\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif result.Failure == nil {\n\t\treturn nil\n\t}\n\n\t// Locate the failing hop and record the amount bound on its\n\t// outgoing channel.\n\tfailIdx := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIdx = 0\n\t}\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\tfailIdx = i + 1\n\t\t}\n\t}\n\n\t// The failing node could not forward over its outgoing channel,\n\t// which is rt.Hops[failIdx].\n\tif failIdx >= 0 && failIdx < len(rt.Hops) {\n\t\thop := rt.Hops[failIdx]\n\t\tamtOver := rt.TotalAmount\n\t\tif failIdx > 0 {\n\t\t\tamtOver = rt.Hops[failIdx-1].AmtToForward\n\t\t}\n\n\t\tbound, ok := r.failedAmt[hop.ChannelID]\n\t\tif !ok || amtOver < bound {\n\t\t\tr.failedAmt[hop.ChannelID] = amtOver\n\t\t}\n\t}\n\n\treturn nil\n}\n"
|
|
}
|
|
},
|
|
{
|
|
"id": 1,
|
|
"parent": 0,
|
|
"score": 0.0284,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = 40\n\tminShardAmt = lnwire.MilliSatoshi(5_000_000)\n\triskCostMsat = 5_000_000.0\n\thopCostMsat = 2_000.0\n)\n\ntype edgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tfailCount uint32\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tvalues map[edgeKey]liquidityBelief\n}{\n\tvalues: make(map[edgeKey]liquidityBelief),\n}\n\ntype candidateEdge struct {\n\tkey edgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[edgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tbeliefs map[edgeKey]liquidityBelief\n\tpolicyBad map[edgeKey]bool\n\tpenalties map[edgeKey]float64\n\tshardAmt lnwire.MilliSatoshi\n\tlastShard lnwire.MilliSatoshi\n\tshardFails uint32\n\tfailures uint32\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\tbalances := make(map[uint64]lnwire.MilliSatoshi, len(localBalances))\n\tfor chanID, balance := range localBalances {\n\t\tbalances[chanID] = balance\n\t}\n\n\trouter := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[edgeKey]*candidateEdge),\n\t\tlocalBalances: balances,\n\t\tbeliefs: make(map[edgeKey]liquidityBelief),\n\t\tpolicyBad: make(map[edgeKey]bool),\n\t\tpenalties: make(map[edgeKey]float64),\n\t\tshardAmt: spec.Amount,\n\t}\n\n\tnow := view.Now()\n\n\tsharedBeliefs.Lock()\n\tfor key, belief := range sharedBeliefs.values {\n\t\tif !belief.updated.IsZero() && !now.Before(belief.updated) {\n\t\t\trouter.beliefs[key] = belief\n\t\t}\n\t}\n\tsharedBeliefs.Unlock()\n\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := edgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.edges[key] = edge\n\t\t\t\trouter.incomingEdges[node] = append(\n\t\t\t\t\trouter.incomingEdges[node], edge,\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 router, nil\n}\n\nfunc clampProbability(probability float64) float64 {\n\tswitch {\n\tcase probability < 0.005:\n\t\treturn 0.005\n\tcase probability > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn probability\n\t}\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode models nearly depleted channels. The high mode models\n\t// channels whose balance remains close to their full capacity.\n\tlowMode := math.Exp(-ratio / 0.025)\n\thighMode := 1 / (1 + math.Exp((ratio-0.88)/0.055))\n\n\treturn clampProbability(0.5*lowMode + 0.5*highMode)\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0.005\n\t\t}\n\n\t\treturn 0.995\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\tbelief, ok := r.beliefs[edge.key]\n\tif !ok || belief.confidence <= 0 {\n\t\treturn prior\n\t}\n\n\tnow := r.view.Now()\n\tif now.Before(belief.updated) {\n\t\treturn prior\n\t}\n\n\tageMinutes := now.Sub(belief.updated).Minutes()\n\tfreshness := math.Exp(-ageMinutes / 12)\n\tweight := belief.confidence * freshness\n\tif weight < 0.02 {\n\t\treturn prior\n\t}\n\tif weight > 0.95 {\n\t\tweight = 0.95\n\t}\n\n\tlearned := prior\n\tswitch {\n\tcase belief.lowerOK > 0 && amt <= belief.lowerOK:\n\t\tlearned = 0.995\n\n\tcase belief.upperFail > 0 && amt >= belief.upperFail:\n\t\tlearned = 0.005\n\n\tcase belief.upperFail > 0:\n\t\testimate := belief.estimate\n\t\tif estimate <= 0 {\n\t\t\testimate = belief.upperFail / 4\n\t\t}\n\n\t\tscale := float64(belief.upperFail) * 0.12\n\t\tif scale < 1_000_000 {\n\t\t\tscale = 1_000_000\n\t\t}\n\n\t\tz := (float64(amt) - float64(estimate)) / scale\n\t\tlearned = 0.005 + 0.99/(1+math.Exp(z))\n\n\tcase belief.estimate > 0:\n\t\tscale := float64(belief.estimate) * 0.15\n\t\tif scale < 1_000_000 {\n\t\t\tscale = 1_000_000\n\t\t}\n\n\t\tz := (float64(amt) - float64(belief.estimate)) / scale\n\t\tlearned = 0.005 + 0.99/(1+math.Exp(z))\n\t}\n\n\tprobability := (1-weight)*prior + weight*learned\n\n\tif belief.upperFail > 0 && amt >= belief.upperFail {\n\t\tcount := belief.failCount\n\t\tif count > 3 {\n\t\t\tcount = 3\n\t\t}\n\t\tprobability *= math.Pow(0.12, float64(count))\n\t}\n\n\treturn clampProbability(probability)\n}\n\ntype pathItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\tindex int\n}\n\ntype pathQueue []*pathItem\n\nfunc (q pathQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q pathQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q pathQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n\tq[i].index = i\n\tq[j].index = j\n}\n\nfunc (q *pathQueue) Push(value any) {\n\titem := value.(*pathItem)\n\titem.index = len(*q)\n\t*q = append(*q, item)\n}\n\nfunc (q *pathQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\n\treturn item\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequiredAmt := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequiredAmt[r.spec.Target] = amt\n\n\tqueue := &pathQueue{}\n\theap.Push(queue, &pathItem{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t\tscore: 0,\n\t})\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*pathItem)\n\t\tbest, ok := bestScore[item.node]\n\t\tif !ok || item.score > best+0.0001 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] || !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] < item.amount {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\trisk := -math.Log(probability) * riskCostMsat\n\t\t\tscore := item.score + float64(fee) + risk +\n\t\t\t\thopCostMsat + r.penalties[edge.key]*riskCostMsat\n\n\t\t\tprevious, exists := bestScore[edge.key.from]\n\t\t\tif exists && score >= previous {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = score\n\t\t\trequiredAmt[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(queue, &pathItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: score,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := requiredAmt[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tforwardEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(forwardEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc reducedShard(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treduced := amt * 3 / 5\n\tif reduced >= amt {\n\t\treduced = amt - 1\n\t}\n\n\treturn reduced\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif r.spec.MaxParts > 0 && inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum in-flight parts reached\")\n\t}\n\tif r.failures >= 64 {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tif r.shardAmt <= 0 || r.shardAmt > amt {\n\t\tr.shardAmt = amt\n\t}\n\n\tfor r.shardAmt >= minShardAmt || r.shardAmt == amt {\n\t\trt, err := r.findRoute(r.shardAmt)\n\t\tif err == nil {\n\t\t\tr.lastShard = r.shardAmt\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif r.shardAmt <= minShardAmt {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.shardAmt = reducedShard(r.shardAmt)\n\t\tif r.shardAmt < minShardAmt {\n\t\t\tr.shardAmt = minShardAmt\n\t\t}\n\t\tif r.shardAmt > amt {\n\t\t\tr.shardAmt = amt\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc routeEdgeKey(rt *route.Route, index int) edgeKey {\n\tfrom := rt.SourcePubKey\n\tif index > 0 {\n\t\tfrom = rt.Hops[index-1].PubKeyBytes\n\t}\n\n\treturn edgeKey{\n\t\tchanID: rt.Hops[index].ChannelID,\n\t\tfrom: from,\n\t\tto: rt.Hops[index].PubKeyBytes,\n\t}\n}\n\nfunc routeEdgeAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc (r *candidateRouter) saveBelief(key edgeKey,\n\tbelief liquidityBelief) {\n\n\tr.beliefs[key] = belief\n\n\tsharedBeliefs.Lock()\n\tsharedBeliefs.values[key] = belief\n\tsharedBeliefs.Unlock()\n}\n\nfunc (r *candidateRouter) recordSuccess(key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif key.from == r.source {\n\t\treturn\n\t}\n\n\tbelief := r.beliefs[key]\n\tif amt > belief.lowerOK {\n\t\tbelief.lowerOK = amt\n\t}\n\tif belief.upperFail > 0 && amt >= belief.upperFail {\n\t\tbelief.upperFail = 0\n\t\tbelief.failCount = 0\n\t}\n\n\testimate := amt + amt/5\n\tif estimate > belief.estimate {\n\t\tbelief.estimate = estimate\n\t}\n\n\tbelief.confidence = math.Min(0.95, belief.confidence+0.32)\n\tbelief.updated = r.view.Now()\n\tbelief.failCount = 0\n\tr.saveBelief(key, belief)\n}\n\nfunc (r *candidateRouter) recordFailure(key edgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif key.from == r.source {\n\t\treturn\n\t}\n\n\tbelief := r.beliefs[key]\n\tif belief.upperFail == 0 || amt < belief.upperFail {\n\t\tbelief.upperFail = amt\n\t}\n\tif belief.lowerOK >= amt {\n\t\tbelief.lowerOK = amt / 3\n\t}\n\n\testimate := amt / 4\n\tif belief.estimate == 0 || estimate < belief.estimate {\n\t\tbelief.estimate = estimate\n\t}\n\n\tbelief.confidence = math.Min(0.95, belief.confidence+0.48)\n\tbelief.updated = r.view.Now()\n\tbelief.failCount++\n\tr.saveBelief(key, belief)\n}\n\nfunc (r *candidateRouter) successfulPrefix(rt *route.Route,\n\tcount int) {\n\n\tif count > len(rt.Hops) {\n\t\tcount = len(rt.Hops)\n\t}\n\n\tfor i := 0; i < count; i++ {\n\t\tr.recordSuccess(\n\t\t\trouteEdgeKey(rt, i), routeEdgeAmount(rt, i),\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) failureEdgeIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tr.successfulPrefix(rt, len(rt.Hops))\n\n\t\tif len(rt.Hops) > 0 {\n\t\t\tfirstChan := rt.Hops[0].ChannelID\n\t\t\tspent := rt.TotalAmount\n\t\t\tif balance := r.localBalances[firstChan]; balance >= spent {\n\t\t\t\tr.localBalances[firstChan] = balance - spent\n\t\t\t} else {\n\t\t\t\tr.localBalances[firstChan] = 0\n\t\t\t}\n\t\t}\n\n\t\tr.shardFails = 0\n\t\tif r.lastShard > 0 {\n\t\t\tr.shardAmt = r.lastShard\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.failures++\n\tfailIndex := r.failureEdgeIndex(rt, result.FailureSource)\n\n\tswitch result.Failure.Code() {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tif failIndex >= 0 && failIndex < len(rt.Hops) {\n\t\t\tr.successfulPrefix(rt, failIndex)\n\n\t\t\tkey := routeEdgeKey(rt, failIndex)\n\t\t\tamount := routeEdgeAmount(rt, failIndex)\n\t\t\tr.recordFailure(key, amount)\n\t\t\tr.penalties[key] += 1.5\n\t\t} else {\n\t\t\tfor i := range rt.Hops {\n\t\t\t\tr.penalties[routeEdgeKey(rt, i)] += 0.18\n\t\t\t}\n\t\t}\n\n\t\tr.shardFails++\n\t\tif r.shardFails >= 2 && r.shardAmt > minShardAmt {\n\t\t\tr.shardAmt = reducedShard(r.shardAmt)\n\t\t\tif r.shardAmt < minShardAmt {\n\t\t\t\tr.shardAmt = minShardAmt\n\t\t\t}\n\t\t\tr.shardFails = 0\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tif failIndex >= 0 && failIndex < len(rt.Hops) {\n\t\t\tr.successfulPrefix(rt, failIndex)\n\t\t\tr.policyBad[routeEdgeKey(rt, failIndex)] = true\n\t\t} else {\n\t\t\tfor i := range rt.Hops {\n\t\t\t\tr.penalties[routeEdgeKey(rt, i)] += 0.3\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tfor i := range rt.Hops {\n\t\t\tr.penalties[routeEdgeKey(rt, i)] += 0.12\n\t\t}\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 2,
|
|
"parent": 0,
|
|
"score": 0.3368,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\tminProbability = 0.005\n\tmaxProbability = 0.995\n\tlowerRetryFactor = 0.55\n\tglobalHalfLifeMin = 20.0\n\tmaxAttempts = 48\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdatedAt time.Time\n}\n\nvar sharedCandidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tlocalBeliefs map[candidateEdgeKey]liquidityBelief\n\tedgePenalty map[candidateEdgeKey]float64\n\tpolicyBad map[candidateEdgeKey]bool\n\n\tretryMax lnwire.MilliSatoshi\n\tattempts uint32\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\tlocalBeliefs: make(map[candidateEdgeKey]liquidityBelief),\n\t\tedgePenalty: make(map[candidateEdgeKey]float64),\n\t\tpolicyBad: make(map[candidateEdgeKey]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(\n\t\t\tctx, 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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < minProbability:\n\t\treturn minProbability\n\tcase p > maxProbability:\n\t\treturn maxProbability\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn minProbability\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\t// The low mode makes very small transfers nearly certain, while the\n\t// high mode retains roughly one-half probability through the middle\n\t// of a channel and falls sharply near full capacity.\n\tlowMode := 0.5 * math.Exp(-x/0.025)\n\thighMode := 0.5 / (1 + math.Exp((x-0.88)/0.07))\n\n\treturn clampProbability(lowMode + highMode)\n}\n\nfunc learnedProbability(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK > 0 && amt <= b.lowerOK {\n\t\treturn maxProbability\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\treturn minProbability\n\t}\n\n\testimate := b.estimate\n\tif estimate <= 0 {\n\t\testimate = capacity / 2\n\t}\n\n\twidth := math.Max(float64(capacity)*0.075, 1)\n\tx := (float64(amt) - float64(estimate)) / width\n\n\treturn clampProbability(1 / (1 + math.Exp(x)))\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] >= amt {\n\t\t\treturn maxProbability\n\t\t}\n\n\t\treturn minProbability\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\n\tif local, ok := r.localBeliefs[edge.key]; ok {\n\t\tlearned := learnedProbability(local, amt, edge.capacity)\n\t\tconfidence := math.Min(0.97, local.confidence)\n\n\t\treturn clampProbability(\n\t\t\tconfidence*learned + (1-confidence)*prior,\n\t\t)\n\t}\n\n\tsharedCandidateKnowledge.RLock()\n\tglobal, ok := sharedCandidateKnowledge.beliefs[edge.key]\n\tsharedCandidateKnowledge.RUnlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tageMinutes := r.view.Now().Sub(global.updatedAt).Minutes()\n\tif ageMinutes < 0 {\n\t\tageMinutes = 0\n\t}\n\n\t// Evidence from earlier payments is deliberately soft. This preserves\n\t// useful batch knowledge while allowing background traffic to invalidate\n\t// old liquidity observations.\n\tdecay := math.Exp(\n\t\t-math.Ln2 * ageMinutes / globalHalfLifeMin,\n\t)\n\tconfidence := math.Min(0.88, global.confidence*decay)\n\tif confidence < 0.03 {\n\t\treturn prior\n\t}\n\n\tlearned := learnedProbability(global, amt, edge.capacity)\n\n\treturn clampProbability(\n\t\tconfidence*learned + (1-confidence)*prior,\n\t)\n}\n\nfunc updateSuccessfulBelief(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\t// Under a bimodal balance model, passing an HTLC is evidence that this\n\t// direction is in the channel's high-liquidity mode.\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.88 * float64(capacity),\n\t)\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif highEstimate > b.estimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.confidence = math.Min(0.98, b.confidence+0.38)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc updateFailedBelief(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = 0\n\t}\n\n\t// A directional failure is evidence for the depleted mode, not merely\n\t// evidence that the balance lies uniformly below the attempted amount.\n\tlowEstimate := lnwire.MilliSatoshi(\n\t\tmath.Min(\n\t\t\tfloat64(amt)*0.25,\n\t\t\tfloat64(capacity)*0.08,\n\t\t),\n\t)\n\tif b.estimate == 0 || lowEstimate < b.estimate {\n\t\tb.estimate = lowEstimate\n\t}\n\n\tb.confidence = math.Min(0.98, b.confidence+0.48)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc (r *candidateRouter) recordSuccess(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := updateSuccessfulBelief(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.localBeliefs[key] = local\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := sharedCandidateKnowledge.beliefs[key]\n\tglobal = updateSuccessfulBelief(\n\t\tglobal, amt, edge.capacity, now,\n\t)\n\tsharedCandidateKnowledge.beliefs[key] = global\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.35\n}\n\nfunc (r *candidateRouter) recordFailure(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := updateFailedBelief(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.localBeliefs[key] = local\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := sharedCandidateKnowledge.beliefs[key]\n\tglobal = updateFailedBelief(\n\t\tglobal, amt, edge.capacity, now,\n\t)\n\tsharedCandidateKnowledge.beliefs[key] = global\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] += 1.25\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = amt\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t})\n\n\tfeeScale := math.Max(float64(amt), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tbest, ok := bestScore[item.node]\n\t\tif !ok || item.score > best+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] || !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] < item.amount {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 2 * float64(fee) / feeScale\n\t\t\thopCost := 0.012\n\t\t\tscore := item.score + riskCost + feeCost + hopCost +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists && (score > oldScore+1e-12 ||\n\t\t\t\t(math.Abs(score-oldScore) <= 1e-12 &&\n\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = score\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: score,\n\t\t\t\tamount: sending,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforwardAmount := amt\n\t\toutgoingExpiry := finalCltvDelta\n\n\t\tif i < last {\n\t\t\tforwardAmount = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forwardAmount,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc ceilDivAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts <= 1 {\n\t\treturn amt\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is already satisfied\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum number of parts in flight\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimumShard := ceilDivAmount(amt, partsLeft)\n\n\tshard := amt\n\tif r.retryMax > 0 && r.retryMax < shard {\n\t\tshard = r.retryMax\n\t}\n\tif shard < minimumShard {\n\t\tshard = minimumShard\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tr.attempts++\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif shard <= minimumShard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnextShard := lnwire.MilliSatoshi(\n\t\t\tfloat64(shard) * lowerRetryFactor,\n\t\t)\n\t\tif nextShard < minimumShard {\n\t\t\tnextShard = minimumShard\n\t\t}\n\t\tif nextShard >= shard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tshard = nextShard\n\t\tr.retryMax = shard\n\t}\n}\n\nfunc routeEdgeData(rt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc failureClass(failure any) (liquidity, policy bool) {\n\tswitch failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\treturn true, false\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\treturn false, true\n\n\tdefault:\n\t\treturn false, false\n\t}\n}\n\nfunc failingEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tkeys, amounts := routeEdgeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tfor i := range keys {\n\t\t\tr.recordSuccess(keys[i], amounts[i])\n\t\t}\n\n\t\tr.retryMax = 0\n\t\treturn nil\n\t}\n\n\tliquidityFailure, policyFailure := failureClass(result.Failure)\n\tfailIdx := failingEdgeIndex(rt, result.FailureSource)\n\n\tif failIdx >= 0 {\n\t\t// Every preceding channel successfully carried this attempt to the\n\t\t// node that reported the failure.\n\t\tfor i := 0; i < failIdx && i < len(keys); i++ {\n\t\t\tr.recordSuccess(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tswitch {\n\tcase liquidityFailure && failIdx >= 0 && failIdx < len(keys):\n\t\tr.recordFailure(keys[failIdx], amounts[failIdx])\n\n\tcase liquidityFailure && failIdx < 0 && len(keys) == 1:\n\t\t// A source-less failure on a direct route can only concern its\n\t\t// single channel.\n\t\tr.recordFailure(keys[0], amounts[0])\n\n\tcase liquidityFailure:\n\t\t// When the simulator cannot identify the failing node, diversify\n\t\t// away from every uncertain edge without inventing hard bounds.\n\t\tfor i, key := range keys {\n\t\t\tif i == 0 && key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.edgePenalty[key] += 0.32\n\t\t}\n\n\tcase policyFailure && failIdx >= 0 && failIdx < len(keys):\n\t\tr.policyBad[keys[failIdx]] = true\n\t\tr.edgePenalty[keys[failIdx]] += 8\n\n\tdefault:\n\t\tfor _, key := range keys {\n\t\t\tr.edgePenalty[key] += 0.18\n\t\t}\n\t}\n\n\tif liquidityFailure {\n\t\tfailedAmount := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\tnext := lnwire.MilliSatoshi(\n\t\t\tfloat64(failedAmount) * lowerRetryFactor,\n\t\t)\n\t\tif next <= 0 {\n\t\t\tnext = 1\n\t\t}\n\t\tif r.retryMax == 0 || next < r.retryMax {\n\t\t\tr.retryMax = next\n\t\t}\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 3,
|
|
"parent": 0,
|
|
"score": -0.0137,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst finalCltvDelta = 40\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\n\testimate lnwire.MilliSatoshi\n\thasEstimate bool\n\tevidence float64\n\tupdated time.Time\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tnetworks map[uint64]map[candidateEdgeKey]*liquidityBelief\n}{\n\tnetworks: make(map[uint64]map[candidateEdgeKey]*liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\tnetworkID uint64\n\n\tpolicyBad map[candidateEdgeKey]bool\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tshardTarget lnwire.MilliSatoshi\n\tpartsUsed uint32\n\n\tattemptsSinceSettle uint32\n\tunknownFailures uint32\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\tbalances := make(map[uint64]lnwire.MilliSatoshi, len(localBalances))\n\tfor chanID, balance := range localBalances {\n\t\tbalances[chanID] = balance\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: balances,\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: candidateEdgeKey{\n\t\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\t\tto: node,\n\t\t\t\t\t},\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[edge.key] = edge\n\t\t\t\tr.networkID += edgeFingerprint(edge)\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tmaxParts := spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\n\tinitialParts := maxParts\n\tif initialParts > 4 {\n\t\tinitialParts = 4\n\t}\n\tr.shardTarget = ceilDivide(spec.Amount, initialParts)\n\n\treturn r, nil\n}\n\nfunc edgeFingerprint(edge *candidateEdge) uint64 {\n\tx := edge.key.chanID + 0x9e3779b97f4a7c15\n\tfor i := 0; i < len(edge.key.from); i++ {\n\t\tx ^= uint64(edge.key.from[i]) + 0x9e3779b97f4a7c15 +\n\t\t\t(x << 6) + (x >> 2)\n\t\tx ^= uint64(edge.key.to[i]) + 0x517cc1b727220a95 +\n\t\t\t(x << 6) + (x >> 2)\n\t}\n\n\tx ^= uint64(edge.capacity) + 0x94d049bb133111eb\n\tx ^= uint64(edge.timeLockDelta) << 32\n\treturn x\n}\n\nfunc ceilDivide(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts <= 1 {\n\t\treturn amt\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc clampProbability(p float64) float64 {\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := math.Exp(-x / 0.025)\n\thighMode := 1 / (1 + math.Exp((x-0.92)/0.055))\n\n\treturn clampProbability(0.5*lowMode + 0.5*highMode)\n}\n\nfunc (r *candidateRouter) beliefProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\n\tsharedBeliefs.Lock()\n\tnetwork := sharedBeliefs.networks[r.networkID]\n\tvar snapshot liquidityBelief\n\tvar found bool\n\tif network != nil {\n\t\tbelief := network[edge.key]\n\t\tif belief != nil {\n\t\t\tsnapshot = *belief\n\t\t\tfound = true\n\t\t}\n\t}\n\tsharedBeliefs.Unlock()\n\n\tif !found || snapshot.evidence == 0 {\n\t\treturn prior\n\t}\n\n\tage := r.view.Now().Sub(snapshot.updated)\n\tif age < 0 {\n\t\tage = 0\n\t}\n\n\tageWeight := math.Exp(-float64(age) / float64(45*time.Minute))\n\tevidenceWeight := 1 - math.Exp(-0.85*snapshot.evidence)\n\tconfidence := ageWeight * evidenceWeight\n\tif confidence < 0.01 {\n\t\treturn prior\n\t}\n\n\ttarget := prior\n\tswitch {\n\tcase snapshot.lowerOK > 0 && amt <= snapshot.lowerOK:\n\t\ttarget = 0.995\n\n\tcase snapshot.upperFail > 0 && amt >= snapshot.upperFail:\n\t\ttarget = 0.005\n\n\tcase snapshot.hasEstimate:\n\t\twidth := math.Max(\n\t\t\tfloat64(edge.capacity)*0.10,\n\t\t\t5_000_000,\n\t\t)\n\t\testimateProbability := 1 / (1 + math.Exp(\n\t\t\t(float64(amt)-float64(snapshot.estimate))/width,\n\t\t))\n\t\ttarget = 0.35*prior + 0.65*estimateProbability\n\t}\n\n\treturn clampProbability(\n\t\tprior*(1-confidence) + target*confidence,\n\t)\n}\n\nfunc (r *candidateRouter) mutateBelief(key candidateEdgeKey,\n\tcapacity lnwire.MilliSatoshi,\n\tmutate func(*liquidityBelief)) {\n\n\tsharedBeliefs.Lock()\n\tdefer sharedBeliefs.Unlock()\n\n\tnetwork := sharedBeliefs.networks[r.networkID]\n\tif network == nil {\n\t\tnetwork = make(map[candidateEdgeKey]*liquidityBelief)\n\t\tsharedBeliefs.networks[r.networkID] = network\n\t}\n\n\tbelief := network[key]\n\tif belief == nil {\n\t\tbelief = &liquidityBelief{capacity: capacity}\n\t\tnetwork[key] = belief\n\t}\n\tif capacity > 0 {\n\t\tbelief.capacity = capacity\n\t}\n\n\tmutate(belief)\n\tbelief.updated = r.view.Now()\n\tif belief.evidence > 12 {\n\t\tbelief.evidence = 12\n\t}\n}\n\nfunc (r *candidateRouter) observePass(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) {\n\n\tr.mutateBelief(edge.key, edge.capacity, func(b *liquidityBelief) {\n\t\tif amt > b.lowerOK {\n\t\t\tb.lowerOK = amt\n\t\t}\n\t\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\t\tb.upperFail = 0\n\t\t}\n\n\t\tfundedEstimate := amt +\n\t\t\tlnwire.MilliSatoshi(\n\t\t\t\t0.72*float64(edge.capacity-amt),\n\t\t\t)\n\t\tif !b.hasEstimate || fundedEstimate > b.estimate {\n\t\t\tb.estimate = fundedEstimate\n\t\t\tb.hasEstimate = true\n\t\t}\n\t\tb.evidence += 1\n\t})\n}\n\nfunc (r *candidateRouter) observeFailure(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) {\n\n\tr.mutateBelief(edge.key, edge.capacity, func(b *liquidityBelief) {\n\t\tif b.upperFail == 0 || amt < b.upperFail {\n\t\t\tb.upperFail = amt\n\t\t}\n\t\tif b.lowerOK >= amt {\n\t\t\tb.lowerOK = 0\n\t\t}\n\n\t\tfailedEstimate := lnwire.MilliSatoshi(\n\t\t\t0.68 * float64(amt),\n\t\t)\n\t\tif failedEstimate > edge.capacity {\n\t\t\tfailedEstimate = edge.capacity\n\t\t}\n\t\tif !b.hasEstimate || failedEstimate < b.estimate {\n\t\t\tb.estimate = failedEstimate\n\t\t\tb.hasEstimate = true\n\t\t}\n\t\tb.evidence += 1.4\n\t})\n}\n\nfunc (r *candidateRouter) observeSettlement(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) {\n\n\tr.mutateBelief(edge.key, edge.capacity, func(b *liquidityBelief) {\n\t\tif !b.hasEstimate {\n\t\t\tb.estimate = edge.capacity * 9 / 10\n\t\t\tb.hasEstimate = true\n\t\t}\n\t\tif b.estimate > amt {\n\t\t\tb.estimate -= amt\n\t\t} else {\n\t\t\tb.estimate = 0\n\t\t}\n\n\t\tif b.lowerOK > amt {\n\t\t\tb.lowerOK -= amt\n\t\t} else {\n\t\t\tb.lowerOK = 0\n\t\t}\n\t\tif b.upperFail > amt {\n\t\t\tb.upperFail -= amt\n\t\t} else {\n\t\t\tb.upperFail = 0\n\t\t}\n\t\tb.evidence += 0.7\n\t})\n\n\treverseKey := candidateEdgeKey{\n\t\tchanID: edge.key.chanID,\n\t\tfrom: edge.key.to,\n\t\tto: edge.key.from,\n\t}\n\treverse := r.edges[reverseKey]\n\tif reverse == nil {\n\t\treturn\n\t}\n\n\tr.mutateBelief(\n\t\treverseKey, reverse.capacity, func(b *liquidityBelief) {\n\t\t\tif !b.hasEstimate {\n\t\t\t\tb.estimate = reverse.capacity / 10\n\t\t\t\tb.hasEstimate = true\n\t\t\t}\n\t\t\tb.estimate += amt\n\t\t\tif b.estimate > reverse.capacity {\n\t\t\t\tb.estimate = reverse.capacity\n\t\t\t}\n\n\t\t\tb.lowerOK += amt\n\t\t\tif b.lowerOK > reverse.capacity {\n\t\t\t\tb.lowerOK = reverse.capacity\n\t\t\t}\n\t\t\tif b.upperFail > 0 {\n\t\t\t\tb.upperFail += amt\n\t\t\t\tif b.upperFail > reverse.capacity {\n\t\t\t\t\tb.upperFail = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\tb.evidence += 0.7\n\t\t},\n\t)\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\trequired lnwire.MilliSatoshi\n\tcost float64\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].cost < q[j].cost\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\treturn item\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tbestCost := map[route.Vertex]float64{\n\t\tr.spec.Target: 0,\n\t}\n\trequired := map[route.Vertex]lnwire.MilliSatoshi{\n\t\tr.spec.Target: amt,\n\t}\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\trequired: amt,\n\t})\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tcurrentCost, ok := bestCost[item.node]\n\t\tif !ok || item.cost > currentCost+0.001 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.required != required[item.node] {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] || !edge.usable(item.required) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] < item.required {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.required\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.required)\n\t\t\t\tsending += fee\n\t\t\t}\n\t\t\tif sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := 0.999\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tprobability = r.beliefProbability(\n\t\t\t\t\tedge, item.required,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\triskScale := 50_000 +\n\t\t\t\t0.0035*float64(item.required)\n\t\t\triskCost := -math.Log(probability) * riskScale\n\t\t\tuseCost := float64(r.edgeUses[edge.key]) *\n\t\t\t\triskScale * 0.16\n\t\t\thopCost := 2_000.0\n\n\t\t\tnewCost := item.cost + float64(fee) +\n\t\t\t\triskCost + useCost + hopCost\n\n\t\t\toldCost, seen := bestCost[edge.key.from]\n\t\t\tif seen && newCost >= oldCost {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestCost[edge.key.from] = newCost\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\trequired: sending,\n\t\t\t\tcost: newCost,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tseen := make(map[route.Vertex]bool)\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif seen[node] {\n\t\t\treturn nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tseen[node] = true\n\n\t\tedge := next[node]\n\t\tif edge == nil {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tnextEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tnextEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(nextEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforwardAmount := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\t\tif i < last {\n\t\t\tforwardAmount = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forwardAmount,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif r.attemptsSinceSettle >= 120 {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tusedParts := r.partsUsed + inFlightHtlcs\n\tif usedParts >= r.spec.MaxParts && r.spec.MaxParts != 0 {\n\t\treturn nil, errors.New(\"maximum payment parts exhausted\")\n\t}\n\n\tpartsLeft := r.spec.MaxParts - usedParts\n\tif r.spec.MaxParts == 0 || partsLeft == 0 {\n\t\tpartsLeft = 1\n\t}\n\n\tminimumShard := ceilDivide(amt, partsLeft)\n\tshard := r.shardTarget\n\tif shard <= 0 || shard > amt {\n\t\tshard = amt\n\t}\n\tif shard < minimumShard {\n\t\tshard = minimumShard\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tfor _, edge := range r.routeEdges(rt) {\n\t\t\t\tr.edgeUses[edge.key]++\n\t\t\t}\n\t\t\tr.attemptsSinceSettle++\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif shard <= minimumShard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treduced := lnwire.MilliSatoshi(\n\t\t\t0.72 * float64(shard),\n\t\t)\n\t\tif reduced < minimumShard {\n\t\t\treduced = minimumShard\n\t\t}\n\t\tif reduced == shard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tshard = reduced\n\t\tr.shardTarget = reduced\n\t}\n}\n\nfunc (r *candidateRouter) routeEdges(\n\trt *route.Route) []*candidateEdge {\n\n\tedges := make([]*candidateEdge, 0, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tif edge := r.edges[key]; edge != nil {\n\t\t\tedges = append(edges, edge)\n\t\t}\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn edges\n}\n\nfunc routeEdgeAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc failureEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tedges := r.routeEdges(rt)\n\tif len(edges) != len(rt.Hops) {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tr.partsUsed++\n\t\tr.attemptsSinceSettle = 0\n\t\tr.unknownFailures = 0\n\n\t\tfor i, edge := range edges {\n\t\t\tamt := routeEdgeAmount(rt, i)\n\t\t\tr.observeSettlement(edge, amt)\n\n\t\t\tif edge.key.from == r.source {\n\t\t\t\tbalance := r.localBalances[edge.key.chanID]\n\t\t\t\tif balance > amt {\n\t\t\t\t\tr.localBalances[edge.key.chanID] =\n\t\t\t\t\t\tbalance - amt\n\t\t\t\t} else {\n\t\t\t\t\tr.localBalances[edge.key.chanID] = 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\tif r.shardTarget > delivered {\n\t\t\tr.shardTarget = delivered\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := failureEdgeIndex(rt, result.FailureSource)\n\tcode := result.Failure.Code()\n\n\tif failIndex < 0 || failIndex >= len(edges) {\n\t\tr.unknownFailures++\n\t\tif r.unknownFailures >= 3 {\n\t\t\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\t\treduced := lnwire.MilliSatoshi(\n\t\t\t\t0.78 * float64(delivered),\n\t\t\t)\n\t\t\tif reduced > 0 &&\n\t\t\t\t(r.shardTarget == 0 ||\n\t\t\t\t\treduced < r.shardTarget) {\n\n\t\t\t\tr.shardTarget = reduced\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < failIndex; i++ {\n\t\tr.observePass(edges[i], routeEdgeAmount(rt, i))\n\t}\n\n\tfailedEdge := edges[failIndex]\n\tfailedAmount := routeEdgeAmount(rt, failIndex)\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tr.observeFailure(failedEdge, failedAmount)\n\n\t\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\treduced := lnwire.MilliSatoshi(\n\t\t\t0.68 * float64(delivered),\n\t\t)\n\t\tif reduced > 0 &&\n\t\t\t(r.shardTarget == 0 || reduced < r.shardTarget) {\n\n\t\t\tr.shardTarget = reduced\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyBad[failedEdge.key] = true\n\n\tdefault:\n\t\tr.edgeUses[failedEdge.key] += 2\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 4,
|
|
"parent": 0,
|
|
"score": 0.2391,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = 40\n\n\tminProbability = 0.005\n\tmaxProbability = 0.985\n\n\tevidenceHalfLife = 30 * time.Minute\n\tminShardAmount = lnwire.MilliSatoshi(1_000_000)\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tlowerOKAt time.Time\n\tupperFail lnwire.MilliSatoshi\n\tupperFailAt time.Time\n}\n\nvar candidateBeliefs = struct {\n\tsync.Mutex\n\tbyEdge map[candidateEdgeKey]liquidityBelief\n}{\n\tbyEdge: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\treservedLocal map[uint64]lnwire.MilliSatoshi\n\treservations map[*route.Route]localReservation\n\n\tpathPenalty map[candidateEdgeKey]float64\n\tblocked map[candidateEdgeKey]bool\n\n\tshardAmt lnwire.MilliSatoshi\n\tlastRemaining lnwire.MilliSatoshi\n\tfailuresAtSize int\n}\n\ntype localReservation struct {\n\tchanID uint64\n\tamount 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\trouter := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\treservedLocal: make(map[uint64]lnwire.MilliSatoshi),\n\t\treservations: make(map[*route.Route]localReservation),\n\t\tpathPenalty: make(map[candidateEdgeKey]float64),\n\t\tblocked: make(map[candidateEdgeKey]bool),\n\t\tshardAmt: spec.Amount,\n\t\tlastRemaining: spec.Amount,\n\t\tfailuresAtSize: 0,\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\trouter.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tif _, ok := router.edges[key]; ok {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.edges[key] = edge\n\t\t\t\trouter.incomingEdges[node] = append(\n\t\t\t\t\trouter.incomingEdges[node], edge,\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 router, nil\n}\n\nfunc clampProbability(probability float64) float64 {\n\tif probability < minProbability {\n\t\treturn minProbability\n\t}\n\tif probability > maxProbability {\n\t\treturn maxProbability\n\t}\n\n\treturn probability\n}\n\nfunc evidenceConfidence(now, observed time.Time) float64 {\n\tif observed.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(observed)\n\tif age <= 0 {\n\t\treturn 1\n\t}\n\n\treturn math.Exp(\n\t\t-math.Ln2 * float64(age) / float64(evidenceHalfLife),\n\t)\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn minProbability\n\t}\n\n\tfraction := float64(amt) / float64(capacity)\n\n\tlowMode := 0.5 * math.Exp(-fraction/0.025)\n\thighMode := 0.5 / (1 + math.Exp((fraction-0.92)/0.045))\n\n\treturn clampProbability(lowMode + highMode)\n}\n\nfunc edgeBeliefProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\n\tcandidateBeliefs.Lock()\n\tbelief, ok := candidateBeliefs.byEdge[edge.key]\n\tcandidateBeliefs.Unlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tprobability := prior\n\n\tif belief.lowerOK > 0 && amt <= belief.lowerOK {\n\t\tconfidence := evidenceConfidence(now, belief.lowerOKAt)\n\t\tprobability += confidence * (0.995 - probability)\n\t}\n\n\tif belief.upperFail > 0 && amt >= belief.upperFail {\n\t\tconfidence := evidenceConfidence(now, belief.upperFailAt)\n\t\tprobability += confidence * (0.003 - probability)\n\t}\n\n\tif belief.lowerOK > 0 && belief.upperFail > belief.lowerOK &&\n\t\tamt > belief.lowerOK && amt < belief.upperFail {\n\n\t\tokConfidence := evidenceConfidence(now, belief.lowerOKAt)\n\t\tfailConfidence := evidenceConfidence(now, belief.upperFailAt)\n\t\tconfidence := math.Min(okConfidence, failConfidence)\n\n\t\tposition := float64(amt-belief.lowerOK) /\n\t\t\tfloat64(belief.upperFail-belief.lowerOK)\n\t\tbounded := 0.995*(1-position) + 0.003*position\n\t\tprobability += confidence * (bounded - probability)\n\t}\n\n\treturn clampProbability(probability)\n}\n\nfunc recordLiquiditySuccess(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateBeliefs.Lock()\n\tdefer candidateBeliefs.Unlock()\n\n\tbelief := candidateBeliefs.byEdge[key]\n\tif belief.lowerOK == 0 || amt > belief.lowerOK ||\n\t\tevidenceConfidence(now, belief.lowerOKAt) < 0.25 {\n\n\t\tbelief.lowerOK = amt\n\t\tbelief.lowerOKAt = now\n\t}\n\n\tif belief.upperFail > 0 && amt >= belief.upperFail {\n\t\tbelief.upperFail = 0\n\t\tbelief.upperFailAt = time.Time{}\n\t}\n\n\tcandidateBeliefs.byEdge[key] = belief\n}\n\nfunc recordLiquidityFailure(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateBeliefs.Lock()\n\tdefer candidateBeliefs.Unlock()\n\n\tbelief := candidateBeliefs.byEdge[key]\n\tif belief.upperFail == 0 || amt < belief.upperFail ||\n\t\tevidenceConfidence(now, belief.upperFailAt) < 0.25 {\n\n\t\tbelief.upperFail = amt\n\t\tbelief.upperFailAt = now\n\t}\n\n\tif belief.lowerOK > 0 && amt <= belief.lowerOK {\n\t\tbelief.lowerOK = 0\n\t\tbelief.lowerOKAt = time.Time{}\n\t}\n\n\tcandidateBeliefs.byEdge[key] = belief\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\n\treturn item\n}\n\nfunc (r *candidateRouter) localAvailable(chanID uint64) lnwire.MilliSatoshi {\n\tbalance := r.localBalances[chanID]\n\treserved := r.reservedLocal[chanID]\n\tif reserved >= balance {\n\t\treturn 0\n\t}\n\n\treturn balance - reserved\n}\n\nfunc (r *candidateRouter) edgeCost(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprobability := edgeBeliefProbability(edge, amt, now)\n\triskCost := -math.Log(probability)\n\n\tfee := edge.fee(amt)\n\tfeeCost := 0.0\n\tif amt > 0 {\n\t\tfeeCost = 50 * float64(fee) / float64(amt)\n\t}\n\n\tcapacityBonus := 0.0\n\tif edge.capacity > 0 {\n\t\tfraction := float64(amt) / float64(edge.capacity)\n\t\tcapacityBonus = 0.04 * fraction\n\t}\n\n\treturn riskCost + feeCost + capacityBonus +\n\t\t0.025 + r.pathPenalty[edge.key]\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tnow := r.view.Now()\n\tdist := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tdist[r.spec.Target] = 0\n\trequired[r.spec.Target] = amt\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t})\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tbestScore, ok := dist[item.node]\n\t\tif !ok || item.score > bestScore+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tarriving := required[item.node]\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.blocked[edge.key] || !edge.usable(arriving) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localAvailable(edge.key.chanID) < arriving {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := arriving\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tsending += edge.fee(arriving)\n\t\t\t}\n\t\t\tif !edge.usable(arriving) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tscore := item.score + r.edgeCost(edge, arriving, now)\n\t\t\tprevious, found := dist[edge.key.from]\n\t\t\tif found && score >= previous {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdist[edge.key.from] = score\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: score,\n\t\t\t\tamount: sending,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamtOver := make([]lnwire.MilliSatoshi, len(path))\n\texpiryOver := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamtOver[last] = amt\n\texpiryOver[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] +\n\t\t\tforwardingEdge.fee(amtOver[i+1])\n\t\texpiryOver[i] = expiryOver[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamtToForward := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamtToForward = amtOver[i+1]\n\t\t\toutgoingExpiry = expiryOver[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amtToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiryOver[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc ceilDivide(amount lnwire.MilliSatoshi,\n\tdivisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amount\n\t}\n\n\tvalue := int64(amount)\n\tparts := int64(divisor)\n\n\treturn lnwire.MilliSatoshi((value + parts - 1) / parts)\n}\n\nfunc (r *candidateRouter) reserveRoute(rt *route.Route) {\n\tif len(rt.Hops) == 0 {\n\t\treturn\n\t}\n\n\tchanID := rt.Hops[0].ChannelID\n\treservation := localReservation{\n\t\tchanID: chanID,\n\t\tamount: rt.TotalAmount,\n\t}\n\n\tr.reservations[rt] = reservation\n\tr.reservedLocal[chanID] += reservation.amount\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif r.spec.MaxParts == 0 || inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tif amt < r.lastRemaining {\n\t\tr.failuresAtSize = 0\n\t\tif r.shardAmt < amt {\n\t\t\tgrown := r.shardAmt + r.shardAmt/4\n\t\t\tif grown > r.shardAmt {\n\t\t\t\tr.shardAmt = grown\n\t\t\t}\n\t\t}\n\t}\n\tr.lastRemaining = amt\n\n\tif r.shardAmt <= 0 || r.shardAmt > amt {\n\t\tr.shardAmt = amt\n\t}\n\n\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\tminimumUseful := minShardAmount\n\tif inFlightHtlcs > 0 {\n\t\trequired := ceilDivide(amt, partsLeft)\n\t\tif required > minimumUseful {\n\t\t\tminimumUseful = required\n\t\t}\n\t}\n\n\tfor {\n\t\tif r.shardAmt < minimumUseful {\n\t\t\tr.shardAmt = minimumUseful\n\t\t}\n\t\tif r.shardAmt > amt {\n\t\t\tr.shardAmt = amt\n\t\t}\n\n\t\trt, err := r.findRoute(r.shardAmt)\n\t\tif err == nil {\n\t\t\tr.reserveRoute(rt)\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif r.shardAmt <= minimumUseful {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnextAmount := r.shardAmt * 2 / 3\n\t\tif nextAmount >= r.shardAmt {\n\t\t\tnextAmount = r.shardAmt - 1\n\t\t}\n\t\tif nextAmount < minimumUseful {\n\t\t\tnextAmount = minimumUseful\n\t\t}\n\n\t\tr.shardAmt = nextAmount\n\t\tr.failuresAtSize = 0\n\t}\n}\n\nfunc routeChannelAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc routeEdgeKey(rt *route.Route, index int) candidateEdgeKey {\n\tfrom := rt.SourcePubKey\n\tif index > 0 {\n\t\tfrom = rt.Hops[index-1].PubKeyBytes\n\t}\n\n\treturn candidateEdgeKey{\n\t\tchanID: rt.Hops[index].ChannelID,\n\t\tfrom: from,\n\t\tto: rt.Hops[index].PubKeyBytes,\n\t}\n}\n\nfunc failureOutgoingIndex(rt *route.Route,\n\tfailureSource route.Vertex) int {\n\n\tif failureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == failureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc isTemporaryLiquidityFailure(failure any) bool {\n\t_, ok := failure.(*lnwire.FailTemporaryChannelFailure)\n\treturn ok\n}\n\nfunc isPolicyFailure(failure any) bool {\n\tswitch failure.(type) {\n\tcase *lnwire.FailFeeInsufficient:\n\t\treturn true\n\tcase *lnwire.FailIncorrectCltvExpiry:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (r *candidateRouter) releaseReservation(rt *route.Route,\n\tsettled bool) {\n\n\treservation, ok := r.reservations[rt]\n\tif !ok {\n\t\treturn\n\t}\n\tdelete(r.reservations, rt)\n\n\treserved := r.reservedLocal[reservation.chanID]\n\tif reservation.amount >= reserved {\n\t\tdelete(r.reservedLocal, reservation.chanID)\n\t} else {\n\t\tr.reservedLocal[reservation.chanID] =\n\t\t\treserved - reservation.amount\n\t}\n\n\tif !settled {\n\t\treturn\n\t}\n\n\tbalance := r.localBalances[reservation.chanID]\n\tif reservation.amount >= balance {\n\t\tr.localBalances[reservation.chanID] = 0\n\t} else {\n\t\tr.localBalances[reservation.chanID] =\n\t\t\tbalance - reservation.amount\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tsettled := result.Failure == nil\n\tr.releaseReservation(rt, settled)\n\n\tnow := r.view.Now()\n\tif settled {\n\t\tfor i := range rt.Hops {\n\t\t\tkey := routeEdgeKey(rt, i)\n\t\t\tif key.from != r.source {\n\t\t\t\trecordLiquiditySuccess(\n\t\t\t\t\tkey, routeChannelAmount(rt, i), now,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tr.pathPenalty[key] *= 0.25\n\t\t}\n\n\t\tr.failuresAtSize = 0\n\t\treturn nil\n\t}\n\n\tfailIndex := failureOutgoingIndex(rt, result.FailureSource)\n\n\tfor i := 0; i < failIndex && i < len(rt.Hops); i++ {\n\t\tkey := routeEdgeKey(rt, i)\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\trecordLiquiditySuccess(\n\t\t\tkey, routeChannelAmount(rt, i), now,\n\t\t)\n\t}\n\n\tif failIndex >= 0 && failIndex < len(rt.Hops) {\n\t\tkey := routeEdgeKey(rt, failIndex)\n\n\t\tswitch {\n\t\tcase isTemporaryLiquidityFailure(result.Failure):\n\t\t\tif key.from != r.source {\n\t\t\t\trecordLiquidityFailure(\n\t\t\t\t\tkey, routeChannelAmount(rt, failIndex), now,\n\t\t\t\t)\n\t\t\t}\n\t\t\tr.pathPenalty[key] += 1.5\n\n\t\tcase isPolicyFailure(result.Failure):\n\t\t\tr.blocked[key] = true\n\t\t\tr.pathPenalty[key] += 20\n\n\t\tdefault:\n\t\t\tr.pathPenalty[key] += 4\n\t\t}\n\t} else {\n\t\tfor i := range rt.Hops {\n\t\t\tkey := routeEdgeKey(rt, i)\n\t\t\tr.pathPenalty[key] += 0.75\n\t\t}\n\t}\n\n\tr.failuresAtSize++\n\tif r.failuresAtSize >= 2 || failIndex < 0 ||\n\t\tisPolicyFailure(result.Failure) {\n\n\t\tnextAmount := r.shardAmt * 2 / 3\n\t\tif nextAmount < minShardAmount {\n\t\t\tnextAmount = minShardAmount\n\t\t}\n\t\tif nextAmount < r.shardAmt {\n\t\t\tr.shardAmt = nextAmount\n\t\t}\n\t\tr.failuresAtSize = 0\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 5,
|
|
"parent": 0,
|
|
"score": 0.5106,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\tmaxRouteHops = 20\n\tmaxAttempts = 72\n\triskCostMsat = 180_000.0\n\thopCostMsat = 750.0\n\tevidenceHalfLife = int64(20 * 60 * 1_000_000_000)\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tcapacity lnwire.MilliSatoshi\n\tconfidence float64\n\tupdated int64\n}\n\nvar sharedLiquidityKnowledge = struct {\n\tsync.Mutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\tlowMode := 0.49 * math.Exp(-x/0.035)\n\thighMode := 0.49 / (1 + math.Exp(15*(x-0.84)))\n\n\treturn clampProbability(0.005 + lowMode + highMode)\n}\n\nfunc agedBelief(key candidateEdgeKey, now int64) (liquidityBelief, bool) {\n\tsharedLiquidityKnowledge.Lock()\n\tdefer sharedLiquidityKnowledge.Unlock()\n\n\tbelief, ok := sharedLiquidityKnowledge.beliefs[key]\n\tif !ok {\n\t\treturn liquidityBelief{}, false\n\t}\n\n\tage := now - belief.updated\n\tif age < 0 {\n\t\tbelief.confidence = 0\n\t} else if age > 0 {\n\t\tbelief.confidence *= math.Exp(\n\t\t\t-math.Ln2 * float64(age) / float64(evidenceHalfLife),\n\t\t)\n\t}\n\n\treturn belief, belief.confidence >= 0.01\n}\n\nfunc channelProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now int64) float64 {\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\tbelief, ok := agedBelief(edge.key, now)\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconfidence := math.Min(0.985, belief.confidence)\n\tevidence := prior\n\n\tswitch {\n\tcase belief.lowerOK > 0 && amt <= belief.lowerOK:\n\t\tevidence = 0.995\n\n\tcase belief.upperFail > 0 && amt >= belief.upperFail:\n\t\tevidence = 0.005\n\n\tcase belief.upperFail > belief.lowerOK:\n\t\tspan := float64(belief.upperFail - belief.lowerOK)\n\t\tx := float64(amt-belief.lowerOK) / span\n\t\tx = math.Max(0, math.Min(1, x))\n\n\t\tboundEstimate := 0.005 + 0.99*math.Pow(1-x, 1.45)\n\t\tevidence = boundEstimate\n\n\t\tif belief.estimate > 0 && belief.capacity > 0 {\n\t\t\twidth := math.Max(\n\t\t\t\tfloat64(belief.capacity)*0.10,\n\t\t\t\tfloat64(belief.upperFail-belief.lowerOK)*0.20,\n\t\t\t)\n\t\t\tpointEstimate := 0.005 + 0.99/(1+math.Exp(\n\t\t\t\t(float64(amt-belief.estimate))/width,\n\t\t\t))\n\t\t\tevidence = 0.65*boundEstimate + 0.35*pointEstimate\n\t\t}\n\n\tcase belief.estimate > 0 && belief.capacity > 0:\n\t\twidth := math.Max(1, float64(belief.capacity)*0.12)\n\t\tevidence = 0.005 + 0.99/(1+math.Exp(\n\t\t\t(float64(amt-belief.estimate))/width,\n\t\t))\n\t}\n\n\treturn clampProbability(\n\t\t(1-confidence)*prior + confidence*evidence,\n\t)\n}\n\nfunc decayStoredBelief(belief liquidityBelief, now int64) liquidityBelief {\n\tage := now - belief.updated\n\tif age < 0 {\n\t\treturn liquidityBelief{}\n\t}\n\tif age > 0 {\n\t\tbelief.confidence *= math.Exp(\n\t\t\t-math.Ln2 * float64(age) / float64(evidenceHalfLife),\n\t\t)\n\t}\n\tif belief.confidence < 0.03 {\n\t\treturn liquidityBelief{}\n\t}\n\n\treturn belief\n}\n\nfunc recordPass(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now int64) {\n\n\tsharedLiquidityKnowledge.Lock()\n\tdefer sharedLiquidityKnowledge.Unlock()\n\n\tbelief := decayStoredBelief(\n\t\tsharedLiquidityKnowledge.beliefs[key], now,\n\t)\n\tbelief.capacity = capacity\n\n\tif belief.upperFail > 0 && amt >= belief.upperFail {\n\t\tbelief.upperFail = 0\n\t}\n\tif amt > belief.lowerOK {\n\t\tbelief.lowerOK = amt\n\t}\n\tif amt > belief.estimate {\n\t\tbelief.estimate = amt\n\t}\n\n\tbelief.confidence = math.Min(0.985, belief.confidence+0.48)\n\tbelief.updated = now\n\tsharedLiquidityKnowledge.beliefs[key] = belief\n}\n\nfunc recordFailure(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now int64) {\n\n\tsharedLiquidityKnowledge.Lock()\n\tdefer sharedLiquidityKnowledge.Unlock()\n\n\tbelief := decayStoredBelief(\n\t\tsharedLiquidityKnowledge.beliefs[key], now,\n\t)\n\tbelief.capacity = capacity\n\n\tif belief.lowerOK >= amt {\n\t\tbelief.lowerOK = 0\n\t}\n\tif belief.upperFail == 0 || amt < belief.upperFail {\n\t\tbelief.upperFail = amt\n\t}\n\n\tdepletedEstimate := amt / 4\n\tif belief.estimate == 0 || depletedEstimate < belief.estimate {\n\t\tbelief.estimate = depletedEstimate\n\t}\n\n\tbelief.confidence = math.Min(0.985, belief.confidence+0.62)\n\tbelief.updated = now\n\tsharedLiquidityKnowledge.beliefs[key] = belief\n}\n\nfunc recordSettlement(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now int64) {\n\n\tsharedLiquidityKnowledge.Lock()\n\tdefer sharedLiquidityKnowledge.Unlock()\n\n\tforward := decayStoredBelief(\n\t\tsharedLiquidityKnowledge.beliefs[key], now,\n\t)\n\tforward.capacity = capacity\n\n\tpreSettlementEstimate := forward.estimate\n\tif preSettlementEstimate < amt {\n\t\tpreSettlementEstimate = amt\n\t}\n\thighModeEstimate := lnwire.MilliSatoshi(\n\t\tfloat64(capacity) * 0.90,\n\t)\n\tif preSettlementEstimate < highModeEstimate {\n\t\tpreSettlementEstimate = highModeEstimate\n\t}\n\n\tif preSettlementEstimate > amt {\n\t\tforward.estimate = preSettlementEstimate - amt\n\t} else {\n\t\tforward.estimate = 0\n\t}\n\tif forward.lowerOK > amt {\n\t\tforward.lowerOK -= amt\n\t} else {\n\t\tforward.lowerOK = 0\n\t}\n\tif forward.upperFail > amt {\n\t\tforward.upperFail -= amt\n\t} else {\n\t\tforward.upperFail = 0\n\t}\n\n\tforward.confidence = math.Min(0.985, forward.confidence+0.52)\n\tforward.updated = now\n\tsharedLiquidityKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n\treverse := decayStoredBelief(\n\t\tsharedLiquidityKnowledge.beliefs[reverseKey], now,\n\t)\n\treverse.capacity = capacity\n\treverse.lowerOK += amt\n\tif reverse.lowerOK > capacity {\n\t\treverse.lowerOK = capacity\n\t}\n\treverse.estimate += amt\n\tif reverse.estimate > capacity {\n\t\treverse.estimate = capacity\n\t}\n\tif reverse.upperFail > 0 && reverse.upperFail <= reverse.lowerOK {\n\t\treverse.upperFail = 0\n\t}\n\treverse.confidence = math.Min(0.985, reverse.confidence+0.35)\n\treverse.updated = now\n\tsharedLiquidityKnowledge.beliefs[reverseKey] = reverse\n}\n\ntype transientFailure struct {\n\tamount lnwire.MilliSatoshi\n\tcount int\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tfailures map[candidateEdgeKey]transientFailure\n\tpolicyBanned map[candidateEdgeKey]bool\n\tshardLimit lnwire.MilliSatoshi\n\tattempts int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\trouter := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\tfailures: make(map[candidateEdgeKey]transientFailure),\n\t\tpolicyBanned: make(map[candidateEdgeKey]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(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.incomingEdges[node] = append(\n\t\t\t\t\trouter.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\trouter.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn router, nil\n}\n\ntype pathItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops int\n\tindex int\n}\n\ntype pathQueue []*pathItem\n\nfunc (q pathQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q pathQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q pathQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n\tq[i].index = i\n\tq[j].index = j\n}\n\nfunc (q *pathQueue) Push(value any) {\n\titem := value.(*pathItem)\n\titem.index = len(*q)\n\t*q = append(*q, item)\n}\n\nfunc (q *pathQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\n\treturn item\n}\n\nfunc (r *candidateRouter) edgePenalty(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tfailure, ok := r.failures[edge.key]\n\tif !ok || failure.count == 0 {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / math.Max(1, float64(failure.amount))\n\tswitch {\n\tcase ratio >= 0.95:\n\t\treturn 3.2 * float64(failure.count)\n\tcase ratio >= 0.65:\n\t\treturn 1.1 * float64(failure.count)\n\tdefault:\n\t\treturn 0.22 * float64(failure.count)\n\t}\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, 0, errors.New(\"invalid route amount\")\n\t}\n\n\tnow := r.view.Now().UnixNano()\n\tbestScore := make(map[route.Vertex]float64)\n\tbestAmount := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\tbestAmount[r.spec.Target] = amt\n\n\tpq := &pathQueue{}\n\theap.Push(pq, &pathItem{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t})\n\n\tvar sourceRisk float64\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*pathItem)\n\t\tknownScore, ok := bestScore[item.node]\n\t\tif !ok || item.score > knownScore {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tamtOver := item.amount\n\t\t\tif !edge.usable(amtOver) || r.policyBanned[edge.key] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source {\n\t\t\t\tif r.localBalances[edge.key.chanID] < amtOver {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := amtOver\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tprobability := 0.999\n\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(amtOver)\n\t\t\t\tsending += fee\n\t\t\t\tprobability = channelProbability(edge, amtOver, now)\n\t\t\t}\n\n\t\t\tedgeRisk := -math.Log(clampProbability(probability))\n\t\t\tedgeRisk += r.edgePenalty(edge, amtOver)\n\t\t\tscore := item.score + float64(fee) +\n\t\t\t\triskCostMsat*edgeRisk + hopCostMsat\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\tif exists && score >= oldScore {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = score\n\t\t\tbestAmount[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(pq, &pathItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: score,\n\t\t\t\trisk: item.risk + edgeRisk,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := bestAmount[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(amt, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, clampProbability(math.Exp(-sourceRisk)), nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, errors.New(\"route exceeds maximum hop count\")\n\t\t}\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"source is the payment target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\tlast := len(path) - 1\n\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tforwardingEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := finalCltvDelta\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc addAmountCandidate(amounts *[]lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool, amt, minimum,\n\tmaximum lnwire.MilliSatoshi) {\n\n\tif amt < minimum {\n\t\tamt = minimum\n\t}\n\tif amt > maximum {\n\t\tamt = maximum\n\t}\n\tif amt <= 0 || seen[amt] {\n\t\treturn\n\t}\n\n\tseen[amt] = true\n\t*amounts = append(*amounts, amt)\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\tif inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\tminimum := (amt + lnwire.MilliSatoshi(partsLeft) - 1) /\n\t\tlnwire.MilliSatoshi(partsLeft)\n\tmaximum := amt\n\n\tif r.shardLimit > 0 && r.shardLimit < maximum {\n\t\tmaximum = r.shardLimit\n\t}\n\tif maximum < minimum {\n\t\tmaximum = minimum\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 5)\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\taddAmountCandidate(&amounts, seen, maximum, minimum, maximum)\n\tif r.shardLimit == 0 && partsLeft > 1 {\n\t\taddAmountCandidate(\n\t\t\t&amounts, seen, amt*3/4, minimum, maximum,\n\t\t)\n\t\taddAmountCandidate(\n\t\t\t&amounts, seen, amt/2, minimum, maximum,\n\t\t)\n\t\taddAmountCandidate(\n\t\t\t&amounts, seen, minimum, minimum, maximum,\n\t\t)\n\t}\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range amounts {\n\t\trt, probability, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprogress := float64(shard) / float64(amt)\n\t\tfee := rt.TotalAmount - shard\n\t\tif fee < 0 {\n\t\t\tfee = 0\n\t\t}\n\t\tfeeFraction := float64(fee) /\n\t\t\tmath.Max(1, float64(shard))\n\n\t\tutility := math.Log(clampProbability(probability)) +\n\t\t\t1.8*math.Log(progress) - 18*feeFraction\n\t\tif utility > bestUtility {\n\t\t\tbestUtility = utility\n\t\t\tbestRoute = rt\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\ttrial := maximum\n\t\tfor trial > minimum {\n\t\t\ttrial = lnwire.MilliSatoshi(float64(trial) * 0.65)\n\t\t\tif trial < minimum {\n\t\t\t\ttrial = minimum\n\t\t\t}\n\n\t\t\trt, _, err := r.findRoute(trial)\n\t\t\tif err == nil {\n\t\t\t\tbestRoute = rt\n\t\t\t\tr.shardLimit = trial\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif trial == minimum {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\treturn bestRoute, nil\n}\n\nfunc routeEdgeKey(rt *route.Route, index int) candidateEdgeKey {\n\tfrom := rt.SourcePubKey\n\tif index > 0 {\n\t\tfrom = rt.Hops[index-1].PubKeyBytes\n\t}\n\n\treturn candidateEdgeKey{\n\t\tchanID: rt.Hops[index].ChannelID,\n\t\tfrom: from,\n\t\tto: rt.Hops[index].PubKeyBytes,\n\t}\n}\n\nfunc routeEdgeAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc (r *candidateRouter) edgeCapacity(\n\tkey candidateEdgeKey) lnwire.MilliSatoshi {\n\n\tif edge, ok := r.edges[key]; ok {\n\t\treturn edge.capacity\n\t}\n\n\treturn 0\n}\n\nfunc (r *candidateRouter) ReportAttempt(attemptID uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\t_ = attemptID\n\tnow := r.view.Now().UnixNano()\n\n\tif result.Failure == nil {\n\t\tfor i := range rt.Hops {\n\t\t\tkey := routeEdgeKey(rt, i)\n\t\t\tamount := routeEdgeAmount(rt, i)\n\t\t\tcapacity := r.edgeCapacity(key)\n\t\t\tif capacity > 0 {\n\t\t\t\trecordSettlement(key, amount, capacity, now)\n\t\t\t}\n\t\t\tdelete(r.failures, key)\n\t\t}\n\n\t\tif len(rt.Hops) > 0 {\n\t\t\tfirst := rt.Hops[0].ChannelID\n\t\t\tif balance := r.localBalances[first]; balance > rt.TotalAmount {\n\t\t\t\tr.localBalances[first] = balance - rt.TotalAmount\n\t\t\t} else {\n\t\t\t\tr.localBalances[first] = 0\n\t\t\t}\n\t\t}\n\n\t\tr.shardLimit = 0\n\t\treturn nil\n\t}\n\n\tfailIndex := -1\n\tif result.FailureSource == rt.SourcePubKey {\n\t\tfailIndex = 0\n\t} else {\n\t\tfor i, hop := range rt.Hops {\n\t\t\tif hop.PubKeyBytes == result.FailureSource {\n\t\t\t\tfailIndex = i + 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := 0; i < failIndex && i < len(rt.Hops); i++ {\n\t\tkey := routeEdgeKey(rt, i)\n\t\tamount := routeEdgeAmount(rt, i)\n\t\tcapacity := r.edgeCapacity(key)\n\t\tif capacity > 0 {\n\t\t\trecordPass(key, amount, capacity, now)\n\t\t}\n\t}\n\n\tif failIndex < 0 || failIndex >= len(rt.Hops) {\n\t\tfor i := range rt.Hops {\n\t\t\tkey := routeEdgeKey(rt, i)\n\t\t\tfailure := r.failures[key]\n\t\t\tfailure.amount = routeEdgeAmount(rt, i)\n\t\t\tfailure.count++\n\t\t\tr.failures[key] = failure\n\t\t}\n\t\treturn nil\n\t}\n\n\tkey := routeEdgeKey(rt, failIndex)\n\tamount := routeEdgeAmount(rt, failIndex)\n\tfailure := r.failures[key]\n\tfailure.amount = amount\n\tfailure.count++\n\tr.failures[key] = failure\n\n\tswitch result.Failure.Code() {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tcapacity := r.edgeCapacity(key)\n\t\tif capacity > 0 {\n\t\t\trecordFailure(key, amount, capacity, now)\n\t\t}\n\n\t\tif failure.count >= 2 {\n\t\t\tdelivered := rt.Hops[len(rt.Hops)-1].AmtToForward\n\t\t\tlowerRetry := lnwire.MilliSatoshi(\n\t\t\t\tfloat64(delivered) * 0.65,\n\t\t\t)\n\t\t\tif lowerRetry > 0 &&\n\t\t\t\t(r.shardLimit == 0 || lowerRetry < r.shardLimit) {\n\n\t\t\t\tr.shardLimit = lowerRetry\n\t\t\t}\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tr.policyBanned[key] = true\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 6,
|
|
"parent": 1,
|
|
"score": 0.3699,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\tminProbability = 0.005\n\tmaxProbability = 0.995\n\tglobalHalfLifeMin = 14.0\n\tlocalHalfLifeMin = 7.0\n\tlowerRetryFactor = 0.55\n\tmaxAttempts = 48\n\tknownFailurePenalty = 0.90\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdatedAt time.Time\n}\n\nvar sharedCandidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n\tlastNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tlocalBeliefs map[candidateEdgeKey]liquidityBelief\n\tedgePenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\tpolicyBad map[candidateEdgeKey]bool\n\n\tretryMax lnwire.MilliSatoshi\n\tattempts uint32\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalBeliefs: make(map[candidateEdgeKey]liquidityBelief),\n\t\tedgePenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t}\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tnow := view.Now()\n\tsharedCandidateKnowledge.Lock()\n\tif !sharedCandidateKnowledge.lastNow.IsZero() &&\n\t\tnow.Before(sharedCandidateKnowledge.lastNow) {\n\n\t\tsharedCandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]liquidityBelief)\n\t}\n\tsharedCandidateKnowledge.lastNow = now\n\tsharedCandidateKnowledge.Unlock()\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < minProbability:\n\t\treturn minProbability\n\tcase p > maxProbability:\n\t\treturn maxProbability\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc clampAmount(amt,\n\tcapacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn minProbability\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\tlowMode := 0.5 * math.Exp(-x/0.025)\n\thighMode := 0.5 / (1 + math.Exp((x-0.90)/0.065))\n\n\treturn clampProbability(lowMode + highMode)\n}\n\nfunc learnedProbability(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK > 0 && amt <= b.lowerOK {\n\t\treturn maxProbability\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\treturn minProbability\n\t}\n\n\testimate := b.estimate\n\tif estimate <= 0 {\n\t\testimate = capacity / 2\n\t}\n\n\twidth := math.Max(float64(capacity)*0.055, 1)\n\tx := (float64(amt) - float64(estimate)) / width\n\n\treturn clampProbability(1 / (1 + math.Exp(x)))\n}\n\nfunc decayedConfidence(b liquidityBelief, now time.Time,\n\thalfLife, ceiling float64) float64 {\n\n\tif b.confidence <= 0 {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\tconfidence := b.confidence\n\tif halfLife > 0 {\n\t\tconfidence *= math.Exp(-math.Ln2 * age / halfLife)\n\t}\n\tif confidence > ceiling {\n\t\tconfidence = ceiling\n\t}\n\n\treturn confidence\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] >= amt {\n\t\t\treturn maxProbability\n\t\t}\n\n\t\treturn minProbability\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\tnow := r.view.Now()\n\n\tif local, ok := r.localBeliefs[edge.key]; ok {\n\t\tconfidence := decayedConfidence(\n\t\t\tlocal, now, localHalfLifeMin, 0.98,\n\t\t)\n\t\tif confidence >= 0.025 {\n\t\t\tlearned := learnedProbability(\n\t\t\t\tlocal, amt, edge.capacity,\n\t\t\t)\n\n\t\t\treturn clampProbability(\n\t\t\t\tconfidence*learned +\n\t\t\t\t\t(1-confidence)*prior,\n\t\t\t)\n\t\t}\n\t}\n\n\tsharedCandidateKnowledge.RLock()\n\tglobal, ok := sharedCandidateKnowledge.beliefs[edge.key]\n\tsharedCandidateKnowledge.RUnlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconfidence := decayedConfidence(\n\t\tglobal, now, globalHalfLifeMin, 0.90,\n\t)\n\tif confidence < 0.025 {\n\t\treturn prior\n\t}\n\n\tlearned := learnedProbability(global, amt, edge.capacity)\n\n\treturn clampProbability(\n\t\tconfidence*learned + (1-confidence)*prior,\n\t)\n}\n\nfunc observePass(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.90 * float64(capacity),\n\t)\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.estimate = clampAmount(b.estimate, capacity)\n\tb.confidence = math.Min(0.985, b.confidence+0.40)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc observeFailure(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = 0\n\t}\n\n\tlowEstimate := lnwire.MilliSatoshi(math.Min(\n\t\tfloat64(amt)*0.20,\n\t\tfloat64(capacity)*0.055,\n\t))\n\tb.estimate = clampAmount(lowEstimate, capacity)\n\tb.confidence = math.Min(0.99, b.confidence+0.52)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc settleBelief(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tb = observePass(b, amt, capacity, now)\n\n\tb.estimate = clampAmount(b.estimate-amt, capacity)\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc mirrorBelief(b liquidityBelief,\n\tcapacity lnwire.MilliSatoshi) liquidityBelief {\n\n\tmirror := liquidityBelief{\n\t\testimate: clampAmount(capacity-b.estimate, capacity),\n\t\tconfidence: b.confidence * 0.92,\n\t\tupdatedAt: b.updatedAt,\n\t}\n\n\tif b.upperFail > 0 && b.upperFail <= capacity {\n\t\tmirror.lowerOK = capacity - b.upperFail + 1\n\t}\n\tif b.lowerOK > 0 && b.lowerOK <= capacity {\n\t\tmirror.upperFail = capacity - b.lowerOK + 1\n\t}\n\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\treturn mirror\n}\n\nfunc reverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc (r *candidateRouter) storeLocalPair(key candidateEdgeKey,\n\tb liquidityBelief, capacity lnwire.MilliSatoshi,\n\treverseMinimum lnwire.MilliSatoshi) {\n\n\tr.localBeliefs[key] = b\n\n\trev := reverseKey(key)\n\tif _, ok := r.edges[rev]; !ok {\n\t\treturn\n\t}\n\n\tmirror := mirrorBelief(b, capacity)\n\tif reverseMinimum > mirror.lowerOK {\n\t\tmirror.lowerOK = clampAmount(\n\t\t\treverseMinimum, capacity,\n\t\t)\n\t}\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\tr.localBeliefs[rev] = mirror\n}\n\nfunc storeSharedPair(key candidateEdgeKey, b liquidityBelief,\n\tcapacity, reverseMinimum lnwire.MilliSatoshi) {\n\n\tsharedCandidateKnowledge.beliefs[key] = b\n\n\trev := reverseKey(key)\n\tmirror := mirrorBelief(b, capacity)\n\tif reverseMinimum > mirror.lowerOK {\n\t\tmirror.lowerOK = clampAmount(\n\t\t\treverseMinimum, capacity,\n\t\t)\n\t}\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\tsharedCandidateKnowledge.beliefs[rev] = mirror\n\tif b.updatedAt.After(sharedCandidateKnowledge.lastNow) {\n\t\tsharedCandidateKnowledge.lastNow = b.updatedAt\n\t}\n}\n\nfunc (r *candidateRouter) recordPass(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observePass(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, 0)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observePass(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, 0)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.40\n}\n\nfunc (r *candidateRouter) recordFailure(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observeFailure(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\treverseMinimum := edge.capacity - clampAmount(\n\t\tamt, edge.capacity,\n\t)\n\tr.storeLocalPair(\n\t\tkey, local, edge.capacity, reverseMinimum,\n\t)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observeFailure(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(\n\t\tkey, global, edge.capacity, reverseMinimum,\n\t)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] += knownFailurePenalty\n}\n\nfunc (r *candidateRouter) recordSettlement(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := settleBelief(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, amt)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := settleBelief(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, amt)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.30\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = amt\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t})\n\n\tfeeScale := math.Max(float64(amt), 250_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tbest, ok := bestScore[item.node]\n\t\tif !ok || item.score > best+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] ||\n\t\t\t\t!edge.usable(item.amount) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] <\n\t\t\t\t\titem.amount {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(\n\t\t\t\tedge, item.amount,\n\t\t\t)\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 1.35 * float64(fee) / feeScale\n\t\t\thopCost := 0.055\n\t\t\tuseCost := 0.075 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 8,\n\t\t\t)\n\n\t\t\tscore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost +\n\t\t\t\tr.edgePenalty[edge.key]\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists && (score > oldScore+1e-12 ||\n\t\t\t\t(math.Abs(score-oldScore) <= 1e-12 &&\n\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = score\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: score,\n\t\t\t\tamount: sending,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforwardAmount := amt\n\t\toutgoingExpiry := finalCltvDelta\n\n\t\tif i < last {\n\t\t\tforwardAmount = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forwardAmount,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc ceilDivAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts <= 1 {\n\t\treturn amt\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc routeEdgeData(rt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tkeys, _ := routeEdgeData(rt)\n\tfor _, key := range keys {\n\t\tr.edgeUses[key]++\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is already satisfied\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum number of parts in flight\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimumShard := ceilDivAmount(amt, partsLeft)\n\tshard := minimumShard\n\n\tif r.retryMax > minimumShard && r.retryMax < amt {\n\t\tshard = r.retryMax\n\t}\n\n\tvar lastErr error\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tr.attempts++\n\t\t\tr.markRouteUsed(rt)\n\n\t\t\treturn rt, nil\n\t\t}\n\t\tlastErr = err\n\n\t\tif shard >= amt {\n\t\t\treturn nil, lastErr\n\t\t}\n\n\t\tnextShard := lnwire.MilliSatoshi(\n\t\t\tmath.Ceil(float64(shard) * 1.7),\n\t\t)\n\t\tif nextShard <= shard {\n\t\t\tnextShard = shard + 1\n\t\t}\n\t\tif nextShard > amt {\n\t\t\tnextShard = amt\n\t\t}\n\t\tshard = nextShard\n\t}\n}\n\nfunc failureClass(failure any) (liquidity, policy bool) {\n\tswitch failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\treturn true, false\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\treturn false, true\n\n\tdefault:\n\t\treturn false, false\n\t}\n}\n\nfunc failingEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerRetry(\n\tfailedAmount lnwire.MilliSatoshi) {\n\n\tnext := lnwire.MilliSatoshi(\n\t\tfloat64(failedAmount) * lowerRetryFactor,\n\t)\n\tif next <= 0 {\n\t\tnext = 1\n\t}\n\tif r.retryMax == 0 || next < r.retryMax {\n\t\tr.retryMax = next\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tkeys, amounts := routeEdgeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tfor i := range keys {\n\t\t\tr.recordSettlement(keys[i], amounts[i])\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif first.from == r.source {\n\t\t\tbalance := r.localBalances[first.chanID]\n\t\t\tif balance > amounts[0] {\n\t\t\t\tr.localBalances[first.chanID] =\n\t\t\t\t\tbalance - amounts[0]\n\t\t\t} else {\n\t\t\t\tr.localBalances[first.chanID] = 0\n\t\t\t}\n\t\t}\n\n\t\tr.retryMax = 0\n\t\treturn nil\n\t}\n\n\tliquidityFailure, policyFailure := failureClass(\n\t\tresult.Failure,\n\t)\n\tfailIdx := failingEdgeIndex(rt, result.FailureSource)\n\n\tif failIdx >= 0 {\n\t\tfor i := 0; i < failIdx && i < len(keys); i++ {\n\t\t\tr.recordPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tswitch {\n\tcase liquidityFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.recordFailure(keys[failIdx], amounts[failIdx])\n\n\tcase liquidityFailure && failIdx < 0 && len(keys) == 1:\n\t\tr.recordFailure(keys[0], amounts[0])\n\n\tcase liquidityFailure:\n\t\tfor i, key := range keys {\n\t\t\tpenalty := 0.72\n\t\t\tif i == 0 && key.from == r.source {\n\t\t\t\tpenalty = 0.25\n\t\t\t}\n\t\t\tr.edgePenalty[key] += penalty\n\t\t}\n\n\tcase policyFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.policyBad[keys[failIdx]] = true\n\t\tr.edgePenalty[keys[failIdx]] += 10\n\n\tdefault:\n\t\tfor _, key := range keys {\n\t\t\tr.edgePenalty[key] += 0.30\n\t\t}\n\t}\n\n\tif liquidityFailure {\n\t\tfailedAmount := rt.Hops[len(rt.Hops)-1].\n\t\t\tAmtToForward\n\t\tr.lowerRetry(failedAmount)\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 7,
|
|
"parent": 0,
|
|
"score": 0.6151,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateMinShard = lnwire.MilliSatoshi(5_000_000)\n\tcandidateRiskCost = 2_000_000.0\n\tcandidateHopCost = 25_000.0\n\tcandidateFailureCost = 8_000_000.0\n\tcandidateReservedCost = 2_000_000.0\n\tcandidateEvidenceLife = 10 * time.Minute\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n}\n\ntype candidateGlobalBelief struct {\n\tquality float64\n\tupdated time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.Mutex\n\n\tbeliefs map[candidateEdgeKey]candidateGlobalBelief\n\tlastViewNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateGlobalBelief),\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tnow time.Time\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\tlocalReserved map[uint64]lnwire.MilliSatoshi\n\n\t// lowerOK records amounts recently proven to traverse a channel.\n\tlowerOK map[candidateEdgeKey]lnwire.MilliSatoshi\n\n\t// upperFail records amounts that failed during the current payment.\n\t// Amounts at or above this bound are excluded, while smaller retries\n\t// remain available.\n\tupperFail map[candidateEdgeKey]lnwire.MilliSatoshi\n\n\tquality map[candidateEdgeKey]float64\n\tfailures map[candidateEdgeKey]uint32\n\tpolicyBad map[candidateEdgeKey]bool\n\tinUse map[candidateEdgeKey]uint32\n\tcurrentShard 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\tnow := view.Now()\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tnow: now,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalReserved: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlowerOK: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tupperFail: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tquality: make(map[candidateEdgeKey]float64),\n\t\tfailures: make(map[candidateEdgeKey]uint32),\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t\tinUse: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tr.loadBeliefs(now)\n\n\tparts := spec.MaxParts\n\tif parts == 0 {\n\t\tparts = 1\n\t}\n\tr.currentShard = divideCandidateAmount(spec.Amount, parts)\n\tif r.currentShard < candidateMinShard {\n\t\tr.currentShard = candidateMinShard\n\t}\n\tif r.currentShard > spec.Amount {\n\t\tr.currentShard = spec.Amount\n\t}\n\n\treturn r, nil\n}\n\nfunc divideCandidateAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc (r *candidateRouter) loadBeliefs(now time.Time) {\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tif !candidateKnowledge.lastViewNow.IsZero() &&\n\t\tnow.Before(candidateKnowledge.lastViewNow) {\n\n\t\tcandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]candidateGlobalBelief)\n\t}\n\tcandidateKnowledge.lastViewNow = now\n\n\tfor key := range r.edges {\n\t\tbelief, ok := candidateKnowledge.beliefs[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tage := now.Sub(belief.updated)\n\t\tif age < 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdecay := math.Exp(\n\t\t\t-float64(age) / float64(candidateEvidenceLife),\n\t\t)\n\t\tr.quality[key] = belief.quality * decay\n\t}\n}\n\nfunc (r *candidateRouter) updateQuality(key candidateEdgeKey,\n\tdelta float64) {\n\n\tvalue := r.quality[key] + delta\n\tif value > 6 {\n\t\tvalue = 6\n\t}\n\tif value < -6 {\n\t\tvalue = -6\n\t}\n\tr.quality[key] = value\n\n\tnow := r.view.Now()\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tif !candidateKnowledge.lastViewNow.IsZero() &&\n\t\tnow.Before(candidateKnowledge.lastViewNow) {\n\n\t\tcandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]candidateGlobalBelief)\n\t}\n\tcandidateKnowledge.lastViewNow = now\n\n\told := candidateKnowledge.beliefs[key]\n\tif !old.updated.IsZero() {\n\t\tage := now.Sub(old.updated)\n\t\tif age >= 0 {\n\t\t\told.quality *= math.Exp(\n\t\t\t\t-float64(age) /\n\t\t\t\t\tfloat64(candidateEvidenceLife),\n\t\t\t)\n\t\t} else {\n\t\t\told.quality = 0\n\t\t}\n\t}\n\n\told.quality += delta\n\tif old.quality > 6 {\n\t\told.quality = 6\n\t}\n\tif old.quality < -6 {\n\t\told.quality = -6\n\t}\n\told.updated = now\n\tcandidateKnowledge.beliefs[key] = old\n}\n\nfunc candidatePrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.001\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\t// The first term models the nearly empty mode. The second models the\n\t// nearly full mode and its capacity cliff.\n\tlowMode := 0.48 * math.Exp(-x/0.018)\n\thighMode := 0.50 / (1 + math.Exp((x-0.92)/0.035))\n\tp := 0.005 + lowMode + highMode\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif lower := r.lowerOK[edge.key]; lower >= amt {\n\t\treturn 0.995\n\t}\n\n\tp := candidatePrior(amt, edge.capacity)\n\tlogOdds := math.Log(p/(1-p)) + r.quality[edge.key]\n\tp = 1 / (1 + math.Exp(-logOdds))\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\ntype candidatePathState struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n}\n\ntype candidatePathQueue []*candidatePathState\n\nfunc (q candidatePathQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidatePathQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q candidatePathQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidatePathQueue) Push(value any) {\n\t*q = append(*q, value.(*candidatePathState))\n}\n\nfunc (q *candidatePathQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\tvalue := old[last]\n\t*q = old[:last]\n\n\treturn value\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tbestScore := map[route.Vertex]float64{\n\t\tr.spec.Target: 0,\n\t}\n\tbestAmount := map[route.Vertex]lnwire.MilliSatoshi{\n\t\tr.spec.Target: amt,\n\t}\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tpq := &candidatePathQueue{}\n\theap.Push(pq, &candidatePathState{\n\t\tnode: r.spec.Target,\n\t\tamount: amt,\n\t})\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*candidatePathState)\n\t\tknownScore, ok := bestScore[item.node]\n\t\tif !ok || item.score > knownScore+0.0001 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tamountOver := item.amount\n\n\t\t\tif !edge.usable(amountOver) ||\n\t\t\t\tr.policyBad[edge.key] {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif upper := r.upperFail[edge.key]; upper != 0 &&\n\t\t\t\tamountOver >= upper {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tavailable := r.localBalances[edge.chanID] -\n\t\t\t\t\tr.localReserved[edge.chanID]\n\t\t\t\tif available < amountOver {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := amountOver\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.from != r.source {\n\t\t\t\tfee = edge.fee(amountOver)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, amountOver)\n\t\t\tedgeScore := float64(fee) + candidateHopCost -\n\t\t\t\tcandidateRiskCost*math.Log(probability)\n\n\t\t\tedgeScore += float64(r.failures[edge.key]) *\n\t\t\t\tcandidateFailureCost\n\t\t\tedgeScore += float64(r.inUse[edge.key]) *\n\t\t\t\tcandidateReservedCost\n\n\t\t\tscore := item.score + edgeScore\n\t\t\toldScore, ok := bestScore[edge.from]\n\t\t\tif ok && score >= oldScore {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.from] = score\n\t\t\tbestAmount[edge.from] = sending\n\t\t\tnext[edge.from] = edge\n\t\t\theap.Push(pq, &candidatePathState{\n\t\t\t\tnode: edge.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: score,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := bestAmount[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tfor node := r.source; node != r.spec.Target; {\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tforwardingEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateRouteEdges(rt *route.Route) []candidateEdgeKey {\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t}\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys\n}\n\nfunc candidateRouteAmount(rt *route.Route,\n\tindex int) lnwire.MilliSatoshi {\n\n\tif index == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[index-1].AmtToForward\n}\n\nfunc (r *candidateRouter) reserveRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\tfor _, key := range keys {\n\t\tr.inUse[key]++\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tr.localReserved[rt.Hops[0].ChannelID] += rt.TotalAmount\n\t}\n}\n\nfunc (r *candidateRouter) releaseRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\tfor _, key := range keys {\n\t\tif r.inUse[key] > 0 {\n\t\t\tr.inUse[key]--\n\t\t}\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tchanID := rt.Hops[0].ChannelID\n\t\treserved := r.localReserved[chanID]\n\t\tif reserved <= rt.TotalAmount {\n\t\t\tdelete(r.localReserved, chanID)\n\t\t} else {\n\t\t\tr.localReserved[chanID] = reserved - rt.TotalAmount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\n\tshard := r.currentShard\n\tif shard == 0 || shard > amt {\n\t\tshard = amt\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tr.reserveRoute(rt)\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif shard <= candidateMinShard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnextShard := shard * 3 / 5\n\t\tif nextShard < candidateMinShard {\n\t\t\tnextShard = candidateMinShard\n\t\t}\n\t\tif nextShard >= shard {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tshard = nextShard\n\t\tr.currentShard = shard\n\t}\n}\n\nfunc (r *candidateRouter) failureIndex(rt *route.Route,\n\tfailureSource route.Vertex) int {\n\n\tif failureSource == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == failureSource {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordPassingPrefix(rt *route.Route,\n\tcount int) {\n\n\tkeys := candidateRouteEdges(rt)\n\tif count > len(keys) {\n\t\tcount = len(keys)\n\t}\n\n\tfor i := 0; i < count; i++ {\n\t\tamount := candidateRouteAmount(rt, i)\n\t\tif amount > r.lowerOK[keys[i]] {\n\t\t\tr.lowerOK[keys[i]] = amount\n\t\t}\n\n\t\tr.updateQuality(keys[i], 0.35)\n\t}\n}\n\nfunc (r *candidateRouter) recordSettledRoute(rt *route.Route) {\n\tkeys := candidateRouteEdges(rt)\n\n\tfor i, key := range keys {\n\t\tamount := candidateRouteAmount(rt, i)\n\n\t\tif lower := r.lowerOK[key]; lower != 0 {\n\t\t\tif lower <= amount {\n\t\t\t\tdelete(r.lowerOK, key)\n\t\t\t} else {\n\t\t\t\tr.lowerOK[key] = lower - amount\n\t\t\t}\n\t\t}\n\n\t\tif upper := r.upperFail[key]; upper != 0 {\n\t\t\tif upper <= amount {\n\t\t\t\tr.upperFail[key] = 1\n\t\t\t} else {\n\t\t\t\tr.upperFail[key] = upper - amount\n\t\t\t}\n\t\t}\n\n\t\tedge := r.edges[key]\n\t\tdelta := 0.8\n\t\tif edge != nil && edge.capacity > 0 {\n\t\t\tfraction := float64(amount) /\n\t\t\t\tfloat64(edge.capacity)\n\t\t\tdelta = 1.3 - 2.5*fraction\n\t\t\tif delta < -0.5 {\n\t\t\t\tdelta = -0.5\n\t\t\t}\n\t\t}\n\t\tr.updateQuality(key, delta)\n\n\t\treverse := candidateEdgeKey{\n\t\t\tchanID: key.chanID,\n\t\t\tfrom: rt.Hops[i].PubKeyBytes,\n\t\t}\n\t\tif _, ok := r.edges[reverse]; ok {\n\t\t\tr.updateQuality(reverse, 0.7)\n\t\t}\n\t}\n\n\tif len(rt.Hops) != 0 {\n\t\tchanID := rt.Hops[0].ChannelID\n\t\tbalance := r.localBalances[chanID]\n\t\tif balance <= rt.TotalAmount {\n\t\t\tr.localBalances[chanID] = 0\n\t\t} else {\n\t\t\tr.localBalances[chanID] =\n\t\t\t\tbalance - rt.TotalAmount\n\t\t}\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tr.releaseRoute(rt)\n\n\tif result.Failure == nil {\n\t\tr.recordSettledRoute(rt)\n\t\treturn nil\n\t}\n\n\tfailIndex := r.failureIndex(rt, result.FailureSource)\n\tkeys := candidateRouteEdges(rt)\n\n\tif failIndex < 0 {\n\t\tfor _, key := range keys {\n\t\t\tr.failures[key]++\n\t\t\tr.updateQuality(key, -0.15)\n\t\t}\n\t\treturn nil\n\t}\n\n\tr.recordPassingPrefix(rt, failIndex)\n\n\tif failIndex >= len(keys) {\n\t\treturn nil\n\t}\n\n\tkey := keys[failIndex]\n\tamount := candidateRouteAmount(rt, failIndex)\n\n\tswitch result.Failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\tupper, ok := r.upperFail[key]\n\t\tif !ok || amount < upper {\n\t\t\tr.upperFail[key] = amount\n\t\t}\n\t\tr.failures[key]++\n\t\tr.updateQuality(key, -2.4)\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\tr.policyBad[key] = true\n\t\tr.failures[key] += 2\n\t\tr.updateQuality(key, -1.0)\n\n\tdefault:\n\t\tr.failures[key]++\n\t\tr.updateQuality(key, -0.5)\n\t}\n\n\treturn nil\n}"
|
|
},
|
|
"role": "best"
|
|
},
|
|
{
|
|
"id": 8,
|
|
"parent": 2,
|
|
"score": 0.3522,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\tminProbability = 0.005\n\tmaxProbability = 0.995\n\tglobalHalfLifeMin = 12.0\n\tlowerRetryFactor = 0.52\n\tmaxAttempts = 48\n\tmaxLabelsPerNode = 8\n\tbaseFailurePenalty = 0.55\n\tunknownRoutePenalty = 0.34\n\tminRetryShardFraction = 0.40\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdatedAt time.Time\n}\n\nvar sharedCandidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n\tlastNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tlocalBeliefs map[candidateEdgeKey]liquidityBelief\n\tedgePenalty map[candidateEdgeKey]float64\n\tfailureAmount map[candidateEdgeKey]lnwire.MilliSatoshi\n\tedgeUses map[candidateEdgeKey]uint32\n\tpolicyBad map[candidateEdgeKey]bool\n\n\tretryMax lnwire.MilliSatoshi\n\tattempts uint32\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalBeliefs: make(map[candidateEdgeKey]liquidityBelief),\n\t\tedgePenalty: make(map[candidateEdgeKey]float64),\n\t\tfailureAmount: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tnow := view.Now()\n\n\tsharedCandidateKnowledge.Lock()\n\tif !sharedCandidateKnowledge.lastNow.IsZero() &&\n\t\tnow.Before(sharedCandidateKnowledge.lastNow) {\n\n\t\tsharedCandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]liquidityBelief)\n\t}\n\tsharedCandidateKnowledge.lastNow = now\n\tsharedCandidateKnowledge.Unlock()\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < minProbability:\n\t\treturn minProbability\n\n\tcase p > maxProbability:\n\t\treturn maxProbability\n\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc clampAmount(amt,\n\tcapacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn minProbability\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\tlowMode := 0.50 * math.Exp(-x/0.028)\n\thighMode := 0.50 / (1 + math.Exp((x-0.91)/0.060))\n\n\treturn clampProbability(lowMode + highMode)\n}\n\nfunc learnedProbability(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK > 0 && amt <= b.lowerOK {\n\t\treturn maxProbability\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\treturn minProbability\n\t}\n\n\testimate := b.estimate\n\tif estimate <= 0 {\n\t\testimate = capacity / 2\n\t}\n\n\twidth := math.Max(float64(capacity)*0.04, 1)\n\tif b.upperFail > 0 {\n\t\twidth = math.Max(\n\t\t\twidth, float64(b.upperFail)*0.22,\n\t\t)\n\t}\n\n\tx := (float64(amt) - float64(estimate)) / width\n\tif x > 40 {\n\t\treturn minProbability\n\t}\n\tif x < -40 {\n\t\treturn maxProbability\n\t}\n\n\treturn clampProbability(1 / (1 + math.Exp(x)))\n}\n\nfunc sharedConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.confidence <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\tconfidence := b.confidence * math.Exp(\n\t\t-math.Ln2*age/globalHalfLifeMin,\n\t)\n\tif confidence > 0.92 {\n\t\tconfidence = 0.92\n\t}\n\n\treturn confidence\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] >= amt {\n\t\t\treturn maxProbability\n\t\t}\n\n\t\treturn minProbability\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\n\tif local, ok := r.localBeliefs[edge.key]; ok &&\n\t\tlocal.confidence >= 0.02 {\n\n\t\tconfidence := math.Min(local.confidence, 0.99)\n\t\tlearned := learnedProbability(\n\t\t\tlocal, amt, edge.capacity,\n\t\t)\n\n\t\treturn clampProbability(\n\t\t\tconfidence*learned + (1-confidence)*prior,\n\t\t)\n\t}\n\n\tsharedCandidateKnowledge.RLock()\n\tglobal, ok := sharedCandidateKnowledge.beliefs[edge.key]\n\tsharedCandidateKnowledge.RUnlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconfidence := sharedConfidence(global, r.view.Now())\n\tif confidence < 0.02 {\n\t\treturn prior\n\t}\n\n\tlearned := learnedProbability(global, amt, edge.capacity)\n\n\treturn clampProbability(\n\t\tconfidence*learned + (1-confidence)*prior,\n\t)\n}\n\nfunc observePass(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.92 * float64(capacity),\n\t)\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.estimate = clampAmount(b.estimate, capacity)\n\tb.confidence = math.Min(0.99, b.confidence+0.48)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc observeFailure(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = 0\n\t}\n\n\tlowEstimate := lnwire.MilliSatoshi(math.Min(\n\t\tfloat64(amt)*0.18,\n\t\tfloat64(capacity)*0.05,\n\t))\n\tb.estimate = clampAmount(lowEstimate, capacity)\n\tb.confidence = math.Min(0.995, b.confidence+0.62)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc settleBelief(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tb = observePass(b, amt, capacity, now)\n\tb.estimate = clampAmount(b.estimate-amt, capacity)\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc mirrorBelief(b liquidityBelief,\n\tcapacity lnwire.MilliSatoshi) liquidityBelief {\n\n\tmirror := liquidityBelief{\n\t\testimate: clampAmount(\n\t\t\tcapacity-b.estimate, capacity,\n\t\t),\n\t\tconfidence: b.confidence * 0.90,\n\t\tupdatedAt: b.updatedAt,\n\t}\n\n\tif b.upperFail > 0 && b.upperFail <= capacity {\n\t\tmirror.lowerOK = capacity - b.upperFail + 1\n\t}\n\tif b.lowerOK > 0 && b.lowerOK <= capacity {\n\t\tmirror.upperFail = capacity - b.lowerOK + 1\n\t}\n\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\treturn mirror\n}\n\nfunc reverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc (r *candidateRouter) storeLocalPair(key candidateEdgeKey,\n\tb liquidityBelief, capacity,\n\treverseMinimum lnwire.MilliSatoshi) {\n\n\tr.localBeliefs[key] = b\n\n\treverse := reverseKey(key)\n\tif _, ok := r.edges[reverse]; !ok {\n\t\treturn\n\t}\n\n\tmirror := mirrorBelief(b, capacity)\n\tif reverseMinimum > mirror.lowerOK {\n\t\tmirror.lowerOK = clampAmount(\n\t\t\treverseMinimum, capacity,\n\t\t)\n\t}\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\tr.localBeliefs[reverse] = mirror\n}\n\nfunc storeSharedPair(key candidateEdgeKey, b liquidityBelief,\n\tcapacity, reverseMinimum lnwire.MilliSatoshi) {\n\n\tsharedCandidateKnowledge.beliefs[key] = b\n\n\treverse := reverseKey(key)\n\tmirror := mirrorBelief(b, capacity)\n\tif reverseMinimum > mirror.lowerOK {\n\t\tmirror.lowerOK = clampAmount(\n\t\t\treverseMinimum, capacity,\n\t\t)\n\t}\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\tsharedCandidateKnowledge.beliefs[reverse] = mirror\n\tif b.updatedAt.After(sharedCandidateKnowledge.lastNow) {\n\t\tsharedCandidateKnowledge.lastNow = b.updatedAt\n\t}\n}\n\nfunc (r *candidateRouter) recordPass(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observePass(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, 0)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observePass(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, 0)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.30\n\tif failedAt := r.failureAmount[key]; failedAt > 0 &&\n\t\tamt >= failedAt {\n\n\t\tdelete(r.failureAmount, key)\n\t}\n}\n\nfunc (r *candidateRouter) recordFailure(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observeFailure(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\treverseMinimum := edge.capacity - clampAmount(\n\t\tamt, edge.capacity,\n\t)\n\tr.storeLocalPair(\n\t\tkey, local, edge.capacity, reverseMinimum,\n\t)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observeFailure(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(\n\t\tkey, global, edge.capacity, reverseMinimum,\n\t)\n\tsharedCandidateKnowledge.Unlock()\n\n\tprevious := r.failureAmount[key]\n\tif previous == 0 || amt < previous {\n\t\tr.failureAmount[key] = amt\n\t}\n\tr.edgePenalty[key] = math.Min(\n\t\t2.5, r.edgePenalty[key]+baseFailurePenalty,\n\t)\n}\n\nfunc (r *candidateRouter) recordSettlement(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := settleBelief(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, amt)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := settleBelief(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, amt)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.20\n\tdelete(r.failureAmount, key)\n}\n\nfunc (r *candidateRouter) amountPenalty(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tfailedAt := r.failureAmount[key]\n\tif failedAt <= 0 || amt >= failedAt {\n\t\treturn penalty\n\t}\n\n\tratio := float64(amt) / float64(failedAt)\n\treturn penalty * (0.10 + 0.90*ratio*ratio)\n}\n\ntype searchLabel struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n\thops uint16\n\n\tedge *candidateEdge\n\tnext *searchLabel\n\tactive bool\n}\n\ntype searchQueue []*searchLabel\n\nfunc (q searchQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q searchQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q searchQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *searchQueue) Push(value any) {\n\t*q = append(*q, value.(*searchLabel))\n}\n\nfunc (q *searchQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc dominates(a, b *searchLabel) bool {\n\treturn a.active &&\n\t\ta.score <= b.score+1e-12 &&\n\t\ta.amount <= b.amount\n}\n\nfunc addSearchLabel(labels map[route.Vertex][]*searchLabel,\n\tlabel *searchLabel) bool {\n\n\tcurrent := labels[label.node]\n\tfor _, existing := range current {\n\t\tif dominates(existing, label) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tkept := current[:0]\n\tfor _, existing := range current {\n\t\tif dominates(label, existing) {\n\t\t\texisting.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, existing)\n\t}\n\n\tlabel.active = true\n\tkept = append(kept, label)\n\n\tif len(kept) > maxLabelsPerNode {\n\t\tworst := 0\n\t\tworstValue := math.Inf(-1)\n\n\t\tfor i, existing := range kept {\n\t\t\tvalue := existing.score +\n\t\t\t\t0.16*math.Log1p(float64(existing.amount))\n\t\t\tif value > worstValue {\n\t\t\t\tworst = i\n\t\t\t\tworstValue = value\n\t\t\t}\n\t\t}\n\n\t\tremoved := kept[worst]\n\t\tremoved.active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\n\t\tif removed == label {\n\t\t\tlabels[label.node] = kept\n\t\t\treturn false\n\t\t}\n\t}\n\n\tlabels[label.node] = kept\n\n\treturn true\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\n\ttarget := &searchLabel{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t\tactive: true,\n\t}\n\n\tlabels := map[route.Vertex][]*searchLabel{\n\t\tr.spec.Target: {target},\n\t}\n\tqueue := &searchQueue{target}\n\theap.Init(queue)\n\n\tfeeScale := math.Max(float64(amt), 300_000)\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*searchLabel)\n\t\tif !item.active {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\treturn r.buildRoute(item)\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] ||\n\t\t\t\t!edge.usable(item.amount) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] <\n\t\t\t\t\titem.amount {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tif sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(\n\t\t\t\tedge, item.amount,\n\t\t\t)\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 0.90 * float64(fee) / feeScale\n\t\t\thopCost := 0.085\n\t\t\tif item.hops >= 10 {\n\t\t\t\thopCost += 0.025 *\n\t\t\t\t\tfloat64(item.hops-9)\n\t\t\t}\n\n\t\t\tuseCost := 0.065 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.amountPenalty(\n\t\t\t\tedge.key, item.amount,\n\t\t\t)\n\n\t\t\tlabel := &searchLabel{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: item.score + riskCost +\n\t\t\t\t\tfeeCost + hopCost +\n\t\t\t\t\tuseCost + penalty,\n\t\t\t\tamount: sending,\n\t\t\t\thops: item.hops + 1,\n\t\t\t\tedge: edge,\n\t\t\t\tnext: item,\n\t\t\t}\n\n\t\t\tif addSearchLabel(labels, label) {\n\t\t\t\theap.Push(queue, label)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tsourceLabel *searchLabel) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tlabel := sourceLabel\n\tfor label.node != r.spec.Target {\n\t\tif visited[label.node] {\n\t\t\treturn nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tvisited[label.node] = true\n\n\t\tif label.edge == nil || label.next == nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"broken path at %v\", label.node,\n\t\t\t)\n\t\t}\n\n\t\tpath = append(path, label.edge)\n\t\tlabel = label.next\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = sourceLabel.nextAmountAtTarget()\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforwardAmount := amounts[last]\n\t\toutgoingExpiry := finalCltvDelta\n\n\t\tif i < last {\n\t\t\tforwardAmount = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forwardAmount,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc (l *searchLabel) nextAmountAtTarget() lnwire.MilliSatoshi {\n\tcurrent := l\n\tfor current.next != nil {\n\t\tcurrent = current.next\n\t}\n\n\treturn current.amount\n}\n\nfunc ceilDivAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts <= 1 {\n\t\treturn amt\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc appendShardCandidate(candidates *[]lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool,\n\tvalue, maximum lnwire.MilliSatoshi) {\n\n\tif value <= 0 {\n\t\tvalue = 1\n\t}\n\tif value > maximum {\n\t\tvalue = maximum\n\t}\n\tif seen[value] {\n\t\treturn\n\t}\n\n\tseen[value] = true\n\t*candidates = append(*candidates, value)\n}\n\nfunc routeEdgeData(rt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tkeys, _ := routeEdgeData(rt)\n\tfor _, key := range keys {\n\t\tr.edgeUses[key]++\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is already satisfied\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum number of parts in flight\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimumShard := ceilDivAmount(amt, partsLeft)\n\n\tdesired := minimumShard\n\tif partsLeft > 1 && r.retryMax > 0 &&\n\t\tr.retryMax < amt {\n\n\t\tfloor := lnwire.MilliSatoshi(math.Ceil(\n\t\t\tfloat64(minimumShard) *\n\t\t\t\tminRetryShardFraction,\n\t\t))\n\t\tif floor < 1 {\n\t\t\tfloor = 1\n\t\t}\n\n\t\tdesired = r.retryMax\n\t\tif desired < floor {\n\t\t\tdesired = floor\n\t\t}\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tvar candidates []lnwire.MilliSatoshi\n\n\tappendShardCandidate(\n\t\t&candidates, seen, desired, amt,\n\t)\n\n\tif partsLeft > 1 {\n\t\tappendShardCandidate(\n\t\t\t&candidates, seen, minimumShard, amt,\n\t\t)\n\t\tappendShardCandidate(\n\t\t\t&candidates, seen,\n\t\t\tlnwire.MilliSatoshi(math.Ceil(\n\t\t\t\tfloat64(minimumShard)*0.72,\n\t\t\t)),\n\t\t\tamt,\n\t\t)\n\t\tappendShardCandidate(\n\t\t\t&candidates, seen,\n\t\t\tlnwire.MilliSatoshi(math.Ceil(\n\t\t\t\tfloat64(minimumShard)*0.45,\n\t\t\t)),\n\t\t\tamt,\n\t\t)\n\t}\n\n\tappendShardCandidate(\n\t\t&candidates, seen,\n\t\tlnwire.MilliSatoshi(math.Ceil(\n\t\t\tfloat64(minimumShard)*1.45,\n\t\t)),\n\t\tamt,\n\t)\n\tappendShardCandidate(\n\t\t&candidates, seen,\n\t\tlnwire.MilliSatoshi(math.Ceil(\n\t\t\tfloat64(minimumShard)*2.15,\n\t\t)),\n\t\tamt,\n\t)\n\tappendShardCandidate(&candidates, seen, amt, amt)\n\n\tvar lastErr error\n\tfor _, shard := range candidates {\n\t\trt, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tr.attempts++\n\t\tr.markRouteUsed(rt)\n\n\t\treturn rt, nil\n\t}\n\n\tif lastErr == nil {\n\t\tlastErr = errors.New(\"no route found\")\n\t}\n\n\treturn nil, lastErr\n}\n\nfunc failureClass(failure any) (liquidity, policy bool) {\n\tswitch failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\treturn true, false\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\treturn false, true\n\n\tdefault:\n\t\treturn false, false\n\t}\n}\n\nfunc failingEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerRetry(\n\tfailedAmount lnwire.MilliSatoshi) {\n\n\tnext := lnwire.MilliSatoshi(math.Ceil(\n\t\tfloat64(failedAmount) * lowerRetryFactor,\n\t))\n\tif next <= 0 {\n\t\tnext = 1\n\t}\n\n\tif r.retryMax == 0 || next < r.retryMax {\n\t\tr.retryMax = next\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tkeys, amounts := routeEdgeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tfor i := range keys {\n\t\t\tr.recordSettlement(keys[i], amounts[i])\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif first.from == r.source {\n\t\t\tbalance := r.localBalances[first.chanID]\n\t\t\tif balance > amounts[0] {\n\t\t\t\tr.localBalances[first.chanID] =\n\t\t\t\t\tbalance - amounts[0]\n\t\t\t} else {\n\t\t\t\tr.localBalances[first.chanID] = 0\n\t\t\t}\n\t\t}\n\n\t\tr.retryMax = 0\n\t\treturn nil\n\t}\n\n\tliquidityFailure, policyFailure := failureClass(\n\t\tresult.Failure,\n\t)\n\tfailIdx := failingEdgeIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIdx >= 0 {\n\t\tfor i := 0; i < failIdx && i < len(keys); i++ {\n\t\t\tr.recordPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tswitch {\n\tcase liquidityFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.recordFailure(keys[failIdx], amounts[failIdx])\n\n\tcase liquidityFailure && failIdx < 0 && len(keys) == 1:\n\t\tr.recordFailure(keys[0], amounts[0])\n\n\tcase liquidityFailure:\n\t\tfor i, key := range keys {\n\t\t\tpenalty := unknownRoutePenalty\n\t\t\tif i == 0 && key.from == r.source {\n\t\t\t\tpenalty *= 0.35\n\t\t\t}\n\n\t\t\tr.edgePenalty[key] = math.Min(\n\t\t\t\t2.0, r.edgePenalty[key]+penalty,\n\t\t\t)\n\t\t}\n\n\tcase policyFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.policyBad[keys[failIdx]] = true\n\t\tr.edgePenalty[keys[failIdx]] += 12\n\n\tdefault:\n\t\tfor _, key := range keys {\n\t\t\tr.edgePenalty[key] = math.Min(\n\t\t\t\t2.0, r.edgePenalty[key]+0.22,\n\t\t\t)\n\t\t}\n\t}\n\n\tif liquidityFailure {\n\t\tfailedAmount := rt.Hops[len(rt.Hops)-1].\n\t\t\tAmtToForward\n\t\tr.lowerRetry(failedAmount)\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 9,
|
|
"parent": 2,
|
|
"score": 0.389,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\n\tminProbability = 0.005\n\tmaxProbability = 0.995\n\n\tlocalHalfLifeMin = 10.0\n\tglobalHalfLifeMin = 8.0\n\n\tlowerRetryFactor = 0.55\n\tminRetryFraction = 0.45\n\tmaxAttempts = 48\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\n\treturn e.maxHTLC == 0 || amt <= e.maxHTLC\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdatedAt time.Time\n}\n\nvar sharedCandidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n\tlastNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tlocalBeliefs map[candidateEdgeKey]liquidityBelief\n\tedgePenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\tfailedAmount map[candidateEdgeKey]lnwire.MilliSatoshi\n\tpolicyBad map[candidateEdgeKey]bool\n\n\tretryAmount lnwire.MilliSatoshi\n\tattempts uint32\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalBeliefs: make(map[candidateEdgeKey]liquidityBelief),\n\t\tedgePenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t\tfailedAmount: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tpolicyBad: make(map[candidateEdgeKey]bool),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tnow := view.Now()\n\n\tsharedCandidateKnowledge.Lock()\n\tif !sharedCandidateKnowledge.lastNow.IsZero() &&\n\t\t!now.After(sharedCandidateKnowledge.lastNow) {\n\n\t\tsharedCandidateKnowledge.beliefs =\n\t\t\tmake(map[candidateEdgeKey]liquidityBelief)\n\t}\n\tsharedCandidateKnowledge.lastNow = now\n\tsharedCandidateKnowledge.Unlock()\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tnodes := []route.Vertex{source}\n\n\tfor len(nodes) > 0 {\n\t\tnode := nodes[0]\n\t\tnodes = nodes[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, 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\tnodes = append(nodes, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[node] = append(\n\t\t\t\t\tr.incomingEdges[node], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < minProbability:\n\t\treturn minProbability\n\tcase p > maxProbability:\n\t\treturn maxProbability\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc clampAmount(amt,\n\tcapacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn minProbability\n\t}\n\tif amt <= 0 {\n\t\treturn maxProbability\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\tlowMode := 0.49 * math.Exp(-x/0.030)\n\thighMode := 0.50 / (1 + math.Exp((x-0.91)/0.060))\n\n\treturn clampProbability(lowMode + highMode)\n}\n\nfunc learnedProbability(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK > 0 && amt <= b.lowerOK {\n\t\treturn maxProbability\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\treturn minProbability\n\t}\n\n\testimate := b.estimate\n\tif estimate <= 0 {\n\t\testimate = capacity / 2\n\t}\n\n\twidth := math.Max(float64(capacity)*0.045, 1)\n\tx := (float64(amt) - float64(estimate)) / width\n\n\treturn clampProbability(1 / (1 + math.Exp(x)))\n}\n\nfunc decayedConfidence(b liquidityBelief, now time.Time,\n\thalfLife, ceiling float64) float64 {\n\n\tif b.confidence <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\tconfidence := b.confidence\n\tif halfLife > 0 {\n\t\tconfidence *= math.Exp(-math.Ln2 * age / halfLife)\n\t}\n\tif confidence > ceiling {\n\t\tconfidence = ceiling\n\t}\n\n\treturn confidence\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] >= amt {\n\t\t\treturn maxProbability\n\t\t}\n\n\t\treturn minProbability\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\tnow := r.view.Now()\n\n\tif belief, ok := r.localBeliefs[edge.key]; ok {\n\t\tconfidence := decayedConfidence(\n\t\t\tbelief, now, localHalfLifeMin, 0.985,\n\t\t)\n\t\tif confidence >= 0.02 {\n\t\t\tlearned := learnedProbability(\n\t\t\t\tbelief, amt, edge.capacity,\n\t\t\t)\n\n\t\t\treturn clampProbability(\n\t\t\t\tconfidence*learned +\n\t\t\t\t\t(1-confidence)*prior,\n\t\t\t)\n\t\t}\n\t}\n\n\tsharedCandidateKnowledge.RLock()\n\tbelief, ok := sharedCandidateKnowledge.beliefs[edge.key]\n\tsharedCandidateKnowledge.RUnlock()\n\tif !ok {\n\t\treturn prior\n\t}\n\n\tconfidence := decayedConfidence(\n\t\tbelief, now, globalHalfLifeMin, 0.88,\n\t)\n\tif confidence < 0.02 {\n\t\treturn prior\n\t}\n\n\tlearned := learnedProbability(belief, amt, edge.capacity)\n\n\treturn clampProbability(\n\t\tconfidence*learned + (1-confidence)*prior,\n\t)\n}\n\nfunc observePass(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.91 * float64(capacity),\n\t)\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.estimate = clampAmount(b.estimate, capacity)\n\tb.confidence = math.Min(0.985, b.confidence+0.42)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc observeFailure(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = 0\n\t}\n\n\tlowEstimate := lnwire.MilliSatoshi(math.Min(\n\t\tfloat64(amt)*0.18,\n\t\tfloat64(capacity)*0.045,\n\t))\n\tif b.estimate == 0 || lowEstimate < b.estimate {\n\t\tb.estimate = lowEstimate\n\t}\n\n\tb.estimate = clampAmount(b.estimate, capacity)\n\tb.confidence = math.Min(0.99, b.confidence+0.55)\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc observeSettlement(b liquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) liquidityBelief {\n\n\tb = observePass(b, amt, capacity, now)\n\tb.estimate = clampAmount(b.estimate-amt, capacity)\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.updatedAt = now\n\n\treturn b\n}\n\nfunc reverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc mirrorBelief(b liquidityBelief,\n\tcapacity lnwire.MilliSatoshi) liquidityBelief {\n\n\tmirror := liquidityBelief{\n\t\testimate: clampAmount(\n\t\t\tcapacity-b.estimate, capacity,\n\t\t),\n\t\tconfidence: b.confidence * 0.90,\n\t\tupdatedAt: b.updatedAt,\n\t}\n\n\tif b.upperFail > 0 && b.upperFail <= capacity {\n\t\tmirror.lowerOK = capacity - b.upperFail + 1\n\t}\n\tif b.lowerOK > 0 && b.lowerOK <= capacity {\n\t\tmirror.upperFail = capacity - b.lowerOK + 1\n\t}\n\n\tif mirror.upperFail > 0 &&\n\t\tmirror.lowerOK >= mirror.upperFail {\n\n\t\tmirror.upperFail = 0\n\t}\n\n\treturn mirror\n}\n\nfunc mergeReverseMinimum(b liquidityBelief,\n\tminimum, capacity lnwire.MilliSatoshi) liquidityBelief {\n\n\tif minimum > b.lowerOK {\n\t\tb.lowerOK = clampAmount(minimum, capacity)\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail > 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\treturn b\n}\n\nfunc (r *candidateRouter) storeLocalPair(key candidateEdgeKey,\n\tb liquidityBelief, capacity,\n\treverseMinimum lnwire.MilliSatoshi) {\n\n\tr.localBeliefs[key] = b\n\n\treverse := reverseKey(key)\n\tif _, ok := r.edges[reverse]; !ok {\n\t\treturn\n\t}\n\n\tmirror := mirrorBelief(b, capacity)\n\tmirror = mergeReverseMinimum(\n\t\tmirror, reverseMinimum, capacity,\n\t)\n\tr.localBeliefs[reverse] = mirror\n}\n\nfunc storeSharedPair(key candidateEdgeKey, b liquidityBelief,\n\tcapacity, reverseMinimum lnwire.MilliSatoshi) {\n\n\tsharedCandidateKnowledge.beliefs[key] = b\n\n\treverse := reverseKey(key)\n\tmirror := mirrorBelief(b, capacity)\n\tmirror = mergeReverseMinimum(\n\t\tmirror, reverseMinimum, capacity,\n\t)\n\tsharedCandidateKnowledge.beliefs[reverse] = mirror\n\n\tif b.updatedAt.After(sharedCandidateKnowledge.lastNow) {\n\t\tsharedCandidateKnowledge.lastNow = b.updatedAt\n\t}\n}\n\nfunc (r *candidateRouter) recordPass(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observePass(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, 0)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observePass(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, 0)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.25\n\tif failed := r.failedAmount[key]; failed > 0 && amt >= failed {\n\t\tdelete(r.failedAmount, key)\n\t}\n}\n\nfunc (r *candidateRouter) recordFailure(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observeFailure(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\treverseMinimum := edge.capacity - clampAmount(\n\t\tamt, edge.capacity,\n\t)\n\tr.storeLocalPair(\n\t\tkey, local, edge.capacity, reverseMinimum,\n\t)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observeFailure(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(\n\t\tkey, global, edge.capacity, reverseMinimum,\n\t)\n\tsharedCandidateKnowledge.Unlock()\n\n\tif old := r.failedAmount[key]; old == 0 || amt < old {\n\t\tr.failedAmount[key] = amt\n\t}\n\tr.edgePenalty[key] = math.Min(\n\t\tr.edgePenalty[key]+0.85, 3.0,\n\t)\n}\n\nfunc (r *candidateRouter) recordSettlement(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnow := r.view.Now()\n\tlocal := observeSettlement(\n\t\tr.localBeliefs[key], amt, edge.capacity, now,\n\t)\n\tr.storeLocalPair(key, local, edge.capacity, amt)\n\n\tsharedCandidateKnowledge.Lock()\n\tglobal := observeSettlement(\n\t\tsharedCandidateKnowledge.beliefs[key],\n\t\tamt, edge.capacity, now,\n\t)\n\tstoreSharedPair(key, global, edge.capacity, amt)\n\tsharedCandidateKnowledge.Unlock()\n\n\tr.edgePenalty[key] *= 0.20\n\tdelete(r.failedAmount, key)\n}\n\nfunc (r *candidateRouter) failurePenalty(key candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.edgePenalty[key]\n\tfailed := r.failedAmount[key]\n\tif failed <= 0 {\n\t\treturn penalty\n\t}\n\n\tratio := float64(amt) / float64(failed)\n\tswitch {\n\tcase ratio <= 0.35:\n\t\treturn penalty * 0.05\n\tcase ratio <= lowerRetryFactor:\n\t\treturn penalty * 0.15\n\tcase ratio < 0.85:\n\t\treturn penalty * 0.50\n\tdefault:\n\t\treturn penalty\n\t}\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = amt\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t})\n\n\tfeeScale := math.Max(float64(amt), 500_000)\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\n\t\tbest, ok := bestScore[item.node]\n\t\tif !ok || item.score > best+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif current := required[item.node]; current != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] ||\n\t\t\t\t!edge.usable(item.amount) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source &&\n\t\t\t\tr.localBalances[edge.key.chanID] <\n\t\t\t\t\titem.amount {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(\n\t\t\t\tedge, item.amount,\n\t\t\t)\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 0.85 * float64(fee) / feeScale\n\t\t\thopCost := 0.16\n\n\t\t\tuses := math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 6,\n\t\t\t)\n\t\t\tuseCost := 0.14 * uses\n\n\t\t\tscore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost +\n\t\t\t\tr.failurePenalty(edge.key, item.amount)\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(score > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(score-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = score\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: score,\n\t\t\t\tamount: sending,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforwardAmount := amt\n\t\toutgoingExpiry := finalCltvDelta\n\n\t\tif i < last {\n\t\t\tforwardAmount = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forwardAmount,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc ceilDivAmount(amt lnwire.MilliSatoshi,\n\tparts uint32) lnwire.MilliSatoshi {\n\n\tif parts <= 1 {\n\t\treturn amt\n\t}\n\n\tdivisor := lnwire.MilliSatoshi(parts)\n\n\treturn (amt + divisor - 1) / divisor\n}\n\nfunc routeEdgeData(rt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tkeys, _ := routeEdgeData(rt)\n\tfor _, key := range keys {\n\t\tr.edgeUses[key]++\n\t}\n}\n\nfunc (r *candidateRouter) chooseShard(amt lnwire.MilliSatoshi,\n\tpartsLeft uint32) lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn amt\n\t}\n\n\taverage := ceilDivAmount(amt, partsLeft)\n\tif r.retryAmount <= 0 {\n\t\treturn average\n\t}\n\n\tfloor := lnwire.MilliSatoshi(\n\t\tmath.Ceil(float64(average) * minRetryFraction),\n\t)\n\tif floor <= 0 {\n\t\tfloor = 1\n\t}\n\n\tshard := r.retryAmount\n\tif shard < floor {\n\t\tshard = floor\n\t}\n\tif shard > amt {\n\t\tshard = amt\n\t}\n\n\treturn shard\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is already satisfied\")\n\t}\n\tif r.attempts >= maxAttempts {\n\t\treturn nil, errors.New(\"attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum number of parts in flight\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tshard := r.chooseShard(amt, partsLeft)\n\n\tvar lastErr error\n\n\tfor {\n\t\trt, err := r.findRoute(shard)\n\t\tif err == nil {\n\t\t\tr.attempts++\n\t\t\tr.markRouteUsed(rt)\n\n\t\t\treturn rt, nil\n\t\t}\n\t\tlastErr = err\n\n\t\tif shard >= amt {\n\t\t\treturn nil, lastErr\n\t\t}\n\n\t\tnextShard := lnwire.MilliSatoshi(\n\t\t\tmath.Ceil(float64(shard) * 1.55),\n\t\t)\n\t\tif nextShard <= shard {\n\t\t\tnextShard = shard + 1\n\t\t}\n\t\tif nextShard > amt {\n\t\t\tnextShard = amt\n\t\t}\n\n\t\tshard = nextShard\n\t}\n}\n\nfunc failureClass(failure any) (liquidity, policy bool) {\n\tswitch failure.(type) {\n\tcase *lnwire.FailTemporaryChannelFailure:\n\t\treturn true, false\n\n\tcase *lnwire.FailFeeInsufficient,\n\t\t*lnwire.FailIncorrectCltvExpiry:\n\n\t\treturn false, true\n\n\tdefault:\n\t\treturn false, false\n\t}\n}\n\nfunc failingEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) lowerRetry(\n\tfailedAmount lnwire.MilliSatoshi) {\n\n\tnext := lnwire.MilliSatoshi(\n\t\tmath.Ceil(float64(failedAmount) * lowerRetryFactor),\n\t)\n\tif next <= 0 {\n\t\tnext = 1\n\t}\n\n\tif r.retryAmount == 0 || next < r.retryAmount {\n\t\tr.retryAmount = next\n\t}\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tpenalty := 0.42\n\t\tif i == 0 && key.from == r.source {\n\t\t\tpenalty = 0.12\n\t\t}\n\n\t\tr.edgePenalty[key] = math.Min(\n\t\t\tr.edgePenalty[key]+penalty, 2.5,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tkeys, amounts := routeEdgeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tif result.Failure == nil {\n\t\tfor i := range keys {\n\t\t\tr.recordSettlement(keys[i], amounts[i])\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif first.from == r.source {\n\t\t\tbalance := r.localBalances[first.chanID]\n\t\t\tif balance > amounts[0] {\n\t\t\t\tr.localBalances[first.chanID] =\n\t\t\t\t\tbalance - amounts[0]\n\t\t\t} else {\n\t\t\t\tr.localBalances[first.chanID] = 0\n\t\t\t}\n\t\t}\n\n\t\tr.retryAmount = 0\n\n\t\treturn nil\n\t}\n\n\tliquidityFailure, policyFailure := failureClass(\n\t\tresult.Failure,\n\t)\n\tfailIdx := failingEdgeIndex(rt, result.FailureSource)\n\n\tif failIdx > len(keys) {\n\t\tfailIdx = -1\n\t}\n\n\tif failIdx >= 0 {\n\t\tfor i := 0; i < failIdx && i < len(keys); i++ {\n\t\t\tr.recordPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tswitch {\n\tcase liquidityFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.recordFailure(keys[failIdx], amounts[failIdx])\n\n\tcase liquidityFailure && len(keys) == 1:\n\t\tr.recordFailure(keys[0], amounts[0])\n\n\tcase policyFailure &&\n\t\tfailIdx >= 0 && failIdx < len(keys):\n\n\t\tr.policyBad[keys[failIdx]] = true\n\t\tr.edgePenalty[keys[failIdx]] = 20\n\n\tdefault:\n\t\tr.penalizeUnknownRoute(keys)\n\t}\n\n\tif liquidityFailure {\n\t\tfailedAmount := rt.Hops[len(rt.Hops)-1].\n\t\t\tAmtToForward\n\t\tif failIdx >= 0 && failIdx < len(amounts) {\n\t\t\tfailedAmount = amounts[failIdx]\n\t\t}\n\n\t\tr.lowerRetry(failedAmount)\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 10,
|
|
"parent": 0,
|
|
"score": 0.1104,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = 40\n\tminShardAmount = lnwire.MilliSatoshi(1_000_000)\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn amt <= e.capacity\n}\n\ntype liquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdated time.Time\n}\n\nvar candidateSharedBeliefs = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]liquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]liquidityBelief),\n}\n\ntype localFailure struct {\n\tamount lnwire.MilliSatoshi\n\tat time.Time\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\treserved map[candidateEdgeKey]lnwire.MilliSatoshi\n\n\tfailed map[candidateEdgeKey]localFailure\n\tpolicyFailed map[candidateEdgeKey]bool\n\n\tshardAmt lnwire.MilliSatoshi\n\tfailuresAtSize int\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif spec == nil {\n\t\treturn nil, errors.New(\"nil payment specification\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\treserved: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tfailed: make(map[candidateEdgeKey]localFailure),\n\t\tpolicyFailed: make(map[candidateEdgeKey]bool),\n\t\tshardAmt: spec.Amount,\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: candidateEdgeKey{\n\t\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\t},\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[edge.to] = append(\n\t\t\t\t\tr.incomingEdges[edge.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[edge.key] = edge\n\n\t\t\t\treturn nil\n\t\t\t}, func() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn r, nil\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\t// The low mode makes very small transfers nearly certain, while the\n\t// high mode leaves ordinary transfers near a one-sided 50% prior.\n\tlowMode := 0.485 * math.Exp(-x/0.025)\n\thighMode := 0.5\n\n\t// Even a high-side channel becomes unlikely close to its capacity.\n\tcliff := 1 / (1 + math.Exp((x-0.92)/0.035))\n\tp := (highMode + lowMode) * cliff\n\n\tif p < 0.005 {\n\t\treturn 0.005\n\t}\n\tif p > 0.985 {\n\t\treturn 0.985\n\t}\n\n\treturn p\n}\n\nfunc beliefConfidence(b liquidityBelief, now time.Time) float64 {\n\tif b.confidence <= 0 {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updated)\n\tif age <= 0 {\n\t\treturn b.confidence\n\t}\n\n\t// Evidence decays quickly enough to tolerate background traffic while\n\t// remaining useful across nearby payments in the same scenario.\n\tdecay := math.Exp(-float64(age) / float64(25*time.Minute))\n\tconfidence := b.confidence * decay\n\tif confidence < 0 {\n\t\treturn 0\n\t}\n\tif confidence > 1 {\n\t\treturn 1\n\t}\n\n\treturn confidence\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.from == r.source {\n\t\treturn 0.999\n\t}\n\n\tprior := bimodalPrior(amt, edge.capacity)\n\tnow := r.view.Now()\n\n\tcandidateSharedBeliefs.RLock()\n\tbelief, ok := candidateSharedBeliefs.beliefs[edge.key]\n\tcandidateSharedBeliefs.RUnlock()\n\n\tif !ok || belief.capacity != edge.capacity {\n\t\treturn prior\n\t}\n\n\tconfidence := beliefConfidence(belief, now)\n\tif confidence < 0.01 {\n\t\treturn prior\n\t}\n\n\treserved := r.reserved[edge.key]\n\tqueryAmt := amt + reserved\n\n\tvar learned float64\n\tswitch {\n\tcase belief.lowerOK > 0 && queryAmt <= belief.lowerOK:\n\t\tlearned = 0.995\n\n\tcase belief.upperFail > 0 && queryAmt >= belief.upperFail:\n\t\tlearned = 0.003\n\n\tcase belief.estimate > 0:\n\t\tscale := math.Max(float64(edge.capacity)*0.06, 1)\n\t\tdistance := (float64(queryAmt) -\n\t\t\tfloat64(belief.estimate)) / scale\n\t\tlearned = 0.005 + 0.985/(1+math.Exp(distance))\n\n\tdefault:\n\t\tlearned = prior\n\t}\n\n\tp := prior*(1-confidence) + learned*confidence\n\tif p < 0.003 {\n\t\treturn 0.003\n\t}\n\tif p > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn p\n}\n\nfunc routeAmount(rt *route.Route, channelIndex int) lnwire.MilliSatoshi {\n\tif channelIndex == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[channelIndex-1].AmtToForward\n}\n\nfunc routeEdgeKeys(rt *route.Route) []candidateEdgeKey {\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t}\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys\n}\n\nfunc (r *candidateRouter) reserveRoute(rt *route.Route) {\n\tfor i, key := range routeEdgeKeys(rt) {\n\t\tr.reserved[key] += routeAmount(rt, i)\n\t}\n}\n\nfunc (r *candidateRouter) releaseRoute(rt *route.Route) {\n\tfor i, key := range routeEdgeKeys(rt) {\n\t\tamount := routeAmount(rt, i)\n\t\tif r.reserved[key] <= amount {\n\t\t\tdelete(r.reserved, key)\n\t\t\tcontinue\n\t\t}\n\n\t\tr.reserved[key] -= amount\n\t}\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\n\treturn item\n}\n\nfunc (r *candidateRouter) failureStillFresh(failure localFailure) bool {\n\treturn r.view.Now().Sub(failure.at) < 8*time.Minute\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tscores := map[route.Vertex]float64{\n\t\tr.spec.Target: 0,\n\t}\n\trequired := map[route.Vertex]lnwire.MilliSatoshi{\n\t\tr.spec.Target: amt,\n\t}\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t})\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tbestScore, ok := scores[item.node]\n\t\tif !ok || item.score > bestScore {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tarriving := required[item.node]\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(arriving) || r.policyFailed[edge.key] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif failure, ok := r.failed[edge.key]; ok {\n\t\t\t\tif r.failureStillFresh(failure) &&\n\t\t\t\t\tarriving*100 >= failure.amount*88 {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treserved := r.reserved[edge.key]\n\t\t\tif arriving+reserved > edge.capacity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tbalance := r.localBalances[edge.chanID]\n\t\t\t\tif balance < arriving+reserved {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := arriving\n\t\t\tfeeCost := lnwire.MilliSatoshi(0)\n\t\t\tif edge.from != r.source {\n\t\t\t\tfeeCost = edge.fee(arriving)\n\t\t\t\tsending += feeCost\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, arriving)\n\n\t\t\t// Reliability dominates the fee term. The amount-dependent\n\t\t\t// weight makes large-payment exploration favor wide channels.\n\t\t\triskWeight := 2_000_000.0 +\n\t\t\t\tfloat64(arriving)/100.0\n\t\t\tedgeScore := float64(feeCost) -\n\t\t\t\tmath.Log(probability)*riskWeight\n\n\t\t\t// Concurrent shards receive a mild diversity penalty.\n\t\t\tif reserved > 0 {\n\t\t\t\tedgeScore += riskWeight *\n\t\t\t\t\tfloat64(reserved) /\n\t\t\t\t\tmath.Max(float64(edge.capacity), 1)\n\t\t\t}\n\n\t\t\tnewScore := item.score + edgeScore\n\t\t\toldScore, seen := scores[edge.from]\n\t\t\tif seen && newScore >= oldScore {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tscores[edge.from] = newScore\n\t\t\trequired[edge.from] = sending\n\t\t\tnext[edge.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.from,\n\t\t\t\tscore: newScore,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 12)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc (r *candidateRouter) reduceShard(current lnwire.MilliSatoshi) bool {\n\tif current <= minShardAmount {\n\t\treturn false\n\t}\n\n\treduced := current * 62 / 100\n\tif reduced < minShardAmount {\n\t\treduced = minShardAmount\n\t}\n\tif reduced >= current {\n\t\treduced = current - 1\n\t}\n\tif reduced <= 0 {\n\t\treturn false\n\t}\n\n\tr.shardAmt = reduced\n\tr.failuresAtSize = 0\n\n\treturn true\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif r.spec.MaxParts != 0 && inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum in-flight parts reached\")\n\t}\n\n\tif r.shardAmt <= 0 || r.shardAmt > amt {\n\t\tr.shardAmt = amt\n\t\tr.failuresAtSize = 0\n\t}\n\n\tfor {\n\t\trt, err := r.findRoute(r.shardAmt)\n\t\tif err == nil {\n\t\t\tr.reserveRoute(rt)\n\t\t\treturn rt, nil\n\t\t}\n\n\t\tif !r.reduceShard(r.shardAmt) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n}\n\nfunc clampMSat(value, low,\n\thigh lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif value < low {\n\t\treturn low\n\t}\n\tif value > high {\n\t\treturn high\n\t}\n\n\treturn value\n}\n\nfunc updateFailureBelief(edge *candidateEdge,\n\tamount lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateSharedBeliefs.Lock()\n\tdefer candidateSharedBeliefs.Unlock()\n\n\tbelief := candidateSharedBeliefs.beliefs[edge.key]\n\tif belief.capacity != edge.capacity {\n\t\tbelief = liquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t\testimate: edge.capacity / 2,\n\t\t}\n\t}\n\n\tif belief.upperFail == 0 || amount < belief.upperFail {\n\t\tbelief.upperFail = amount\n\t}\n\tif belief.lowerOK >= amount {\n\t\tbelief.lowerOK = amount - 1\n\t}\n\tif belief.estimate == 0 || belief.estimate >= amount {\n\t\tbelief.estimate = amount / 4\n\t}\n\tbelief.confidence = 1\n\tbelief.updated = now\n\tcandidateSharedBeliefs.beliefs[edge.key] = belief\n\n\treverseKey := candidateEdgeKey{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.to,\n\t}\n\treverse := candidateSharedBeliefs.beliefs[reverseKey]\n\tif reverse.capacity != edge.capacity {\n\t\treverse = liquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t\testimate: edge.capacity / 2,\n\t\t}\n\t}\n\n\treverseLower := edge.capacity - amount\n\tif reverseLower < 0 {\n\t\treverseLower = 0\n\t}\n\tif reverseLower > reverse.lowerOK {\n\t\treverse.lowerOK = reverseLower\n\t}\n\tif reverse.estimate < reverseLower {\n\t\treverse.estimate = reverseLower\n\t}\n\treverse.confidence = math.Max(reverse.confidence, 0.9)\n\treverse.updated = now\n\tcandidateSharedBeliefs.beliefs[reverseKey] = reverse\n}\n\nfunc updateSuccessBeliefs(r *candidateRouter, rt *route.Route) {\n\tnow := r.view.Now()\n\tkeys := routeEdgeKeys(rt)\n\n\tcandidateSharedBeliefs.Lock()\n\tdefer candidateSharedBeliefs.Unlock()\n\n\tfor i, key := range keys {\n\t\tedge, ok := r.edges[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tamount := routeAmount(rt, i)\n\t\tremaining := edge.capacity - amount\n\t\tif remaining < 0 {\n\t\t\tremaining = 0\n\t\t}\n\n\t\tbelief := candidateSharedBeliefs.beliefs[key]\n\t\tif belief.capacity != edge.capacity {\n\t\t\tbelief = liquidityBelief{\n\t\t\t\tcapacity: edge.capacity,\n\t\t\t}\n\t\t}\n\n\t\t// Passing in a bimodal channel strongly identifies the high side.\n\t\tinferredLower := edge.capacity*7/10 - amount\n\t\tif inferredLower < 0 {\n\t\t\tinferredLower = 0\n\t\t}\n\t\tbelief.lowerOK = inferredLower\n\t\tbelief.estimate = remaining\n\t\tbelief.upperFail = 0\n\t\tbelief.confidence = 0.92\n\t\tbelief.updated = now\n\t\tcandidateSharedBeliefs.beliefs[key] = belief\n\n\t\treverseKey := candidateEdgeKey{\n\t\t\tchanID: edge.chanID,\n\t\t\tfrom: edge.to,\n\t\t}\n\t\treverse := candidateSharedBeliefs.beliefs[reverseKey]\n\t\tif reverse.capacity != edge.capacity {\n\t\t\treverse = liquidityBelief{\n\t\t\t\tcapacity: edge.capacity,\n\t\t\t}\n\t\t}\n\n\t\treverse.estimate = clampMSat(\n\t\t\treverse.estimate+amount, amount, edge.capacity,\n\t\t)\n\t\tif reverse.lowerOK < amount {\n\t\t\treverse.lowerOK = amount\n\t\t}\n\t\tif reverse.upperFail != 0 &&\n\t\t\treverse.upperFail <= reverse.lowerOK {\n\n\t\t\treverse.upperFail = 0\n\t\t}\n\t\treverse.confidence = math.Max(reverse.confidence, 0.8)\n\t\treverse.updated = now\n\t\tcandidateSharedBeliefs.beliefs[reverseKey] = reverse\n\t}\n}\n\nfunc failingChannelIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"nil attempted route\")\n\t}\n\n\tr.releaseRoute(rt)\n\n\tif result.Failure == nil {\n\t\tupdateSuccessBeliefs(r, rt)\n\t\tr.failuresAtSize = 0\n\t\treturn nil\n\t}\n\n\tfailIndex := failingChannelIndex(rt, result.FailureSource)\n\tif failIndex < 0 || failIndex >= len(rt.Hops) {\n\t\tr.failuresAtSize++\n\t\tif r.failuresAtSize >= 2 {\n\t\t\tr.reduceShard(r.shardAmt)\n\t\t}\n\t\treturn nil\n\t}\n\n\tkeys := routeEdgeKeys(rt)\n\tkey := keys[failIndex]\n\tedge, ok := r.edges[key]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tamount := routeAmount(rt, failIndex)\n\tcode := result.Failure.Code()\n\n\tswitch code {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tprevious, exists := r.failed[key]\n\t\tif !exists || amount < previous.amount ||\n\t\t\t!r.failureStillFresh(previous) {\n\n\t\t\tr.failed[key] = localFailure{\n\t\t\t\tamount: amount,\n\t\t\t\tat: r.view.Now(),\n\t\t\t}\n\t\t}\n\n\t\tupdateFailureBelief(edge, amount, r.view.Now())\n\n\t\tr.failuresAtSize++\n\t\tif r.failuresAtSize >= 2 {\n\t\t\tr.reduceShard(r.shardAmt)\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry,\n\t\tlnwire.CodeAmountBelowMinimum,\n\t\tlnwire.CodeExpiryTooSoon,\n\t\tlnwire.CodePermanentChannelFailure,\n\t\tlnwire.CodeRequiredChannelFeatureMissing,\n\t\tlnwire.CodeUnknownNextPeer:\n\n\t\tr.policyFailed[key] = true\n\n\tdefault:\n\t\t// Avoid repeatedly selecting the identical failing direction for\n\t\t// failures that are not actionable from public gossip.\n\t\tr.policyFailed[key] = true\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 11,
|
|
"parent": 0,
|
|
"score": 0.5178,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = 40\n\tmaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\treturn true\n}\n\ntype candidateBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperBad lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tcapacity lnwire.MilliSatoshi\n\tconf float64\n\tupdated time.Time\n}\n\nvar candidateMemory = struct {\n\tsync.Mutex\n\tbeliefs map[candidateEdgeKey]candidateBelief\n\tlastNow time.Time\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateBelief),\n}\n\nfunc clampProbability(p float64) float64 {\n\tswitch {\n\tcase p < 0.005:\n\t\treturn 0.005\n\tcase p > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn p\n\t}\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tx := float64(amt) / float64(capacity)\n\n\t// The low mode models a nearly depleted direction, while the high mode\n\t// models a direction holding almost the entire channel capacity.\n\tlowMode := math.Exp(-x / 0.025)\n\thighMode := 1 / (1 + math.Exp((x-0.92)/0.055))\n\n\treturn clampProbability(0.48*lowMode + 0.52*highMode)\n}\n\nfunc beliefProbability(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) float64 {\n\n\tprior := bimodalPrior(amt, capacity)\n\n\tcandidateMemory.Lock()\n\tbelief, ok := candidateMemory.beliefs[key]\n\tcandidateMemory.Unlock()\n\n\tif !ok || belief.conf <= 0 {\n\t\treturn prior\n\t}\n\n\tcapacity = belief.capacity\n\tif capacity <= 0 {\n\t\treturn prior\n\t}\n\n\tvar learned float64\n\tswitch {\n\tcase belief.lowerOK > 0 && amt <= belief.lowerOK:\n\t\tlearned = 0.995\n\n\tcase belief.upperBad > 0 && amt >= belief.upperBad:\n\t\tlearned = 0.005\n\n\tcase belief.lowerOK > 0 && belief.upperBad > belief.lowerOK:\n\t\twidth := float64(belief.upperBad - belief.lowerOK)\n\t\tpos := float64(amt-belief.lowerOK) / width\n\t\tpos = math.Max(0, math.Min(1, pos))\n\t\tlearned = 0.985 - 0.98*pos\n\n\tcase belief.estimate > 0:\n\t\tscale := math.Max(float64(capacity)*0.07, 1)\n\t\tlearned = 1 / (1 + math.Exp(\n\t\t\t(float64(amt)-float64(belief.estimate))/scale,\n\t\t))\n\n\tcase belief.upperBad > 0:\n\t\tratio := float64(amt) / float64(belief.upperBad)\n\t\tlearned = 0.01 + 0.97*math.Exp(-3.2*ratio)\n\n\tdefault:\n\t\tlearned = math.Max(prior, 0.80)\n\t}\n\n\tconfidence := 1 - math.Exp(-0.70*belief.conf)\n\tif !now.IsZero() && !belief.updated.IsZero() {\n\t\tage := now.Sub(belief.updated)\n\t\tif age < 0 {\n\t\t\tage = 0\n\t\t}\n\t\tconfidence *= math.Exp(-float64(age) /\n\t\t\tfloat64(18*time.Minute))\n\t}\n\n\treturn clampProbability(\n\t\tconfidence*clampProbability(learned) +\n\t\t\t(1-confidence)*prior,\n\t)\n}\n\nfunc recordPass(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateMemory.Lock()\n\tdefer candidateMemory.Unlock()\n\n\tb := candidateMemory.beliefs[key]\n\tb.capacity = capacity\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperBad > 0 && amt >= b.upperBad {\n\t\tb.upperBad = 0\n\t}\n\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.84 * float64(capacity),\n\t)\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.conf = math.Min(8, b.conf+1)\n\tb.updated = now\n\tcandidateMemory.beliefs[key] = b\n}\n\nfunc recordFailure(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateMemory.Lock()\n\tdefer candidateMemory.Unlock()\n\n\tb := candidateMemory.beliefs[key]\n\tb.capacity = capacity\n\n\tif b.upperBad == 0 || amt < b.upperBad {\n\t\tb.upperBad = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = 0\n\t}\n\n\tdepletedEstimate := amt / 4\n\tif b.estimate == 0 || depletedEstimate < b.estimate {\n\t\tb.estimate = depletedEstimate\n\t}\n\n\tb.conf = math.Min(8, b.conf+1.35)\n\tb.updated = now\n\tcandidateMemory.beliefs[key] = b\n}\n\nfunc recordSettlement(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) {\n\n\tcandidateMemory.Lock()\n\tdefer candidateMemory.Unlock()\n\n\tb := candidateMemory.beliefs[key]\n\tb.capacity = capacity\n\n\tpreEstimate := b.estimate\n\thighEstimate := lnwire.MilliSatoshi(\n\t\t0.84 * float64(capacity),\n\t)\n\tif preEstimate < highEstimate {\n\t\tpreEstimate = highEstimate\n\t}\n\tif preEstimate < amt {\n\t\tpreEstimate = amt\n\t}\n\n\tif b.lowerOK < amt {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperBad > 0 && amt >= b.upperBad {\n\t\tb.upperBad = 0\n\t}\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperBad > amt {\n\t\tb.upperBad -= amt\n\t} else {\n\t\tb.upperBad = 0\n\t}\n\tif preEstimate > amt {\n\t\tb.estimate = preEstimate - amt\n\t} else {\n\t\tb.estimate = 0\n\t}\n\n\tb.conf = math.Min(8, b.conf+1)\n\tb.updated = now\n\tcandidateMemory.beliefs[key] = b\n}\n\nfunc recordReverseCredit(key candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi, now time.Time) {\n\n\treverse := candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n\n\tcandidateMemory.Lock()\n\tdefer candidateMemory.Unlock()\n\n\tb := candidateMemory.beliefs[reverse]\n\tb.capacity = capacity\n\n\tif b.lowerOK < amt {\n\t\tb.lowerOK = amt\n\t}\n\tif b.estimate == 0 {\n\t\tb.estimate = amt\n\t} else if b.estimate < capacity-amt {\n\t\tb.estimate += amt\n\t}\n\tif b.estimate > capacity {\n\t\tb.estimate = capacity\n\t}\n\tif b.upperBad > 0 && b.upperBad <= b.lowerOK {\n\t\tb.upperBad = 0\n\t}\n\n\tb.conf = math.Min(8, b.conf+0.8)\n\tb.updated = now\n\tcandidateMemory.beliefs[reverse] = b\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\tlocalSpent map[uint64]lnwire.MilliSatoshi\n\tlocalFailed map[uint64]lnwire.MilliSatoshi\n\n\tattemptPenalty map[candidateEdgeKey]float64\n\tpolicyBad map[candidateEdgeKey]bool\n\n\tfailedAttempts uint32\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\tnow := view.Now()\n\n\tcandidateMemory.Lock()\n\tif !candidateMemory.lastNow.IsZero() &&\n\t\t!now.IsZero() && now.Before(candidateMemory.lastNow) {\n\n\t\tcandidateMemory.beliefs =\n\t\t\tmake(map[candidateEdgeKey]candidateBelief)\n\t}\n\tif now.After(candidateMemory.lastNow) {\n\t\tcandidateMemory.lastNow = now\n\t}\n\tcandidateMemory.Unlock()\n\n\trouter := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: localBalances,\n\t\tlocalSpent: make(map[uint64]lnwire.MilliSatoshi),\n\t\tlocalFailed: make(map[uint64]lnwire.MilliSatoshi),\n\t\tattemptPenalty: make(map[candidateEdgeKey]float64),\n\t\tpolicyBad: make(map[candidateEdgeKey]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(\n\t\t\tctx, node, func(ch *graphdb.DirectedChannel) error {\n\t\t\t\tif !seen[ch.OtherNode] {\n\t\t\t\t\tseen[ch.OtherNode] = true\n\t\t\t\t\tqueue = append(queue, ch.OtherNode)\n\t\t\t\t}\n\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tif _, exists := router.edges[key]; exists {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.\n\t\t\t\t\t\tFeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\trouter.edges[key] = edge\n\t\t\t\trouter.incomingEdges[key.to] = append(\n\t\t\t\t\trouter.incomingEdges[key.to], edge,\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 router, nil\n}\n\ntype candidateItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamt lnwire.MilliSatoshi\n\thops int\n}\n\ntype candidateQueue []*candidateItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(x any) {\n\t*q = append(*q, x.(*candidateItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\treturn last\n}\n\nfunc (r *candidateRouter) edgeProbability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.key.from == r.source {\n\t\treturn 0.997\n\t}\n\n\treturn beliefProbability(\n\t\tedge.key, amt, edge.capacity, r.view.Now(),\n\t)\n}\n\nfunc (r *candidateRouter) localAvailable(chanID uint64) lnwire.MilliSatoshi {\n\tbalance := r.localBalances[chanID]\n\tspent := r.localSpent[chanID]\n\tif spent >= balance {\n\t\treturn 0\n\t}\n\treturn balance - spent\n}\n\nfunc (r *candidateRouter) findRoute(amt lnwire.MilliSatoshi) (*route.Route,\n\tfloat64, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, 0, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, 0, errors.New(\"source is payment target\")\n\t}\n\n\tdist := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tdist[r.spec.Target] = 0\n\trequired[r.spec.Target] = amt\n\n\tpq := &candidateQueue{}\n\theap.Push(pq, &candidateItem{\n\t\tnode: r.spec.Target,\n\t\tamt: amt,\n\t})\n\n\tfor pq.Len() > 0 {\n\t\titem := heap.Pop(pq).(*candidateItem)\n\t\tbestScore, ok := dist[item.node]\n\t\tif !ok || item.score > bestScore+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= maxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tarriving := required[item.node]\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif r.policyBad[edge.key] || !edge.usable(arriving) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from == r.source {\n\t\t\t\tif arriving > r.localAvailable(edge.key.chanID) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif bound := r.localFailed[edge.key.chanID];\n\t\t\t\t\tbound > 0 && arriving >= bound {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := arriving\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(arriving)\n\t\t\t\tif fee < 0 ||\n\t\t\t\t\tarriving > lnwire.MilliSatoshi(\n\t\t\t\t\t\tmath.MaxInt64,\n\t\t\t\t\t)-fee {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tp := r.edgeProbability(edge, arriving)\n\t\t\triskCost := -math.Log(p)\n\t\t\tfeeCost := 0.0\n\t\t\tif amt > 0 {\n\t\t\t\tfeeCost = 18 * float64(fee) /\n\t\t\t\t\tfloat64(amt)\n\t\t\t}\n\n\t\t\tretryCost := r.attemptPenalty[edge.key]\n\t\t\tscore := item.score + riskCost + feeCost +\n\t\t\t\tretryCost + 0.012\n\n\t\t\told, exists := dist[edge.key.from]\n\t\t\tif exists && score >= old-1e-12 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdist[edge.key.from] = score\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\t\t\theap.Push(pq, &candidateItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tscore: score,\n\t\t\t\tamt: sending,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := dist[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, path, err := r.buildRoute(amt, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tlogProbability := 0.0\n\tfor i, edge := range path {\n\t\tamtOver := rt.TotalAmount\n\t\tif i > 0 {\n\t\t\tamtOver = rt.Hops[i-1].AmtToForward\n\t\t}\n\t\tlogProbability += math.Log(\n\t\t\tr.edgeProbability(edge, amtOver),\n\t\t)\n\t}\n\n\treturn rt, math.Exp(math.Max(logProbability, -700)), nil\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route,\n\t[]*candidateEdge, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, nil, errors.New(\"route contains a cycle\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tpath = append(path, edge)\n\t\tif len(path) > maxRouteHops {\n\t\t\treturn nil, nil, errors.New(\"route exceeds hop limit\")\n\t\t}\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, nil, errors.New(\"empty route\")\n\t}\n\n\tamtOver := make([]lnwire.MilliSatoshi, len(path))\n\texpiryOver := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamtOver[last] = amt\n\texpiryOver[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamtOver[i] = amtOver[i+1] +\n\t\t\tforwardingEdge.fee(amtOver[i+1])\n\t\texpiryOver[i] = expiryOver[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamtToForward := amt\n\t\toutgoingExpiry := uint32(finalCltvDelta)\n\t\tif i < last {\n\t\t\tamtToForward = amtOver[i+1]\n\t\t\toutgoingExpiry = expiryOver[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amtToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiryOver[0],\n\t\tTotalAmount: amtOver[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, path, nil\n}\n\nfunc addShardSize(sizes *[]lnwire.MilliSatoshi,\n\tseen map[lnwire.MilliSatoshi]bool, size, minimum,\n\tmaximum lnwire.MilliSatoshi) {\n\n\tif size < minimum {\n\t\tsize = minimum\n\t}\n\tif size > maximum {\n\t\tsize = maximum\n\t}\n\tif size <= 0 || seen[size] {\n\t\treturn\n\t}\n\n\tseen[size] = true\n\t*sizes = append(*sizes, size)\n}\n\nfunc scaledAmount(amt lnwire.MilliSatoshi,\n\tnumerator, denominator int64) lnwire.MilliSatoshi {\n\n\treturn lnwire.MilliSatoshi(\n\t\tint64(amt) * numerator / denominator,\n\t)\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\tminimum := amt / lnwire.MilliSatoshi(partsLeft)\n\tif amt%lnwire.MilliSatoshi(partsLeft) != 0 {\n\t\tminimum++\n\t}\n\n\tvar sizes []lnwire.MilliSatoshi\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\n\taddShardSize(&sizes, seen, amt, minimum, amt)\n\taddShardSize(\n\t\t&sizes, seen, scaledAmount(amt, 4, 5), minimum, amt,\n\t)\n\taddShardSize(\n\t\t&sizes, seen, scaledAmount(amt, 3, 5), minimum, amt,\n\t)\n\taddShardSize(\n\t\t&sizes, seen, scaledAmount(amt, 1, 2), minimum, amt,\n\t)\n\taddShardSize(\n\t\t&sizes, seen, scaledAmount(amt, 2, 5), minimum, amt,\n\t)\n\taddShardSize(&sizes, seen, minimum, minimum, amt)\n\n\tvar (\n\t\tbestRoute *route.Route\n\t\tbestUtility = math.Inf(-1)\n\t\tlastErr error\n\t)\n\n\tfor _, size := range sizes {\n\t\trt, probability, err := r.findRoute(size)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability = math.Max(probability, 1e-300)\n\t\tprogress := float64(size) / float64(minimum)\n\t\tutility := math.Log(probability) +\n\t\t\t0.78*math.Log(math.Max(progress, 1))\n\n\t\tfee := rt.TotalAmount - size\n\t\tif size > 0 {\n\t\t\tutility -= 12 * float64(fee) / float64(size)\n\t\t}\n\n\t\tif utility > bestUtility {\n\t\t\tbestUtility = utility\n\t\t\tbestRoute = rt\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\tif lastErr == nil {\n\t\t\tlastErr = errors.New(\"no route found\")\n\t\t}\n\t\treturn nil, lastErr\n\t}\n\n\treturn bestRoute, nil\n}\n\nfunc routeEdgeKeys(rt *route.Route) []candidateEdgeKey {\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tfrom := rt.SourcePubKey\n\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys\n}\n\nfunc routeAmountAt(rt *route.Route, edgeIndex int) lnwire.MilliSatoshi {\n\tif edgeIndex == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\treturn rt.Hops[edgeIndex-1].AmtToForward\n}\n\nfunc failureEdgeIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"attempt route is nil\")\n\t}\n\n\tkeys := routeEdgeKeys(rt)\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamtOver := routeAmountAt(rt, i)\n\t\t\trecordSettlement(\n\t\t\t\tkey, amtOver, edge.capacity, now,\n\t\t\t)\n\t\t\trecordReverseCredit(\n\t\t\t\tkey, amtOver, edge.capacity, now,\n\t\t\t)\n\n\t\t\tr.attemptPenalty[key] *= 0.45\n\t\t}\n\n\t\tif len(rt.Hops) > 0 {\n\t\t\tfirst := rt.Hops[0]\n\t\t\tr.localSpent[first.ChannelID] += rt.TotalAmount\n\t\t}\n\n\t\tr.failedAttempts = 0\n\t\treturn nil\n\t}\n\n\tr.failedAttempts++\n\tfailIndex := failureEdgeIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\t// Every channel preceding the failure carried the HTLC successfully.\n\t// The failed attempt is atomic, so these observations do not move funds.\n\tif failIndex > 0 {\n\t\tfor i := 0; i < failIndex && i < len(keys); i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trecordPass(\n\t\t\t\tkeys[i], routeAmountAt(rt, i),\n\t\t\t\tedge.capacity, now,\n\t\t\t)\n\t\t\tr.attemptPenalty[keys[i]] *= 0.75\n\t\t}\n\t}\n\n\tswitch result.Failure.Code() {\n\tcase lnwire.CodeTemporaryChannelFailure:\n\t\tif failIndex >= 0 && failIndex < len(keys) {\n\t\t\tkey := keys[failIndex]\n\t\t\tedge := r.edges[key]\n\t\t\tamtOver := routeAmountAt(rt, failIndex)\n\n\t\t\tif edge != nil {\n\t\t\t\trecordFailure(\n\t\t\t\t\tkey, amtOver, edge.capacity, now,\n\t\t\t\t)\n\t\t\t}\n\t\t\tr.attemptPenalty[key] += 1.4 +\n\t\t\t\t0.08*float64(r.failedAttempts)\n\n\t\t\tif failIndex == 0 {\n\t\t\t\told := r.localFailed[key.chanID]\n\t\t\t\tif old == 0 || amtOver < old {\n\t\t\t\t\tr.localFailed[key.chanID] = amtOver\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, key := range keys {\n\t\t\t\tr.attemptPenalty[key] += 0.35\n\t\t\t}\n\t\t}\n\n\tcase lnwire.CodeFeeInsufficient,\n\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\tif failIndex >= 0 && failIndex < len(keys) {\n\t\t\tkey := keys[failIndex]\n\t\t\tr.policyBad[key] = true\n\t\t\tr.attemptPenalty[key] += 8\n\t\t} else {\n\t\t\tfor _, key := range keys {\n\t\t\t\tr.attemptPenalty[key] += 0.65\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\t// Unknown failures provide no safe liquidity bound. Penalizing the\n\t\t// attempted path still prevents an identical retry loop.\n\t\tfor _, key := range keys {\n\t\t\tr.attemptPenalty[key] += 0.45\n\t\t}\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 12,
|
|
"parent": 0,
|
|
"score": 0.349,
|
|
"accepted": true,
|
|
"frontier": true,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\tproportional := (amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n\n\treturn e.baseFeeMsat + proportional\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(amt,\n\tcapacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tif b.conf < 0 {\n\t\tb.conf = 0\n\t}\n\tif b.conf > 0.99 {\n\t\tb.conf = 0.99\n\t}\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Background traffic can invalidate old directional evidence quickly.\n\tconst halfLifeMinutes = 35.0\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\n\tif conf < 0.01 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tb.conf = conf\n\n\t// Bounds become hints rather than permanent facts after substantial age.\n\tif now.Sub(b.updatedAt) > 20*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidateKnowledge.beliefs[reverseKey]\n\treverse = candidatePrepareObservation(\n\t\treverse, capacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.88)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\thighEstimate := edge.capacity * 9 / 10\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.92)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\tlowEstimate := amt / 16\n\tcapFloor := edge.capacity / 500\n\tif capFloor < 1 {\n\t\tcapFloor = 1\n\t}\n\tif lowEstimate > capFloor {\n\t\tlowEstimate = capFloor\n\t}\n\tif lowEstimate < b.lowerOK {\n\t\tlowEstimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || lowEstimate < b.estimate {\n\t\tb.estimate = lowEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.98)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\testimate := b.estimate\n\tif estimate < amt {\n\t\testimate = edge.capacity * 9 / 10\n\t\tif estimate < amt {\n\t\t\testimate = amt\n\t\t}\n\t}\n\n\tb.estimate = estimate - amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\tif probability < 0.005 {\n\t\treturn 0.005\n\t}\n\tif probability > 0.995 {\n\t\treturn 0.995\n\t}\n\n\treturn probability\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\tlowMode := 0.48 * math.Exp(-ratio/0.025)\n\thighMode := 0.50 /\n\t\t(1 + math.Exp((ratio-0.92)/0.045))\n\n\treturn candidateClampProbability(0.005 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.035, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tif fraction < 0 {\n\t\t\tfraction = 0\n\t\t}\n\t\tif fraction > 1 {\n\t\t\tfraction = 1\n\t\t}\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.4)\n\t\tprobability = 0.65*bounded + 0.35*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tif failedAt := r.sessionFailed[edge.key]; failedAt != 0 {\n\t\tif amt >= failedAt {\n\t\t\treturn 0\n\t\t}\n\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tif ratio > 0.75 {\n\t\t\treturn 0.006\n\t\t}\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\tif conf == 0 {\n\t\treturn prior\n\t}\n\n\tlearned := candidateLearnedProbability(\n\t\tb, amt, edge.capacity,\n\t)\n\tprobability := conf*learned + (1-conf)*prior\n\n\tif failedAt := r.sessionFailed[edge.key]; failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.55:\n\t\t\tprobability *= 0.08\n\t\tcase ratio > 0.30:\n\t\t\tprobability *= 0.30\n\t\tcase ratio > 0.12:\n\t\t\tprobability *= 0.65\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 8 * float64(fee) / feeScale\n\t\t\thopCost := 0.055\n\t\t\tuseCost := 0.035 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 8,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 20)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\tlimit := partsLeft\n\tif limit > 16 {\n\t\tlimit = 16\n\t}\n\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, candidateCeilDiv(amt, partsLeft),\n\t)\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tshards := candidateShardAmounts(amt, partsLeft)\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log1p(\n\t\t\tfloat64(shard) / math.Max(float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 6 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.48*progress - feePenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\n\t\tif probability >= 0.55 && shard >= amt/2 {\n\t\t\tbestRoute = rt\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.20\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.25, 6,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.15\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.45\n\t\tif i > len(keys)/2 {\n\t\t\tpenalty = 0.60\n\t\t}\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 4,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown-source failures contain no reliable channel attribution.\n\t// Penalizing the route still forces exploration without poisoning\n\t// persistent channel beliefs.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 13,
|
|
"parent": 6,
|
|
"score": 0.2423,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 56\n\tcandidateMaxRouteHops = 20\n\tcandidateMaxLabels = 4\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(amt,\n\tcapacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Persistent evidence is useful across nearby payments, but background\n\t// traffic can substantially alter a channel within a few virtual minutes.\n\tconst halfLifeMinutes = 12.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tb.conf = conf\n\n\t// Old bounds remain represented by the estimate, but cease being hard\n\t// observations once background traffic could plausibly cross them.\n\tif now.Sub(b.updatedAt) > 6*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.86)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\thighEstimate := edge.capacity * 9 / 10\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.93)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity miss is strong evidence for the depleted mode, but it does\n\t// not imply that much smaller shards cannot pass.\n\tlowEstimate := amt / 12\n\tcapFloor := edge.capacity / 400\n\tif capFloor < 1 {\n\t\tcapFloor = 1\n\t}\n\tif lowEstimate > capFloor {\n\t\tlowEstimate = capFloor\n\t}\n\tif lowEstimate < b.lowerOK {\n\t\tlowEstimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || lowEstimate < b.estimate {\n\t\tb.estimate = lowEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\testimate := b.estimate\n\tif estimate < amt {\n\t\testimate = edge.capacity * 9 / 10\n\t\tif estimate < amt {\n\t\t\testimate = amt\n\t\t}\n\t}\n\n\tb.estimate = estimate - amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.005, math.Min(probability, 0.995))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode describes tiny payments that pass even on the depleted\n\t// side. The high mode describes channels whose liquidity is concentrated\n\t// almost entirely in the requested direction.\n\tlowMode := 0.49 * math.Exp(-ratio/0.026)\n\thighMode := 0.495 /\n\t\t(1 + math.Exp((ratio-0.915)/0.042))\n\n\treturn candidateClampProbability(0.005 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.03, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.2)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.78:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.55:\n\t\t\tprobability *= 0.11\n\t\tcase ratio > 0.32:\n\t\t\tprobability *= 0.35\n\t\tcase ratio > 0.15:\n\t\t\tprobability *= 0.72\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgePenalty(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tpenalty := r.sessionPenalty[edge.key]\n\tfailedAt := r.sessionFailed[edge.key]\n\n\tif failedAt != 0 && amt < failedAt {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tpenalty *= math.Max(0.08, ratio)\n\t}\n\n\treturn penalty\n}\n\ntype candidateLabel struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n\n\tedge *candidateEdge\n\tchild *candidateLabel\n\tactive bool\n}\n\ntype candidateQueue []*candidateLabel\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].amount != q[j].amount {\n\t\treturn q[i].amount < q[j].amount\n\t}\n\n\treturn q[i].hops < q[j].hops\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateLabel))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\ntype candidateLabelKey struct {\n\tnode route.Vertex\n\thops uint16\n}\n\nfunc candidateDominates(a, b *candidateLabel) bool {\n\treturn a.score <= b.score+1e-12 && a.amount <= b.amount\n}\n\nfunc candidateInsertLabel(\n\tlabels map[candidateLabelKey][]*candidateLabel,\n\tlabel *candidateLabel) bool {\n\n\tkey := candidateLabelKey{\n\t\tnode: label.node,\n\t\thops: label.hops,\n\t}\n\tcurrent := labels[key]\n\n\tfor _, existing := range current {\n\t\tif existing.active && candidateDominates(existing, label) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tkept := current[:0]\n\tfor _, existing := range current {\n\t\tif existing.active && candidateDominates(label, existing) {\n\t\t\texisting.active = false\n\t\t\tcontinue\n\t\t}\n\t\tif existing.active {\n\t\t\tkept = append(kept, existing)\n\t\t}\n\t}\n\n\tlabel.active = true\n\tkept = append(kept, label)\n\n\tif len(kept) > candidateMaxLabels {\n\t\tworst := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tworstValue := kept[worst].score +\n\t\t\t\t0.12*math.Log1p(float64(kept[worst].amount))\n\t\t\tvalue := kept[i].score +\n\t\t\t\t0.12*math.Log1p(float64(kept[i].amount))\n\t\t\tif value > worstValue {\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tremoved := kept[worst]\n\t\tremoved.active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t\tif removed == label {\n\t\t\tlabels[key] = kept\n\t\t\treturn false\n\t\t}\n\t}\n\n\tlabels[key] = kept\n\treturn true\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\ttarget := &candidateLabel{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t\tactive: true,\n\t}\n\n\tlabels := make(map[candidateLabelKey][]*candidateLabel)\n\tlabels[candidateLabelKey{\n\t\tnode: r.spec.Target,\n\t\thops: 0,\n\t}] = []*candidateLabel{target}\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, target)\n\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateLabel)\n\t\tif !item.active {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\trt, err := r.buildRoute(deliver, item)\n\t\t\tif err != nil {\n\t\t\t\titem.active = false\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn rt, item.risk, nil\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\t\t\tif sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 5.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.075\n\t\t\tuseCost := 0.055 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.edgePenalty(edge, item.amount)\n\n\t\t\tlabel := &candidateLabel{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: item.score + riskCost + feeCost +\n\t\t\t\t\thopCost + useCost + penalty,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t\tedge: edge,\n\t\t\t\tchild: item,\n\t\t\t}\n\n\t\t\tif candidateInsertLabel(labels, label) {\n\t\t\t\theap.Push(queue, label)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, 0, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tsourceLabel *candidateLabel) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor label := sourceLabel; label.node != r.spec.Target; {\n\t\tif visited[label.node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[label.node] = true\n\n\t\tif label.edge == nil || label.child == nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"broken path at %v\", label.node,\n\t\t\t)\n\t\t}\n\n\t\tpath = append(path, label.edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tlabel = label.child\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt, minimum,\n\tmaximum lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt < minimum || amt > maximum || amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc (r *candidateRouter) candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts := make([]lnwire.MilliSatoshi, 0, 32)\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, amt, minimum, amt,\n\t)\n\tamounts = candidateAppendUnique(\n\t\tamounts, minimum, minimum, amt,\n\t)\n\n\tlimit := partsLeft\n\tif limit > 24 {\n\t\tlimit = 24\n\t}\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t\tminimum, amt,\n\t\t)\n\t}\n\n\tfor _, fraction := range []float64{\n\t\t0.85, 0.72, 0.60, 0.48, 0.38, 0.30, 0.22, 0.16,\n\t} {\n\t\tshard := lnwire.MilliSatoshi(float64(amt) * fraction)\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, shard, minimum, amt,\n\t\t)\n\t}\n\n\t// Explicitly test amounts below known failures. These candidates often\n\t// recover depleted-mode channels without forcing the search onto a much\n\t// longer path.\n\tfor _, failedAt := range r.sessionFailed {\n\t\tfor _, fraction := range []float64{0.72, 0.48, 0.27, 0.12} {\n\t\t\tshard := lnwire.MilliSatoshi(\n\t\t\t\tfloat64(failedAt) * fraction,\n\t\t\t)\n\t\t\tamounts = candidateAppendUnique(\n\t\t\t\tamounts, shard, minimum, amt,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := r.candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log(\n\t\t\tmath.Max(float64(shard)/float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 4.5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\thopPenalty := 0.0075 * float64(len(rt.Hops))\n\n\t\t// Reliability dominates. Progress only breaks ties between routes\n\t\t// whose probabilities are reasonably close.\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.24*progress - feePenalty - hopPenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\n\t\tif probability >= 0.82 && shard >= amt*3/5 {\n\t\t\tbestRoute = rt\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.12\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+0.65, 4,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.28\n\t\tif i > len(keys)/2 {\n\t\t\tpenalty = 0.42\n\t\t}\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 3,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 8\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 14,
|
|
"parent": 6,
|
|
"score": 0.288,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 12\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateKnowledgeKey struct {\n\tnetwork string\n\tedge candidateEdgeKey\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\tproportional := (amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n\n\treturn e.baseFeeMsat + proportional\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateKnowledgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateKnowledgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Recent evidence remains valuable across adjacent payments, while\n\t// background traffic gradually returns the belief to its prior.\n\tconst halfLifeMinutes = 18.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tage := now.Sub(b.updatedAt)\n\tb.conf = conf\n\n\t// Bounds are exact within a short observation window. After that they\n\t// become a point estimate because background payments may have moved funds.\n\tif age > 8*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tnetwork string, edge *candidateEdge) candidateLiquidityBelief {\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{capacity: edge.capacity}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tnetwork string, key candidateEdgeKey,\n\tforward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tforwardKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: key,\n\t}\n\tcandidateKnowledge.beliefs[forwardKey] = forward\n\n\treverseEdge := candidateReverseKey(key)\n\treverseKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: reverseEdge,\n\t}\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.82)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tknowledgeKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[knowledgeKey],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tknowledgeKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[knowledgeKey],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// Under a bimodal prior, a liquidity failure is evidence that the\n\t// direction is close to its depleted mode.\n\testimate := amt / 24\n\tfloor := edge.capacity / 1000\n\tif floor < 1 {\n\t\tfloor = 1\n\t}\n\tif estimate > floor {\n\t\testimate = floor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tknowledgeKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[knowledgeKey],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\n\tb.estimate -= amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tnetworkID string\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\trouteUses map[uint64]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tnetworkID: fmt.Sprintf(\"%T:%p\", view, view),\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t\trouteUses: make(map[uint64]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.005, math.Min(probability, 0.995))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\tlowMode := 0.49 * math.Exp(-ratio/0.022)\n\thighMode := 0.495 /\n\t\t(1 + math.Exp((ratio-0.91)/0.042))\n\n\treturn candidateClampProbability(\n\t\t0.005 + lowMode + highMode,\n\t)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.75*bounded + 0.25*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(r.networkID, edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.70:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.45:\n\t\t\tprobability *= 0.12\n\t\tcase ratio > 0.25:\n\t\t\tprobability *= 0.38\n\t\tcase ratio > 0.10:\n\t\t\tprobability *= 0.72\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 7 * float64(fee) / feeScale\n\t\t\thopCost := 0.14\n\t\t\tuseCost := 0.12 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 24)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\tfor _, numerator := range []int64{80, 65, 50, 40, 30, 22, 16} {\n\t\tshard := lnwire.MilliSatoshi(\n\t\t\t(int64(amt)*numerator + 99) / 100,\n\t\t)\n\t\tamounts = candidateAppendUnique(amounts, shard)\n\t}\n\n\tlimit := partsLeft\n\tif limit > 16 {\n\t\tlimit = 16\n\t}\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, candidateCeilDiv(amt, partsLeft),\n\t)\n\n\treturn amounts\n}\n\nfunc candidateRouteHash(rt *route.Route) uint64 {\n\tconst (\n\t\toffset = uint64(1469598103934665603)\n\t\tprime = uint64(1099511628211)\n\t)\n\n\thash := offset\n\tfor _, hop := range rt.Hops {\n\t\thash ^= hop.ChannelID\n\t\thash *= prime\n\t}\n\n\treturn hash\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tr.routeUses[candidateRouteHash(rt)]++\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\tif shard < minimum {\n\t\t\tcontinue\n\t\t}\n\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log1p(\n\t\t\tfloat64(shard) / math.Max(float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\trepeatPenalty := 0.55 * math.Min(\n\t\t\tfloat64(r.routeUses[candidateRouteHash(rt)]), 6,\n\t\t)\n\t\tlongPenalty := 0.035 * math.Max(\n\t\t\tfloat64(len(rt.Hops)-6), 0,\n\t\t)\n\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.72*progress - feePenalty -\n\t\t\trepeatPenalty - longPenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.12\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.8, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.08\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tlast := len(keys) - 1\n\tfor i, key := range keys {\n\t\tpenalty := 1.0\n\t\tif i == 0 {\n\t\t\tpenalty = 0.35\n\t\t} else if i >= last-1 {\n\t\t\tpenalty = 1.5\n\t\t}\n\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 8,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tbalance := r.localBalances[first.chanID]\n\t\tif balance > amounts[0] {\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tr.networkID, edge,\n\t\t\t\tamounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unattributed failure should diversify the next route without\n\t// corrupting persistent liquidity evidence.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 15,
|
|
"parent": 0,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"sync\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tfinalCltvDelta = uint32(40)\n\tbeliefHalfLife = int64(30 * 60 * 1_000_000_000)\n)\n\ntype candidateEdge struct {\n\tchanID uint64\n\tfrom, to route.Vertex\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\treturn e.baseFeeMsat + amt*e.feeRatePPM/1_000_000\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype channelKey struct {\n\tworld uint64\n\tgraph uint64\n\tchanID uint64\n\tfrom route.Vertex\n}\n\ntype directedLookup struct {\n\tchanID uint64\n\tfrom route.Vertex\n}\n\ntype liquidityBelief struct {\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconfidence float64\n\tupdated int64\n}\n\nvar sharedBeliefs = struct {\n\tsync.Mutex\n\tvalues map[channelKey]liquidityBelief\n}{\n\tvalues: make(map[channelKey]liquidityBelief),\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedgeLookup map[directedLookup]*candidateEdge\n\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\treserved map[directedLookup]lnwire.MilliSatoshi\n\tpending map[*route.Route]map[directedLookup]lnwire.MilliSatoshi\n\n\tbeliefs map[directedLookup]liquidityBelief\n\tloaded map[directedLookup]bool\n\tpolicyBad map[directedLookup]bool\n\tpenalty map[directedLookup]float64\n\n\tworldID uint64\n\tgraphID uint64\n}\n\nfunc newCandidateRouter(view routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif spec == nil {\n\t\treturn nil, errors.New(\"nil payment specification\")\n\t}\n\n\tbalanceCopy := make(map[uint64]lnwire.MilliSatoshi, len(localBalances))\n\tfor chanID, balance := range localBalances {\n\t\tbalanceCopy[chanID] = balance\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedgeLookup: make(map[directedLookup]*candidateEdge),\n\t\tlocalBalances: balanceCopy,\n\t\treserved: make(map[directedLookup]lnwire.MilliSatoshi),\n\t\tpending: make(map[*route.Route]map[directedLookup]lnwire.MilliSatoshi),\n\t\tbeliefs: make(map[directedLookup]liquidityBelief),\n\t\tloaded: make(map[directedLookup]bool),\n\t\tpolicyBad: make(map[directedLookup]bool),\n\t\tpenalty: make(map[directedLookup]float64),\n\t\tworldID: simulationIdentity(view),\n\t}\n\n\tctx := context.Background()\n\tseen := make(map[route.Vertex]bool)\n\tqueue := []route.Vertex{source}\n\tseen[source] = true\n\n\tvar graphHash uint64\n\tvar edgeCount uint64\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tedge := &candidateEdge{\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[edge.to] = append(\n\t\t\t\t\tr.incomingEdges[edge.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edgeLookup[directedLookup{\n\t\t\t\t\tchanID: edge.chanID,\n\t\t\t\t\tfrom: edge.from,\n\t\t\t\t}] = edge\n\n\t\t\t\tgraphHash ^= edgeFingerprint(edge)\n\t\t\t\tedgeCount++\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tr.graphID = mix64(graphHash ^ mix64(edgeCount))\n\n\treturn r, nil\n}\n\nfunc simulationIdentity(view routing.SimNetworkView) uint64 {\n\tvalue := reflect.ValueOf(view)\n\tif !value.IsValid() {\n\t\treturn 0\n\t}\n\n\tswitch value.Kind() {\n\tcase reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func,\n\t\treflect.Chan, reflect.UnsafePointer:\n\n\t\treturn uint64(value.Pointer())\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc edgeFingerprint(edge *candidateEdge) uint64 {\n\thash := mix64(edge.chanID) ^\n\t\tmix64(uint64(edge.capacity)) ^\n\t\tmix64(uint64(edge.baseFeeMsat)) ^\n\t\tmix64(uint64(edge.feeRatePPM)) ^\n\t\tmix64(uint64(edge.timeLockDelta))\n\n\tfor i, value := range edge.from {\n\t\thash ^= mix64(uint64(value) + uint64(i+1)*0x9e3779b97f4a7c15)\n\t}\n\tfor i, value := range edge.to {\n\t\thash ^= mix64(uint64(value) + uint64(i+37)*0x517cc1b727220a95)\n\t}\n\n\treturn mix64(hash)\n}\n\nfunc mix64(value uint64) uint64 {\n\tvalue ^= value >> 30\n\tvalue *= 0xbf58476d1ce4e5b9\n\tvalue ^= value >> 27\n\tvalue *= 0x94d049bb133111eb\n\tvalue ^= value >> 31\n\n\treturn value\n}\n\nfunc (r *candidateRouter) now() int64 {\n\treturn r.view.Now().UnixNano()\n}\n\nfunc (r *candidateRouter) sharedKey(key directedLookup) channelKey {\n\treturn channelKey{\n\t\tworld: r.worldID,\n\t\tgraph: r.graphID,\n\t\tchanID: key.chanID,\n\t\tfrom: key.from,\n\t}\n}\n\nfunc (r *candidateRouter) belief(key directedLookup) liquidityBelief {\n\tif r.loaded[key] {\n\t\treturn r.beliefs[key]\n\t}\n\n\tr.loaded[key] = true\n\n\tsharedBeliefs.Lock()\n\tbelief := sharedBeliefs.values[r.sharedKey(key)]\n\tsharedBeliefs.Unlock()\n\n\tr.beliefs[key] = belief\n\n\treturn belief\n}\n\nfunc (r *candidateRouter) storeBelief(key directedLookup,\n\tbelief liquidityBelief) {\n\n\tr.loaded[key] = true\n\tr.beliefs[key] = belief\n\n\tsharedBeliefs.Lock()\n\tsharedBeliefs.values[r.sharedKey(key)] = belief\n\tsharedBeliefs.Unlock()\n}\n\nfunc beliefWeight(belief liquidityBelief, now int64) float64 {\n\tif belief.confidence <= 0 {\n\t\treturn 0\n\t}\n\tif belief.updated > now {\n\t\treturn 0\n\t}\n\n\tage := now - belief.updated\n\tweight := belief.confidence *\n\t\tmath.Exp(-float64(age)/float64(beliefHalfLife))\n\n\treturn math.Min(0.985, math.Max(0, weight))\n}\n\nfunc bimodalPrior(amt, capacity lnwire.MilliSatoshi) float64 {\n\tif capacity <= 0 || amt > capacity {\n\t\treturn 0.005\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\tlowMode := 0.48 * math.Exp(-ratio/0.07)\n\thighMode := 0.50 / (1 + math.Exp((ratio-0.90)/0.06))\n\tprobability := lowMode + highMode\n\n\treturn math.Min(0.985, math.Max(0.005, probability))\n}\n\nfunc (r *candidateRouter) probability(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif edge.from == r.source {\n\t\treturn 0.999\n\t}\n\n\tkey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.from,\n\t}\n\tbelief := r.belief(key)\n\tprior := bimodalPrior(amt, edge.capacity)\n\tweight := beliefWeight(belief, r.now())\n\tif weight == 0 {\n\t\treturn prior\n\t}\n\n\tevidence := prior\n\tswitch {\n\tcase belief.lowerOK > 0 && amt <= belief.lowerOK:\n\t\tevidence = 0.995\n\n\tcase belief.upperFail > 0 && amt >= belief.upperFail:\n\t\tevidence = 0.005\n\n\tcase belief.estimate > 0:\n\t\twidth := math.Max(\n\t\t\tfloat64(edge.capacity)*0.075,\n\t\t\t1_000_000,\n\t\t)\n\t\tdistance := (float64(amt) - float64(belief.estimate)) /\n\t\t\twidth\n\t\tevidence = 1 / (1 + math.Exp(distance))\n\t\tevidence = math.Min(0.995, math.Max(0.005, evidence))\n\t}\n\n\tprobability := (1-weight)*prior + weight*evidence\n\n\treturn math.Min(0.995, math.Max(0.005, probability))\n}\n\nfunc (r *candidateRouter) freshFailureExcludes(edge *candidateEdge,\n\tamt lnwire.MilliSatoshi) bool {\n\n\tkey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.from,\n\t}\n\tbelief := r.belief(key)\n\tif belief.upperFail <= 0 {\n\t\treturn false\n\t}\n\n\tweight := beliefWeight(belief, r.now())\n\tretryThreshold := belief.upperFail * 9 / 10\n\n\treturn weight >= 0.72 && amt >= retryThreshold\n}\n\ntype dijkstraItem struct {\n\tnode route.Vertex\n\tscore float64\n\tamount lnwire.MilliSatoshi\n}\n\ntype dijkstraQueue []*dijkstraItem\n\nfunc (q dijkstraQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q dijkstraQueue) Less(i, j int) bool {\n\treturn q[i].score < q[j].score\n}\n\nfunc (q dijkstraQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *dijkstraQueue) Push(value any) {\n\t*q = append(*q, value.(*dijkstraItem))\n}\n\nfunc (q *dijkstraQueue) Pop() any {\n\told := *q\n\tlast := len(old) - 1\n\titem := old[last]\n\t*q = old[:last]\n\n\treturn item\n}\n\nfunc (r *candidateRouter) findRoute(\n\tamt lnwire.MilliSatoshi) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"invalid route amount\")\n\t}\n\tif r.source == r.spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tdist := make(map[route.Vertex]float64)\n\tarrival := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tdist[r.spec.Target] = 0\n\tarrival[r.spec.Target] = amt\n\n\tqueue := &dijkstraQueue{}\n\theap.Push(queue, &dijkstraItem{\n\t\tnode: r.spec.Target,\n\t\tscore: 0,\n\t\tamount: amt,\n\t})\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*dijkstraItem)\n\t\tbest, ok := dist[item.node]\n\t\tif !ok || item.score > best+0.001 {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tkey := directedLookup{\n\t\t\t\tchanID: edge.chanID,\n\t\t\t\tfrom: edge.from,\n\t\t\t}\n\t\t\tif r.policyBad[key] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treserved := r.reserved[key]\n\t\t\trequiredLiquidity := item.amount + reserved\n\t\t\tif !edge.usable(requiredLiquidity) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif r.freshFailureExcludes(edge, requiredLiquidity) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.from == r.source {\n\t\t\t\tavailable := r.localBalances[edge.chanID]\n\t\t\t\tif available < requiredLiquidity {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\trisk := 0.0\n\n\t\t\tif edge.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\n\t\t\t\tprobability := r.probability(\n\t\t\t\t\tedge, requiredLiquidity,\n\t\t\t\t)\n\t\t\t\triskScale := math.Max(\n\t\t\t\t\t100_000,\n\t\t\t\t\tfloat64(item.amount)*0.008,\n\t\t\t\t)\n\t\t\t\trisk = -math.Log(probability) * riskScale\n\t\t\t}\n\n\t\t\thistoryPenalty := r.penalty[key] *\n\t\t\t\tmath.Max(50_000, float64(item.amount)*0.003)\n\t\t\tscore := item.score + float64(fee) + risk +\n\t\t\t\thistoryPenalty\n\n\t\t\toldScore, exists := dist[edge.from]\n\t\t\tif exists && score >= oldScore {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdist[edge.from] = score\n\t\t\tarrival[edge.from] = sending\n\t\t\tnext[edge.from] = edge\n\t\t\theap.Push(queue, &dijkstraItem{\n\t\t\t\tnode: edge.from,\n\t\t\t\tscore: score,\n\t\t\t\tamount: sending,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := dist[r.source]; !ok {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\treturn r.buildRoute(amt, next)\n}\n\nfunc (r *candidateRouter) buildRoute(amt lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tvar path []*candidateEdge\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tnode = edge.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"empty route\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = amt\n\texpiries[last] = finalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\tforwardingEdge := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\tforwardingEdge.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(forwardingEdge.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := amt\n\t\toutgoingExpiry := finalCltvDelta\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.to,\n\t\t\tChannelID: edge.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc (r *candidateRouter) RequestRoute(amt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"payment amount is zero\")\n\t}\n\tif inFlightHtlcs >= r.spec.MaxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := r.spec.MaxParts - inFlightHtlcs\n\tminimum := (amt + lnwire.MilliSatoshi(partsLeft) - 1) /\n\t\tlnwire.MilliSatoshi(partsLeft)\n\n\tcandidates := []lnwire.MilliSatoshi{\n\t\tamt,\n\t\tamt * 13 / 20,\n\t\tamt * 9 / 20,\n\t\tamt * 3 / 10,\n\t\tamt / 5,\n\t\tminimum,\n\t}\n\n\tseen := make(map[lnwire.MilliSatoshi]bool)\n\tvar lastErr error\n\n\tfor _, candidate := range candidates {\n\t\tif candidate < minimum {\n\t\t\tcandidate = minimum\n\t\t}\n\t\tif candidate > amt || candidate <= 0 || seen[candidate] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[candidate] = true\n\n\t\trt, err := r.findRoute(candidate)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tr.reserveRoute(rt)\n\n\t\treturn rt, nil\n\t}\n\n\tif lastErr == nil {\n\t\tlastErr = errors.New(\"no route found\")\n\t}\n\n\treturn nil, lastErr\n}\n\nfunc routeEdgeAmount(rt *route.Route, edgeIndex int) lnwire.MilliSatoshi {\n\tif edgeIndex == 0 {\n\t\treturn rt.TotalAmount\n\t}\n\n\treturn rt.Hops[edgeIndex-1].AmtToForward\n}\n\nfunc (r *candidateRouter) routeEdges(\n\trt *route.Route) []struct {\n\tkey directedLookup\n\tedge *candidateEdge\n\tamount lnwire.MilliSatoshi\n} {\n\n\tresult := make([]struct {\n\t\tkey directedLookup\n\t\tedge *candidateEdge\n\t\tamount lnwire.MilliSatoshi\n\t}, 0, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkey := directedLookup{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t}\n\t\tresult = append(result, struct {\n\t\t\tkey directedLookup\n\t\t\tedge *candidateEdge\n\t\t\tamount lnwire.MilliSatoshi\n\t\t}{\n\t\t\tkey: key,\n\t\t\tedge: r.edgeLookup[key],\n\t\t\tamount: routeEdgeAmount(rt, i),\n\t\t})\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn result\n}\n\nfunc (r *candidateRouter) reserveRoute(rt *route.Route) {\n\treservation := make(map[directedLookup]lnwire.MilliSatoshi)\n\n\tfor _, routeEdge := range r.routeEdges(rt) {\n\t\treservation[routeEdge.key] += routeEdge.amount\n\t\tr.reserved[routeEdge.key] += routeEdge.amount\n\t}\n\n\tr.pending[rt] = reservation\n}\n\nfunc (r *candidateRouter) releaseRoute(rt *route.Route) {\n\treservation, ok := r.pending[rt]\n\tif !ok {\n\t\treturn\n\t}\n\n\tfor key, amount := range reservation {\n\t\tif r.reserved[key] <= amount {\n\t\t\tdelete(r.reserved, key)\n\t\t} else {\n\t\t\tr.reserved[key] -= amount\n\t\t}\n\t}\n\n\tdelete(r.pending, rt)\n}\n\nfunc (r *candidateRouter) recordPass(edge *candidateEdge,\n\tamount lnwire.MilliSatoshi) {\n\n\tif edge == nil || edge.from == r.source {\n\t\treturn\n\t}\n\n\tkey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.from,\n\t}\n\tbelief := r.belief(key)\n\n\tif amount > belief.lowerOK {\n\t\tbelief.lowerOK = amount\n\t}\n\tif belief.upperFail > 0 && belief.upperFail <= belief.lowerOK {\n\t\tbelief.upperFail = 0\n\t}\n\n\testimate := amount * 23 / 20\n\tif estimate > edge.capacity {\n\t\testimate = edge.capacity\n\t}\n\tif estimate > belief.estimate {\n\t\tbelief.estimate = estimate\n\t}\n\n\tbelief.confidence = math.Max(belief.confidence, 0.88)\n\tbelief.updated = r.now()\n\tr.storeBelief(key, belief)\n}\n\nfunc (r *candidateRouter) recordFailure(edge *candidateEdge,\n\tamount lnwire.MilliSatoshi) {\n\n\tif edge == nil {\n\t\treturn\n\t}\n\n\tkey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.from,\n\t}\n\n\tif edge.from == r.source {\n\t\tif amount > 0 && r.localBalances[edge.chanID] >= amount {\n\t\t\tr.localBalances[edge.chanID] = amount - 1\n\t\t}\n\t\treturn\n\t}\n\n\tbelief := r.belief(key)\n\tif belief.upperFail == 0 || amount < belief.upperFail {\n\t\tbelief.upperFail = amount\n\t}\n\tif belief.lowerOK >= amount {\n\t\tbelief.lowerOK = 0\n\t}\n\n\tfailureEstimate := amount / 4\n\tif belief.estimate == 0 || failureEstimate < belief.estimate {\n\t\tbelief.estimate = failureEstimate\n\t}\n\n\tbelief.confidence = 0.97\n\tbelief.updated = r.now()\n\tr.storeBelief(key, belief)\n\tr.penalty[key] += 1\n}\n\nfunc (r *candidateRouter) recordSettlement(edge *candidateEdge,\n\tamount lnwire.MilliSatoshi) {\n\n\tif edge == nil {\n\t\treturn\n\t}\n\n\tif edge.from == r.source {\n\t\tif balance := r.localBalances[edge.chanID]; balance > amount {\n\t\t\tr.localBalances[edge.chanID] = balance - amount\n\t\t} else {\n\t\t\tr.localBalances[edge.chanID] = 0\n\t\t}\n\t\treturn\n\t}\n\n\tkey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.from,\n\t}\n\tbelief := r.belief(key)\n\n\thighEstimate := edge.capacity * 9 / 10\n\tif belief.estimate < highEstimate {\n\t\tbelief.estimate = highEstimate\n\t}\n\tif belief.estimate > amount {\n\t\tbelief.estimate -= amount\n\t} else {\n\t\tbelief.estimate = 0\n\t}\n\n\tif belief.lowerOK > amount {\n\t\tbelief.lowerOK -= amount\n\t} else {\n\t\tbelief.lowerOK = 0\n\t}\n\tif belief.upperFail > amount {\n\t\tbelief.upperFail -= amount\n\t} else {\n\t\tbelief.upperFail = 0\n\t}\n\n\tbelief.confidence = math.Max(belief.confidence, 0.82)\n\tbelief.updated = r.now()\n\tr.storeBelief(key, belief)\n\n\treverseKey := directedLookup{\n\t\tchanID: edge.chanID,\n\t\tfrom: edge.to,\n\t}\n\treverseEdge := r.edgeLookup[reverseKey]\n\tif reverseEdge == nil {\n\t\treturn\n\t}\n\n\treverse := r.belief(reverseKey)\n\treverse.lowerOK += amount\n\tif reverse.lowerOK > reverseEdge.capacity {\n\t\treverse.lowerOK = reverseEdge.capacity\n\t}\n\treverse.estimate += amount\n\tif reverse.estimate > reverseEdge.capacity {\n\t\treverse.estimate = reverseEdge.capacity\n\t}\n\tif reverse.upperFail > 0 {\n\t\treverse.upperFail += amount\n\t\tif reverse.upperFail > reverseEdge.capacity {\n\t\t\treverse.upperFail = reverseEdge.capacity\n\t\t}\n\t}\n\treverse.confidence = math.Max(reverse.confidence, 0.80)\n\treverse.updated = r.now()\n\tr.storeBelief(reverseKey, reverse)\n}\n\nfunc failureOutgoingIndex(rt *route.Route,\n\tsource route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) ReportAttempt(_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"nil attempted route\")\n\t}\n\n\tr.releaseRoute(rt)\n\tedges := r.routeEdges(rt)\n\n\tif result.Failure == nil {\n\t\tfor _, routeEdge := range edges {\n\t\t\tr.recordPass(routeEdge.edge, routeEdge.amount)\n\t\t\tr.recordSettlement(routeEdge.edge, routeEdge.amount)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := failureOutgoingIndex(rt, result.FailureSource)\n\n\tif failIndex >= 0 {\n\t\tfor i := 0; i < failIndex && i < len(edges); i++ {\n\t\t\tr.recordPass(edges[i].edge, edges[i].amount)\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\tif failIndex >= 0 && failIndex < len(edges) {\n\t\tfailed := edges[failIndex]\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tr.recordFailure(failed.edge, failed.amount)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.policyBad[failed.key] = true\n\t\t\tr.penalty[failed.key] += 4\n\n\t\tdefault:\n\t\t\tr.policyBad[failed.key] = true\n\t\t\tr.penalty[failed.key] += 2\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, routeEdge := range edges {\n\t\tr.penalty[routeEdge.key] += 0.35\n\t}\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 16,
|
|
"parent": 6,
|
|
"score": 0.4425,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n\tcandidateLabelsPerNode = 6\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateKnowledgeKey struct {\n\tscope string\n\tedge candidateEdgeKey\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tscope string\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateKnowledgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateKnowledgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Evidence decays quickly enough to tolerate active background traffic.\n\tconst halfLifeMinutes = 14.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tb.conf = conf\n\n\t// Old bounds are discarded while the softer estimate continues to decay.\n\tif now.Sub(b.updatedAt) > 12*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tscope string, key candidateEdgeKey,\n\tforward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\n\tforwardKey := candidateKnowledgeKey{\n\t\tscope: scope,\n\t\tedge: key,\n\t}\n\tcandidateKnowledge.beliefs[forwardKey] = forward\n\n\treverseKey := candidateKnowledgeKey{\n\t\tscope: scope,\n\t\tedge: candidateReverseKey(key),\n\t}\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.86)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 93 / 100\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif estimate > b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(\n\t\tedge.scope, edge.key, b, edge.capacity,\n\t)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\testimate := amt / 20\n\tfloor := edge.capacity / 1000\n\tif floor < 1 {\n\t\tfloor = 1\n\t}\n\tif estimate > floor {\n\t\testimate = floor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(\n\t\tedge.scope, edge.key, b, edge.capacity,\n\t)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 93 / 100\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\tb.estimate -= amt\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(\n\t\tedge.scope, edge.key, b, edge.capacity,\n\t)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\tscope string\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tscope: fmt.Sprintf(\"%p\", view),\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tscope: r.scope,\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.003, math.Min(probability, 0.997))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The two modes represent a nearly depleted and a nearly full direction.\n\tlowMode := 0.485 * math.Exp(-ratio/0.022)\n\thighMode := 0.505 /\n\t\t(1 + math.Exp((ratio-0.925)/0.042))\n\n\treturn candidateClampProbability(\n\t\t0.004 + lowMode + highMode,\n\t)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.997\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.003\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.003 +\n\t\t\t0.994*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9998\n\t}\n\n\tif failedAt := r.sessionFailed[edge.key]; failedAt != 0 {\n\t\tif amt >= failedAt {\n\t\t\treturn 0\n\t\t}\n\t\tif float64(amt)/float64(failedAt) > 0.78 {\n\t\t\treturn 0.003\n\t\t}\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\tif conf == 0 {\n\t\treturn prior\n\t}\n\n\tlearned := candidateLearnedProbability(\n\t\tb, amt, edge.capacity,\n\t)\n\tprobability := conf*learned + (1-conf)*prior\n\n\tif failedAt := r.sessionFailed[edge.key]; failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.60:\n\t\t\tprobability *= 0.06\n\t\tcase ratio > 0.40:\n\t\t\tprobability *= 0.22\n\t\tcase ratio > 0.20:\n\t\t\tprobability *= 0.55\n\t\tdefault:\n\t\t\tprobability *= 0.85\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateSearchLabel struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n\tpath []*candidateEdge\n\talive bool\n\tindex int\n}\n\ntype candidateQueue []*candidateSearchLabel\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].amount != q[j].amount {\n\t\treturn q[i].amount < q[j].amount\n\t}\n\n\treturn q[i].hops < q[j].hops\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n\tq[i].index = i\n\tq[j].index = j\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\titem := value.(*candidateSearchLabel)\n\titem.index = len(*q)\n\t*q = append(*q, item)\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\tlast.index = -1\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc candidatePathContains(\n\tpath []*candidateEdge, node route.Vertex) bool {\n\n\tfor _, edge := range path {\n\t\tif edge.key.from == node || edge.key.to == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc candidateDominates(\n\ta, b *candidateSearchLabel) bool {\n\n\treturn a.score <= b.score+1e-12 &&\n\t\ta.amount <= b.amount &&\n\t\ta.hops <= b.hops\n}\n\nfunc candidateAddLabel(\n\tlabels map[route.Vertex][]*candidateSearchLabel,\n\tlabel *candidateSearchLabel) bool {\n\n\texisting := labels[label.node]\n\tfor _, other := range existing {\n\t\tif other.alive && candidateDominates(other, label) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tkept := existing[:0]\n\tfor _, other := range existing {\n\t\tif candidateDominates(label, other) {\n\t\t\tother.alive = false\n\t\t\tcontinue\n\t\t}\n\t\tif other.alive {\n\t\t\tkept = append(kept, other)\n\t\t}\n\t}\n\texisting = kept\n\n\tif len(existing) >= candidateLabelsPerNode {\n\t\tworst := 0\n\t\tworstValue := math.Inf(-1)\n\n\t\tfor i, other := range existing {\n\t\t\tvalue := other.score +\n\t\t\t\t0.06*float64(other.hops) +\n\t\t\t\t1e-12*float64(other.amount)\n\t\t\tif value > worstValue {\n\t\t\t\tworstValue = value\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tnewValue := label.score +\n\t\t\t0.06*float64(label.hops) +\n\t\t\t1e-12*float64(label.amount)\n\t\tif newValue >= worstValue {\n\t\t\treturn false\n\t\t}\n\n\t\texisting[worst].alive = false\n\t\texisting = append(\n\t\t\texisting[:worst], existing[worst+1:]...,\n\t\t)\n\t}\n\n\tlabel.alive = true\n\tlabels[label.node] = append(existing, label)\n\n\treturn true\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tlabels := make(map[route.Vertex][]*candidateSearchLabel)\n\tqueue := &candidateQueue{}\n\n\tstart := &candidateSearchLabel{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t\talive: true,\n\t}\n\tlabels[start.node] = []*candidateSearchLabel{start}\n\theap.Push(queue, start)\n\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateSearchLabel)\n\t\tif !item.alive {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\trt, err := r.buildRoute(deliver, item.path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\n\t\t\treturn rt, item.risk, nil\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif candidatePathContains(item.path, edge.key.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 6.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.035\n\t\t\tuseCost := 0.12 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\n\t\t\tpath := make([]*candidateEdge, len(item.path)+1)\n\t\t\tpath[0] = edge\n\t\t\tcopy(path[1:], item.path)\n\n\t\t\tlabel := &candidateSearchLabel{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: item.score + riskCost + feeCost +\n\t\t\t\t\thopCost + useCost +\n\t\t\t\t\tr.sessionPenalty[edge.key],\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t\tpath: path,\n\t\t\t}\n\n\t\t\tif candidateAddLabel(labels, label) {\n\t\t\t\theap.Push(queue, label)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, 0, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\tif len(path) > candidateMaxRouteHops {\n\t\treturn nil, errors.New(\"selected route is too long\")\n\t}\n\tif path[0].key.from != r.source ||\n\t\tpath[len(path)-1].key.to != r.spec.Target {\n\n\t\treturn nil, errors.New(\"selected route has invalid endpoints\")\n\t}\n\n\tfor i := 1; i < len(path); i++ {\n\t\tif path[i-1].key.to != path[i].key.from {\n\t\t\treturn nil, errors.New(\"selected route is disconnected\")\n\t\t}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 24)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\tlimit := partsLeft\n\tif limit > 18 {\n\t\tlimit = 18\n\t}\n\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, candidateCeilDiv(amt, partsLeft),\n\t)\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) addFailureSizedShards(\n\tamounts []lnwire.MilliSatoshi, remaining,\n\tminimum lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tfor _, failedAt := range r.sessionFailed {\n\t\tfor _, factor := range []int64{72, 55, 40, 25} {\n\t\t\tshard := lnwire.MilliSatoshi(\n\t\t\t\tint64(failedAt) * factor / 100,\n\t\t\t)\n\t\t\tif shard < minimum || shard > remaining {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tamounts = candidateAppendUnique(amounts, shard)\n\t\t}\n\t}\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\tshards = r.addFailureSizedShards(shards, amt, minimum)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log(\n\t\t\tmath.Max(float64(shard)/float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 5.0 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\tpartPenalty := 0.025 *\n\t\t\tfloat64(partsLeft-1)\n\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.72*progress - feePenalty - partPenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\n\t\tif probability >= 0.72 && shard >= amt*2/3 {\n\t\t\tbestRoute = rt\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.15\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.8, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tposition := float64(i+1) / float64(len(keys))\n\t\tpenalty := 0.8 + 0.8*position\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 7,\n\t\t)\n\t\tr.edgeUses[key] += 2\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tbalance := r.localBalances[first.chanID]\n\t\tif balance > amounts[0] {\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 30\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 17,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n\tcandidateLabelsPerNode = 6\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateKnowledgeKey struct {\n\tnetwork uintptr\n\tedge candidateEdgeKey\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateKnowledgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateKnowledgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateNetworkID(view routing.SimNetworkView) uintptr {\n\tvalue := reflect.ValueOf(view)\n\tswitch value.Kind() {\n\tcase reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func,\n\t\treflect.Chan, reflect.UnsafePointer:\n\n\t\tif !value.IsNil() {\n\t\t\treturn value.Pointer()\n\t\t}\n\t}\n\n\treturn 0\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.99))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Directional liquidity evidence decays because background payments can\n\t// move funds between observations.\n\tconst halfLifeMinutes = 18.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tb.conf = conf\n\n\t// Recent bounds remain strong. Older observations retain only their\n\t// confidence-weighted point estimate.\n\tif now.Sub(b.updatedAt) > 8*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tnetwork uintptr,\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{capacity: edge.capacity}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tnetwork uintptr, edgeKey candidateEdgeKey,\n\tforward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edgeKey,\n\t}\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseEdge := candidateReverseKey(edgeKey)\n\treverseKey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: reverseEdge,\n\t}\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.80)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tnetwork uintptr, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.93)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tnetwork uintptr, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\testimate := amt / 24\n\tfloor := edge.capacity / 1000\n\tif floor < 1 {\n\t\tfloor = 1\n\t}\n\tif estimate > floor {\n\t\testimate = floor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tnetwork uintptr, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\tb.estimate -= amt\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tnetworkID uintptr\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tnetworkID: candidateNetworkID(view),\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.005, math.Min(probability, 0.995))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode models nearly empty directions while the high mode\n\t// models directions holding almost the entire channel balance.\n\tlowMode := 0.485 * math.Exp(-ratio/0.022)\n\thighMode := 0.505 /\n\t\t(1 + math.Exp((ratio-0.925)/0.042))\n\n\treturn candidateClampProbability(0.004 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK > 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail > 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tspan := math.Max(\n\t\t\tfloat64(b.upperFail-b.lowerOK), 1,\n\t\t)\n\t\tfraction := (float64(amt) - float64(b.lowerOK)) / span\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\tif failedAt == 0 {\n\t\t\treturn 0.9995\n\t\t}\n\n\t\tratio := float64(amt) / float64(failedAt)\n\t\treturn candidateClampProbability(\n\t\t\t0.9995 * math.Pow(math.Max(1-ratio, 0.01), 0.35),\n\t\t)\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(r.networkID, edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.70:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.45:\n\t\t\tprobability *= 0.14\n\t\tcase ratio > 0.22:\n\t\t\tprobability *= 0.48\n\t\tdefault:\n\t\t\tprobability *= 0.82\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateLabel struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n\tpath []*candidateEdge\n\tactive bool\n}\n\ntype candidateQueue []*candidateLabel\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateLabel))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc candidatePathContains(\n\tpath []*candidateEdge, vertex route.Vertex) bool {\n\n\tfor _, edge := range path {\n\t\tif edge.key.from == vertex || edge.key.to == vertex {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc candidateDominates(a, b *candidateLabel) bool {\n\treturn a.active &&\n\t\ta.score <= b.score+1e-12 &&\n\t\ta.amount <= b.amount &&\n\t\ta.hops <= b.hops\n}\n\nfunc candidateInsertLabel(\n\tlabels map[route.Vertex][]*candidateLabel,\n\tlabel *candidateLabel) bool {\n\n\tcurrent := labels[label.node]\n\tfor _, existing := range current {\n\t\tif candidateDominates(existing, label) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tkept := current[:0]\n\tfor _, existing := range current {\n\t\tif candidateDominates(label, existing) {\n\t\t\texisting.active = false\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, existing)\n\t}\n\tkept = append(kept, label)\n\n\tif len(kept) > candidateLabelsPerNode {\n\t\tworst := 0\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tif kept[i].score > kept[worst].score ||\n\t\t\t\t(kept[i].score == kept[worst].score &&\n\t\t\t\t\tkept[i].amount > kept[worst].amount) {\n\n\t\t\t\tworst = i\n\t\t\t}\n\t\t}\n\n\t\tif kept[worst] == label {\n\t\t\treturn false\n\t\t}\n\n\t\tkept[worst].active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\t}\n\n\tlabel.active = true\n\tlabels[label.node] = kept\n\n\treturn true\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tstart := &candidateLabel{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t\tactive: true,\n\t}\n\tlabels := map[route.Vertex][]*candidateLabel{\n\t\tr.spec.Target: {start},\n\t}\n\tqueue := &candidateQueue{start}\n\theap.Init(queue)\n\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*candidateLabel)\n\t\tif !item.active {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\trt, err := r.buildRoute(deliver, item.path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\n\t\t\treturn rt, item.risk, nil\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif candidatePathContains(item.path, edge.key.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tsending := item.amount\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 7.0 * float64(fee) / feeScale\n\t\t\thopCost := 0.14\n\t\t\tif item.hops >= 10 {\n\t\t\t\thopCost += 0.035 * float64(item.hops-9)\n\t\t\t}\n\t\t\tuseCost := 0.075 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\n\t\t\tpath := make([]*candidateEdge, len(item.path)+1)\n\t\t\tpath[0] = edge\n\t\t\tcopy(path[1:], item.path)\n\n\t\t\tnext := &candidateLabel{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: item.score + riskCost + feeCost +\n\t\t\t\t\thopCost + useCost +\n\t\t\t\t\tr.sessionPenalty[edge.key],\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t\tpath: path,\n\t\t\t}\n\n\t\t\tif candidateInsertLabel(labels, next) {\n\t\t\t\theap.Push(queue, next)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, 0, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\tif len(path) > candidateMaxRouteHops {\n\t\treturn nil, errors.New(\"selected route is too long\")\n\t}\n\n\tnode := r.source\n\tfor _, edge := range path {\n\t\tif edge.key.from != node {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tnode = edge.key.to\n\t}\n\tif node != r.spec.Target {\n\t\treturn nil, errors.New(\"selected route misses target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] + outgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tforward := deliver\n\t\texpiry := uint32(candidateFinalCltvDelta)\n\t\tif i < last {\n\t\t\tforward = amounts[i+1]\n\t\t\texpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: forward,\n\t\t\tOutgoingTimeLock: expiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts := make([]lnwire.MilliSatoshi, 0, 24)\n\n\t// Reliability-first candidates come first, while every candidate is large\n\t// enough to leave the payment completable within the remaining part slots.\n\tamounts = candidateAppendUnique(amounts, minimum)\n\n\tfactors := []float64{\n\t\t1.18, 1.40, 1.70, 2.0, 2.5, 3.0, 4.0, 6.0, 8.0,\n\t}\n\tfor _, factor := range factors {\n\t\tcandidate := lnwire.MilliSatoshi(\n\t\t\tmath.Ceil(float64(minimum) * factor),\n\t\t)\n\t\tif candidate < amt {\n\t\t\tamounts = candidateAppendUnique(amounts, candidate)\n\t\t}\n\t}\n\n\tlimit := partsLeft\n\tif limit > 16 {\n\t\tlimit = 16\n\t}\n\tfor divisor := limit; divisor >= 2; divisor-- {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, divisor),\n\t\t)\n\t}\n\n\treturn candidateAppendUnique(amounts, amt)\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log(\n\t\t\tmath.Max(float64(shard)/float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 5.0 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\tlongPenalty := 0.018 *\n\t\t\tmath.Max(float64(len(rt.Hops)-8), 0)\n\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.24*progress - feePenalty - longPenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\n\t\tif probability >= 0.72 && shard >= amt/2 {\n\t\t\tbestRoute = rt\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.18\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.65, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.12\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.65\n\t\tif i >= len(keys)/2 {\n\t\t\tpenalty = 0.85\n\t\t}\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 5,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tr.networkID, edge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\t\t// A local failure is stronger than the initial balance snapshot.\n\t\t\tif edge.key.from == r.source {\n\t\t\t\tupper := amounts[failIndex] - 1\n\t\t\t\tif upper < 0 {\n\t\t\t\t\tupper = 0\n\t\t\t\t}\n\t\t\t\tif r.localBalances[key.chanID] > upper {\n\t\t\t\t\tr.localBalances[key.chanID] = upper\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown-source failures drive path exploration without contaminating\n\t// persistent channel liquidity observations.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 18,
|
|
"parent": 6,
|
|
"score": 0.3058,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\n\tconfidence float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\n\tcase amt > capacity:\n\t\treturn capacity\n\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.confidence = math.Max(0, math.Min(b.confidence, 0.995))\n\n\treturn b\n}\n\nfunc candidateEvidenceWeight(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.confidence <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Recent attempt feedback should dominate within a payment, while\n\t// evidence from earlier payments becomes a soft hint as traffic moves.\n\tconst halfLifeMinutes = 12.0\n\n\tweight := b.confidence *\n\t\tmath.Exp(-math.Ln2*age/halfLifeMinutes)\n\n\tif weight < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn weight\n}\n\nfunc candidatePrepareBelief(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tweight := candidateEvidenceWeight(b, now)\n\tif weight == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tb.confidence = weight\n\n\t// Old bounds are not physical bounds in a moving network. Preserve the\n\t// estimate, but let the prior and fresh observations override them.\n\tage := now.Sub(b.updatedAt)\n\tif age > 8*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareBelief(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.confidence = math.Max(\n\t\treverse.confidence, forward.confidence*0.82,\n\t)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareBelief(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif estimate > b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.confidence = math.Max(b.confidence, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareBelief(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A directional miss is strong evidence for the depleted mode, but\n\t// retaining a small amount permits useful lower-amount retries.\n\testimate := amt / 12\n\tdepletedMode := edge.capacity / 400\n\tif depletedMode < 1 {\n\t\tdepletedMode = 1\n\t}\n\tif estimate > depletedMode {\n\t\testimate = depletedMode\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.confidence = math.Max(b.confidence, 0.99)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareBelief(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\testimate := b.estimate\n\tif estimate < amt {\n\t\testimate = edge.capacity * 9 / 10\n\t\tif estimate < amt {\n\t\t\testimate = amt\n\t\t}\n\t}\n\n\tb.estimate = estimate - amt\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.confidence = math.Max(b.confidence, 0.96)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.002, math.Min(probability, 0.998))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low-liquidity mode gives tiny HTLCs a material chance of passing.\n\t// The high-liquidity mode remains nearly flat until its capacity cliff.\n\tlowMode := 0.47 * math.Exp(-ratio/0.018)\n\thighMode := 0.515 /\n\t\t(1 + math.Exp((ratio-0.91)/0.038))\n\n\treturn candidateClampProbability(0.003 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.998\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.002\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.022, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.002 + 0.996*\n\t\t\tmath.Pow(1-fraction, 2.8)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) > 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9998\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconfidence := candidateEvidenceWeight(b, r.view.Now())\n\n\tprobability := prior\n\tif confidence > 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = confidence*learned +\n\t\t\t(1-confidence)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\n\t\tswitch {\n\t\tcase ratio > 0.72:\n\t\t\tprobability *= 0.025\n\n\t\tcase ratio > 0.50:\n\t\t\tprobability *= 0.10\n\n\t\tcase ratio > 0.30:\n\t\t\tprobability *= 0.32\n\n\t\tcase ratio > 0.12:\n\t\t\tprobability *= 0.72\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 2_000_000)\n\n\tfor queue.Len() > 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tsending := item.amount\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\tif sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 4.0 * float64(fee) / feeScale\n\t\t\thopCost := 0.022\n\n\t\t\tuses := float64(r.edgeUses[edge.key])\n\t\t\tuseCost := 0.0\n\t\t\tif uses > 0 {\n\t\t\t\t// Reusing an edge is reasonable after evidence of success,\n\t\t\t\t// but repeated unproductive routes must quickly give way to\n\t\t\t\t// alternatives.\n\t\t\t\tuseCost = 0.075 * math.Min(uses, 12)\n\t\t\t\tif r.sessionLower[edge.key] >= item.amount {\n\t\t\t\t\tuseCost *= 0.12\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcapacityRatio := float64(item.amount) /\n\t\t\t\tmath.Max(float64(edge.capacity), 1)\n\t\t\tcapacityCost := 0.025 * capacityRatio\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + capacityCost +\n\t\t\t\tr.sessionPenalty[edge.key]\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts := make([]lnwire.MilliSatoshi, 0, 18)\n\n\t// Start with amounts that preserve enough part slots to complete the\n\t// payment. Dense coverage near the minimum is valuable because depleted\n\t// bimodal channels often still pass a much smaller retry.\n\tmultipliers := []float64{\n\t\t1.0, 1.15, 1.35, 1.6, 2.0, 2.5, 3.2, 4.0,\n\t}\n\n\tfor _, multiplier := range multipliers {\n\t\tshard := lnwire.MilliSatoshi(\n\t\t\tmath.Ceil(float64(minimum) * multiplier),\n\t\t)\n\t\tif shard > amt {\n\t\t\tshard = amt\n\t\t}\n\t\tamounts = candidateAppendUnique(amounts, shard)\n\t}\n\n\tfor parts := partsLeft; parts >= 2; parts-- {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t\tif parts == 2 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tfee := rt.TotalAmount - shard\n\n\t\tprogress := math.Log(\n\t\t\t1 + float64(shard)/math.Max(float64(minimum), 1),\n\t\t)\n\t\tfeePenalty := 3.0 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\n\t\t// Success probability dominates. Progress is still rewarded enough\n\t\t// to avoid needless dust-like parts when two candidates are similarly\n\t\t// reliable.\n\t\tutility := math.Log(math.Max(probability, 1e-15)) +\n\t\t\t0.34*progress - feePenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.08\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.8, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.05\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tposition := float64(i+1) / float64(len(keys))\n\t\tpenalty := 0.9 + 0.8*position\n\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 7,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tbalance := r.localBalances[first.chanID]\n\t\tif balance > amounts[0] {\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 30\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unattributed failure should cause broad route exploration without\n\t// turning uncertain feedback into persistent liquidity evidence.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 19,
|
|
"parent": 6,
|
|
"score": 0.5128,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 64\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\tproportional := (amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n\n\treturn e.baseFeeMsat + proportional\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tif amt < 0 {\n\t\treturn 0\n\t}\n\tif amt > capacity {\n\t\treturn capacity\n\t}\n\n\treturn amt\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tif b.conf < 0 {\n\t\tb.conf = 0\n\t}\n\tif b.conf > 0.99 {\n\t\tb.conf = 0.99\n\t}\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Directional liquidity can move substantially between payments.\n\tconst halfLifeMinutes = 10.0\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tb.conf = conf\n\n\t// Old bounds are useful as estimates, but no longer as hard facts.\n\tif now.Sub(b.updatedAt) > 6*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.85)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\t// Under a bimodal prior, observing a pass strongly favors the high mode.\n\thighEstimate := edge.capacity * 9 / 10\n\tif highEstimate < amt {\n\t\thighEstimate = amt\n\t}\n\tif b.estimate < highEstimate {\n\t\tb.estimate = highEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.92)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity failure strongly favors the depleted mode.\n\tlowEstimate := amt / 20\n\tcapFloor := edge.capacity / 700\n\tif capFloor < 1 {\n\t\tcapFloor = 1\n\t}\n\tif lowEstimate > capFloor {\n\t\tlowEstimate = capFloor\n\t}\n\tif lowEstimate < b.lowerOK {\n\t\tlowEstimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || lowEstimate < b.estimate {\n\t\tb.estimate = lowEstimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.98)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\testimate := b.estimate\n\tif estimate < amt {\n\t\testimate = edge.capacity * 9 / 10\n\t\tif estimate < amt {\n\t\t\testimate = amt\n\t\t}\n\t}\n\n\tb.estimate = estimate - amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailCount map[candidateEdgeKey]uint8\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailCount: make(map[candidateEdgeKey]uint8),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\tif probability < 0.003 {\n\t\treturn 0.003\n\t}\n\tif probability > 0.997 {\n\t\treturn 0.997\n\t}\n\n\treturn probability\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode supplies near-certain probability for tiny HTLCs. The\n\t// high mode represents channels whose balance sits near full capacity.\n\tlowMode := 0.49 * math.Exp(-ratio/0.027)\n\thighMode := 0.50 /\n\t\t(1 + math.Exp((ratio-0.91)/0.043))\n\n\treturn candidateClampProbability(0.003 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.997\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.003\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.03, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(1, fraction))\n\n\t\tbounded := 0.003 + 0.994*math.Pow(1-fraction, 2.7)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tnow := r.view.Now()\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf != 0 && now.Sub(b.updatedAt) > 6*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt := r.sessionFailed[edge.key]; failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tcount := float64(r.sessionFailCount[edge.key])\n\n\t\tswitch {\n\t\tcase ratio >= 1:\n\t\t\tprobability *= 0.006 / math.Max(count, 1)\n\t\tcase ratio > 0.72:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.48:\n\t\t\tprobability *= 0.12\n\t\tcase ratio > 0.25:\n\t\t\tprobability *= 0.42\n\t\tcase ratio > 0.10:\n\t\t\tprobability *= 0.72\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 6.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.045\n\t\t\tuseCost := 0.11 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateScaledAmount(\n\tamt lnwire.MilliSatoshi, numerator,\n\tdenominator int64) lnwire.MilliSatoshi {\n\n\tif denominator <= 0 {\n\t\treturn 0\n\t}\n\n\tvalue := int64(amt)\n\tif value > math.MaxInt64/numerator {\n\t\treturn amt\n\t}\n\n\treturn lnwire.MilliSatoshi(\n\t\t(value*numerator + denominator - 1) / denominator,\n\t)\n}\n\nfunc (r *candidateRouter) shardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 32)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\tlimit := partsLeft\n\tif limit > 20 {\n\t\tlimit = 20\n\t}\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\t// Geometric probes make lower-amount retries available even when MaxParts\n\t// is small. They are selected only when their probability compensates for\n\t// the risk of consuming a part with little progress.\n\tfor _, fraction := range [][2]int64{\n\t\t{4, 5}, {3, 4}, {2, 3}, {3, 5}, {1, 2},\n\t\t{2, 5}, {1, 3}, {1, 4}, {1, 6}, {1, 8},\n\t} {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts,\n\t\t\tcandidateScaledAmount(\n\t\t\t\tamt, fraction[0], fraction[1],\n\t\t\t),\n\t\t)\n\t}\n\n\t// Probe directly below observed liquidity cliffs.\n\tfor _, failedAt := range r.sessionFailed {\n\t\tif failedAt <= 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, fraction := range [][2]int64{\n\t\t\t{3, 4}, {1, 2}, {1, 3}, {1, 5},\n\t\t} {\n\t\t\tprobe := candidateScaledAmount(\n\t\t\t\tfailedAt, fraction[0], fraction[1],\n\t\t\t)\n\t\t\tif probe <= amt {\n\t\t\t\tamounts = candidateAppendUnique(\n\t\t\t\t\tamounts, probe,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := r.shardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log1p(\n\t\t\tfloat64(shard) / math.Max(float64(minimum), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.72*progress - feePenalty\n\n\t\tif shard < minimum {\n\t\t\tshortfall := 1 - float64(shard)/float64(minimum)\n\t\t\tutility -= 0.55 * shortfall\n\t\t}\n\n\t\t// Prefer a route that can make meaningful progress when otherwise\n\t\t// equivalent, but never at the cost of a large reliability loss.\n\t\tif shard >= amt/2 {\n\t\t\tutility += 0.06\n\t\t}\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t\tdelete(r.sessionFailCount, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.15\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t\tr.sessionFailCount[key] = 1\n\t} else if r.sessionFailCount[key] < math.MaxUint8 {\n\t\tr.sessionFailCount[key]++\n\t}\n\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tincrement := 1.15 +\n\t\t0.25*math.Min(float64(r.sessionFailCount[key]), 4)\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+increment, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t\tdelete(r.sessionFailCount, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.75\n\t\tif i >= len(keys)/2 {\n\t\t\tpenalty = 0.95\n\t\t}\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 5,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown failures trigger path exploration without corrupting persistent\n\t// channel-liquidity observations.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 20,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateKnowledgeKey struct {\n\tscope uint64\n\tedge candidateEdgeKey\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tscope uint64\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateKnowledgeKey]candidateLiquidityBelief\n\tviews map[uint64]routing.SimNetworkView\n}{\n\tbeliefs: make(map[candidateKnowledgeKey]candidateLiquidityBelief),\n\tviews: make(map[uint64]routing.SimNetworkView),\n}\n\nfunc candidateViewScope(view routing.SimNetworkView) uint64 {\n\tvalue := reflect.ValueOf(view)\n\tswitch value.Kind() {\n\tcase reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer,\n\t\treflect.Slice, reflect.UnsafePointer:\n\n\t\tif pointer := value.Pointer(); pointer != 0 {\n\t\t\treturn uint64(pointer)\n\t\t}\n\t}\n\n\treturn uint64(uintptr(reflect.ValueOf(&view).Pointer()))\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\tconst halfLifeMinutes = 18.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tb.conf = conf\n\n\tage := now.Sub(b.updatedAt)\n\tswitch {\n\tcase age > 12*time.Minute:\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\n\tcase age > 5*time.Minute:\n\t\t// Old bounds remain useful as point-estimate evidence, but they are\n\t\t// no longer safe hard constraints in a moving network.\n\t\tif b.lowerOK != 0 {\n\t\t\tb.estimate = (b.estimate + b.lowerOK) / 2\n\t\t}\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(edge *candidateEdge) candidateLiquidityBelief {\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{capacity: edge.capacity}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tedge *candidateEdge, forward candidateLiquidityBelief) {\n\n\tforward = candidateNormalizeBelief(forward, edge.capacity)\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: candidateReverseKey(edge.key),\n\t}\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tedge.capacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.82)\n\treverse.estimate = edge.capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = edge.capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = edge.capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, edge.capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(edge, b)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\testimate := amt / 20\n\tfloor := edge.capacity / 1000\n\tif floor < 1 {\n\t\tfloor = 1\n\t}\n\tif estimate > floor {\n\t\testimate = floor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(edge, b)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateKnowledgeKey{\n\t\tscope: edge.scope,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\tb.estimate -= amt\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge, b)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\tscope uint64\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tfailedUses map[candidateEdgeKey]uint32\n\trouteFailures map[uint64]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tscope := candidateViewScope(view)\n\tcandidateKnowledge.Lock()\n\tcandidateKnowledge.views[scope] = view\n\tcandidateKnowledge.Unlock()\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tscope: scope,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tfailedUses: make(map[candidateEdgeKey]uint32),\n\t\trouteFailures: make(map[uint64]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tscope: scope,\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\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.003, math.Min(probability, 0.997))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// A bimodal balance has roughly half its mass near either endpoint.\n\t// Tiny HTLCs can pass in either mode, while medium HTLCs principally\n\t// depend on being on the funded side.\n\tlowMode := 0.49 * math.Exp(-ratio/0.018)\n\thighMode := 0.495 /\n\t\t(1 + math.Exp((ratio-0.91)/0.04))\n\n\treturn candidateClampProbability(0.003 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.997\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.003\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.003 + 0.994*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.75*bounded + 0.25*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9998\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\tif r.sessionLower[edge.key] >= amt {\n\t\treturn 0.999\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\t// A failed amount is strong evidence under a bimodal prior, but smaller\n\t// probes remain useful instead of permanently blacklisting the edge.\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.72:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.48:\n\t\t\tprobability *= 0.12\n\t\tcase ratio > 0.24:\n\t\t\tprobability *= 0.42\n\t\tcase ratio > 0.10:\n\t\t\tprobability *= 0.75\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi,\n\textraPenalty map[candidateEdgeKey]float64) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 500_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 7 * float64(fee) / feeScale\n\t\t\thopCost := 0.14\n\t\t\tfailureCost := 0.12 * math.Min(\n\t\t\t\tfloat64(r.failedUses[edge.key]), 10,\n\t\t\t)\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + failureCost +\n\t\t\t\tr.sessionPenalty[edge.key] +\n\t\t\t\textraPenalty[edge.key]\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 14)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\t// Prefer a compact geometric ladder. It covers large progress-making\n\t// shards and low-risk probes without running a full route search for\n\t// every possible part count.\n\tparts := []uint32{2, 3, 4, 6, 8, 10, 12, 16}\n\tfor _, count := range parts {\n\t\tif count <= partsLeft {\n\t\t\tamounts = candidateAppendUnique(\n\t\t\t\tamounts, candidateCeilDiv(amt, count),\n\t\t\t)\n\t\t}\n\t}\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, candidateCeilDiv(amt, partsLeft),\n\t)\n\n\treturn amounts\n}\n\nfunc candidateRouteHash(rt *route.Route) uint64 {\n\tconst (\n\t\toffset = uint64(1469598103934665603)\n\t\tprime = uint64(1099511628211)\n\t)\n\n\thash := offset\n\tfor _, hop := range rt.Hops {\n\t\tvalue := hop.ChannelID\n\t\tfor i := 0; i < 8; i++ {\n\t\t\thash ^= value & 0xff\n\t\t\thash *= prime\n\t\t\tvalue >>= 8\n\t\t}\n\t\tfor _, value := range hop.PubKeyBytes {\n\t\t\thash ^= uint64(value)\n\t\t\thash *= prime\n\t\t}\n\t}\n\n\treturn hash\n}\n\nfunc candidatePenalizePath(\n\trt *route.Route, penalties map[candidateEdgeKey]float64,\n\tamount float64) {\n\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tpenalties[key] += amount\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\textra := make(map[candidateEdgeKey]float64)\n\n\t\t// Generate several genuinely different path candidates for each\n\t\t// shard. This avoids repeatedly returning the same route after an\n\t\t// unattributed failure.\n\t\tfor alternative := 0; alternative < 3; alternative++ {\n\t\t\trt, logRisk, err := r.findRoute(shard, extra)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tprobability := math.Exp(-logRisk)\n\t\t\tfee := rt.TotalAmount - shard\n\t\t\tprogress := math.Log1p(\n\t\t\t\tfloat64(shard) /\n\t\t\t\t\tmath.Max(float64(minimum), 1),\n\t\t\t)\n\t\t\tfeePenalty := 5 * float64(fee) /\n\t\t\t\tmath.Max(float64(shard), 1)\n\t\t\thopPenalty := 0.015 * float64(len(rt.Hops))\n\n\t\t\thash := candidateRouteHash(rt)\n\t\t\thistoryPenalty := 1.35 *\n\t\t\t\tfloat64(r.routeFailures[hash])\n\n\t\t\tutility := math.Log(math.Max(probability, 1e-15)) +\n\t\t\t\t0.42*progress - feePenalty - hopPenalty -\n\t\t\t\thistoryPenalty\n\n\t\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\t\tbestRoute = rt\n\t\t\t\tbestUtility = utility\n\t\t\t}\n\n\t\t\tcandidatePenalizePath(rt, extra, 0.85)\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.15\n\tif r.failedUses[key] != 0 {\n\t\tr.failedUses[key]--\n\t}\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.failedUses[key]++\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\tif r.failedUses[key] != 0 {\n\t\tr.failedUses[key]--\n\t}\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeRoute(\n\tkeys []candidateEdgeKey, severe bool) {\n\n\tbase := 0.75\n\tif severe {\n\t\tbase = 1.5\n\t}\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tposition := float64(i+1) / float64(len(keys))\n\t\tpenalty := base * (0.75 + 0.5*position)\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 8,\n\t\t)\n\t\tr.failedUses[key]++\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\thash := candidateRouteHash(rt)\n\n\tif result.Failure == nil {\n\t\tdelete(r.routeFailures, hash)\n\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tbalance := r.localBalances[first.chanID]\n\t\tif balance > amounts[0] {\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tr.routeFailures[hash]++\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\tr.penalizeRoute(keys, false)\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\t\t// The exact path should still be avoided while smaller probes\n\t\t\t// of the identified channel remain available.\n\t\t\tr.sessionPenalty[key] = math.Min(\n\t\t\t\tr.sessionPenalty[key]+0.35, 2,\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 30\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 15\n\t\t\tr.penalizeRoute(keys, false)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// An unknown failure source must not poison a persistent liquidity\n\t// belief. Strongly diversify the session path instead, including exact\n\t// route history so repeated unattributed failures cannot consume the\n\t// entire attempt budget on one route.\n\tr.penalizeRoute(keys, true)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 21,
|
|
"parent": 6,
|
|
"score": -0.0611,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Evidence remains useful in static scenarios, but becomes a soft hint\n\t// after background traffic has had time to move liquidity.\n\tconst halfLifeMinutes = 18.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tb.conf = conf\n\n\t// Old bounds must not remain absolute in a moving network. The estimate is\n\t// retained and blended with the prior through the decayed confidence.\n\tif now.Sub(b.updatedAt) > 12*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(edge *candidateEdge) candidateLiquidityBelief {\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{capacity: edge.capacity}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey], capacity,\n\t\tforward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.86)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key], edge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\t// Under the bimodal model, a successful probe is evidence that this is\n\t// the funded direction, not merely that exactly the probe amount exists.\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key], edge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity failure strongly selects the depleted mode. Preserve any\n\t// proven lower bound, but otherwise place the estimate near zero.\n\testimate := edge.capacity / 1000\n\tprobeEstimate := amt / 24\n\tif estimate < 1 {\n\t\testimate = 1\n\t}\n\tif probeEstimate < estimate {\n\t\testimate = probeEstimate\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key], edge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\tb.estimate -= amt\n\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateFailureEvidence struct {\n\tamount lnwire.MilliSatoshi\n\tcount uint32\n\tat time.Time\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]candidateFailureEvidence\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]candidateFailureEvidence),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\t// InPolicy is the other node's policy for the directed\n\t\t\t\t// edge from OtherNode into node.\n\t\t\t\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.002, math.Min(probability, 0.997))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode covers the small chance that even the depleted direction\n\t// can carry a tiny HTLC. The high mode models a funded direction with a\n\t// steep cliff near the channel capacity.\n\tlowMode := 0.47 * math.Exp(-ratio/0.018)\n\thighMode := 0.515 /\n\t\t(1 + math.Exp((ratio-0.90)/0.038))\n\n\treturn candidateClampProbability(0.003 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\tprobability = math.Max(probability, 0.997)\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tprobability = math.Min(probability, 0.002)\n\t}\n\tif b.upperFail != 0 && amt > b.lowerOK && amt < b.upperFail {\n\t\tspan := float64(b.upperFail - b.lowerOK)\n\t\tfraction := float64(amt-b.lowerOK) / math.Max(span, 1)\n\t\tbounded := 0.002 + 0.995*math.Pow(1-fraction, 2.7)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9998\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.999\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tfailure, failed := r.sessionFailed[edge.key]\n\tif failed && failure.amount > 0 {\n\t\tage := r.view.Now().Sub(failure.at).Minutes()\n\t\tif age < 0 {\n\t\t\tage = 0\n\t\t}\n\n\t\t// Session failures are strong but deliberately not absolute. This\n\t\t// avoids premature \"no route\" termination when a cut edge is the\n\t\t// only path and background traffic has since rebalanced it.\n\t\tfreshness := math.Exp(-math.Ln2 * age / 4.0)\n\t\tratio := float64(amt) / float64(failure.amount)\n\n\t\tvar multiplier float64\n\t\tswitch {\n\t\tcase ratio >= 1:\n\t\t\tmultiplier = 0.008\n\t\tcase ratio >= 0.72:\n\t\t\tmultiplier = 0.025\n\t\tcase ratio >= 0.45:\n\t\t\tmultiplier = 0.16\n\t\tcase ratio >= 0.20:\n\t\t\tmultiplier = 0.52\n\t\tdefault:\n\t\t\tmultiplier = 0.84\n\t\t}\n\n\t\tif failure.count > 1 {\n\t\t\tmultiplier = math.Pow(\n\t\t\t\tmultiplier,\n\t\t\t\tmath.Min(float64(failure.count), 3),\n\t\t\t)\n\t\t}\n\n\t\teffective := 1 - freshness*(1-multiplier)\n\t\tprobability *= effective\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\t\t\tif sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 5.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.14\n\t\t\tuseCost := 0.055 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\t// Prefer channels with substantial capacity headroom even when\n\t\t\t// their raw bimodal probability is similar.\n\t\t\theadroom := float64(edge.capacity) /\n\t\t\t\tmath.Max(float64(item.amount), 1)\n\t\t\theadroomCost := 0.0\n\t\t\tif headroom < 2 {\n\t\t\t\theadroomCost = 0.18 * (2 - headroom)\n\t\t\t}\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty + headroomCost\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts := make([]lnwire.MilliSatoshi, 0, 18)\n\n\t// Evaluate the completion-safe minimum first, then progressively larger\n\t// shards. Every candidate leaves enough part slots to finish the payment.\n\tamounts = candidateAppendUnique(amounts, minimum)\n\tfor _, numerator := range []int64{\n\t\t5, 6, 8, 10, 12, 16, 24, 32,\n\t} {\n\t\tshard := minimum * lnwire.MilliSatoshi(numerator) / 4\n\t\tif shard > amt {\n\t\t\tshard = amt\n\t\t}\n\t\tamounts = candidateAppendUnique(amounts, shard)\n\t}\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tfee := rt.TotalAmount - shard\n\n\t\t// Success dominates. Larger shards receive only a modest progress\n\t\t// reward, preventing them from displacing much safer MPP shards.\n\t\tprogress := math.Log(\n\t\t\tmath.Max(float64(shard)/float64(minimum), 1),\n\t\t)\n\t\tfeePenalty := 4.5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\thopPenalty := 0.015 * float64(len(rt.Hops))\n\n\t\tutility := math.Log(math.Max(probability, 1e-15)) +\n\t\t\t0.20*progress - feePenalty - hopPenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\t// A node reports failure of its outgoing channel. If source is hop i,\n\t// the failed directed edge is therefore i+1.\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\n\tif failure, ok := r.sessionFailed[key]; ok &&\n\t\tamt >= failure.amount {\n\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.15\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi,\n\tnow time.Time) {\n\n\tfailure := r.sessionFailed[key]\n\tif failure.amount == 0 || amt < failure.amount {\n\t\tfailure.amount = amt\n\t\tfailure.count = 1\n\t} else {\n\t\tfailure.count++\n\t}\n\tfailure.at = now\n\tr.sessionFailed[key] = failure\n\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\t// Penalize the observed edge enough to explore alternatives, while its\n\t// nonzero probability still permits a later or lower-amount retry.\n\tincrement := 1.7 + 0.35*math.Min(float64(failure.count), 4)\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+increment, 9,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failure, ok := r.sessionFailed[key]; ok {\n\t\tif failure.amount > amt {\n\t\t\tfailure.amount -= amt\n\t\t\tr.sessionFailed[key] = failure\n\t\t} else {\n\t\t\tdelete(r.sessionFailed, key)\n\t\t}\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n\tr.sessionPenalty[reverse] *= 0.25\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tposition := float64(i+1) / float64(len(keys))\n\t\tpenalty := 0.35 + 0.55*position\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 5,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tbalance := r.localBalances[first.chanID]\n\t\tif balance > amounts[0] {\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex], now,\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\t// These indicate a deterministic mismatch between the route\n\t\t\t// and the advertised policy. Do not retry this direction.\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown-source failures cannot safely update a particular liquidity\n\t// belief. Spread a temporary penalty across the attempted route instead.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 22,
|
|
"parent": 6,
|
|
"score": 0.2742,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 30\n\tcandidateLabelsPerNode = 7\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(\n\tamt lnwire.MilliSatoshi) bool {\n\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(\n\tkey candidateEdgeKey) candidateEdgeKey {\n\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\n\tcase amt > capacity:\n\t\treturn capacity\n\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Evidence remains durable when the simulation clock does not move, while\n\t// background traffic progressively returns the belief toward its prior.\n\tconst halfLifeMinutes = 24.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tage := now.Sub(b.updatedAt)\n\tb.conf = conf\n\n\t// Old bounds are no longer treated as physical limits after enough\n\t// simulated time for background traffic to reverse the channel.\n\tif age > 16*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.84)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi,\n\tnow time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.93)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi,\n\tnow time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity failure under a bimodal prior moves the estimate close to\n\t// the depleted mode. Smaller retries remain possible through the smooth\n\t// probability model rather than a permanent blacklist.\n\testimate := amt / 20\n\tdepletedMode := edge.capacity / 600\n\tif depletedMode < 1 {\n\t\tdepletedMode = 1\n\t}\n\tif estimate > depletedMode {\n\t\testimate = depletedMode\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi,\n\tnow time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\n\tb.estimate -= amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(\n\tprobability float64) float64 {\n\n\treturn math.Max(0.005, math.Min(probability, 0.995))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode makes tiny probes likely to pass even on the depleted\n\t// side. The high mode represents channels whose funds sit almost wholly\n\t// in this direction and falls sharply near capacity.\n\tlowMode := 0.47 * math.Exp(-ratio/0.022)\n\thighMode := 0.505 /\n\t\t(1 + math.Exp((ratio-0.91)/0.042))\n\n\treturn candidateClampProbability(0.005 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.03, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.6)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge,\n\tamt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\tif !edge.usable(amt) {\n\t\treturn 0\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\tprobability := 0.9995\n\t\tif failedAt != 0 {\n\t\t\tratio := float64(amt) / float64(failedAt)\n\t\t\tprobability *= 0.08 +\n\t\t\t\t0.92*math.Pow(math.Max(1-ratio, 0), 1.4)\n\t\t}\n\n\t\treturn candidateClampProbability(probability)\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tretryWeight := 0.08 +\n\t\t\t0.92*math.Pow(math.Max(1-ratio, 0), 1.45)\n\t\tprobability *= retryWeight\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateSearchLabel struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n\tpath []*candidateEdge\n\tactive bool\n}\n\ntype candidateQueue []*candidateSearchLabel\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].amount != q[j].amount {\n\t\treturn q[i].amount < q[j].amount\n\t}\n\n\treturn q[i].hops < q[j].hops\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateSearchLabel))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc candidatePathContains(\n\tpath []*candidateEdge, node route.Vertex) bool {\n\n\tfor _, edge := range path {\n\t\tif edge.key.from == node || edge.key.to == node {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc candidateDominates(\n\tleft, right *candidateSearchLabel) bool {\n\n\treturn left.active &&\n\t\tleft.amount <= right.amount &&\n\t\tleft.score <= right.score+1e-12\n}\n\nfunc candidateLabelQuality(\n\tlabel *candidateSearchLabel,\n\tdeliver lnwire.MilliSatoshi) float64 {\n\n\tamountCost := 0.04 * float64(label.amount) /\n\t\tmath.Max(float64(deliver), 1)\n\n\treturn label.score + amountCost\n}\n\nfunc candidateInsertLabel(\n\tlabels map[route.Vertex][]*candidateSearchLabel,\n\tlabel *candidateSearchLabel,\n\tdeliver lnwire.MilliSatoshi) bool {\n\n\tcurrent := labels[label.node]\n\tfor _, existing := range current {\n\t\tif candidateDominates(existing, label) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tkept := current[:0]\n\tfor _, existing := range current {\n\t\tif candidateDominates(label, existing) {\n\t\t\texisting.active = false\n\t\t\tcontinue\n\t\t}\n\n\t\tkept = append(kept, existing)\n\t}\n\n\tlabel.active = true\n\tkept = append(kept, label)\n\n\tif len(kept) > candidateLabelsPerNode {\n\t\tworst := 0\n\t\tworstQuality := candidateLabelQuality(kept[0], deliver)\n\n\t\tfor i := 1; i < len(kept); i++ {\n\t\t\tquality := candidateLabelQuality(kept[i], deliver)\n\t\t\tif quality > worstQuality {\n\t\t\t\tworst = i\n\t\t\t\tworstQuality = quality\n\t\t\t}\n\t\t}\n\n\t\tremoved := kept[worst]\n\t\tremoved.active = false\n\t\tkept = append(kept[:worst], kept[worst+1:]...)\n\n\t\tif removed == label {\n\t\t\tlabels[label.node] = kept\n\t\t\treturn false\n\t\t}\n\t}\n\n\tlabels[label.node] = kept\n\treturn true\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tstart := &candidateSearchLabel{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t\tactive: true,\n\t}\n\n\tlabels := map[route.Vertex][]*candidateSearchLabel{\n\t\tr.spec.Target: {start},\n\t}\n\tqueue := &candidateQueue{start}\n\theap.Init(queue)\n\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateSearchLabel)\n\t\tif !item.active {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\trt, err := r.buildRoute(deliver, item.path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, err\n\t\t\t}\n\n\t\t\treturn rt, item.risk, nil\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif candidatePathContains(item.path, edge.key.from) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tsending := item.amount\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\t\t\tif !edge.usable(item.amount) || sending <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 5.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.13\n\t\t\tuseCost := 0.07 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\n\t\t\tpath := make([]*candidateEdge, len(item.path)+1)\n\t\t\tpath[0] = edge\n\t\t\tcopy(path[1:], item.path)\n\n\t\t\tnext := &candidateSearchLabel{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: item.score + riskCost + feeCost +\n\t\t\t\t\thopCost + useCost +\n\t\t\t\t\tr.sessionPenalty[edge.key],\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t\tpath: path,\n\t\t\t}\n\n\t\t\tif !candidateInsertLabel(labels, next, deliver) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\theap.Push(queue, next)\n\t\t}\n\t}\n\n\treturn nil, 0, errors.New(\"no route found\")\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tpath []*candidateEdge) (*route.Route, error) {\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\tif len(path) > candidateMaxRouteHops {\n\t\treturn nil, errors.New(\"selected route is too long\")\n\t}\n\n\tnode := r.source\n\tfor _, edge := range path {\n\t\tif edge.key.from != node {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\t\tnode = edge.key.to\n\t}\n\tif node != r.spec.Target {\n\t\treturn nil, errors.New(\"selected route misses target\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi,\n\tdivisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc (r *candidateRouter) shardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 28)\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\tlimit := partsLeft\n\tif limit > 20 {\n\t\tlimit = 20\n\t}\n\n\tfor parts := uint32(2); parts <= limit; parts++ {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts = candidateAppendUnique(amounts, minimum)\n\n\t// A slightly undersized probe can escape a learned liquidity cliff\n\t// without spending so many part slots that completion becomes unlikely.\n\tif partsLeft >= 3 {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, minimum*3/4,\n\t\t)\n\t}\n\n\t// Retry just below liquidity failures observed during this payment.\n\tfor _, failedAt := range r.sessionFailed {\n\t\tfor _, numerator := range []lnwire.MilliSatoshi{2, 1} {\n\t\t\tretry := failedAt * numerator / 3\n\t\t\tif retry > amt {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif retry < minimum/2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamounts = candidateAppendUnique(amounts, retry)\n\t\t}\n\t}\n\n\tsort.Slice(amounts, func(i, j int) bool {\n\t\treturn amounts[i] > amounts[j]\n\t})\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(\n\trt *route.Route) {\n\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) releaseRouteUse(\n\tkey candidateEdgeKey) {\n\n\tif r.edgeUses[key] > 0 {\n\t\tr.edgeUses[key]--\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tshards := r.shardAmounts(amt, partsLeft)\n\treference := candidateCeilDiv(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := math.Log(\n\t\t\tmath.Max(float64(shard), 1) /\n\t\t\t\tmath.Max(float64(reference), 1),\n\t\t)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 4.5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\n\t\t// Expected progress dominates fees, while route probability remains\n\t\t// the primary term. A small completion bonus avoids unnecessary MPP\n\t\t// fragmentation when the whole payment has a credible route.\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.82*progress - feePenalty\n\n\t\tif shard == amt {\n\t\t\tutility += 0.12\n\t\t}\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.16\n\tr.releaseRouteUse(key)\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey,\n\tamt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.65, 7,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\tr.releaseRouteUse(key)\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.55\n\t\tif i >= len(keys)/2 {\n\t\t\tpenalty = 0.75\n\t\t}\n\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 4.5,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tcandidateRecordPass(\n\t\t\t\t\tedge, amounts[i], now,\n\t\t\t\t)\n\t\t\t}\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tcandidateRecordFailure(\n\t\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t\t)\n\t\t\t}\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// A failure attributed to the target proves every traversed channel,\n\t// though it does not identify an outgoing channel to blacklist.\n\tif failIndex == len(keys) {\n\t\tfor _, key := range keys {\n\t\t\tr.releaseRouteUse(key)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown-source failures force route exploration without corrupting\n\t// persistent directional liquidity beliefs.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 23,
|
|
"parent": 6,
|
|
"score": 0.3232,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 56\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateBeliefKey struct {\n\tnetwork string\n\tedge candidateEdgeKey\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateBeliefKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateBeliefKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Evidence decays quickly enough to tolerate background traffic while\n\t// remaining useful for consecutive payments in a batch.\n\tconst halfLifeMinutes = 14.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{capacity: capacity}\n\t}\n\n\tb.conf = conf\n\n\t// Old bounds are treated as soft estimates because intervening payments\n\t// may have shifted liquidity in either direction.\n\tif now.Sub(b.updatedAt) > 7*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tnetwork string, edge *candidateEdge) candidateLiquidityBelief {\n\n\tkey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{capacity: edge.capacity}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tnetwork string, key candidateEdgeKey,\n\tforward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tforwardKey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: key,\n\t}\n\tcandidateKnowledge.beliefs[forwardKey] = forward\n\n\treverseEdge := candidateReverseKey(key)\n\treverseKey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: reverseEdge,\n\t}\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.84)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\testimate := edge.capacity * 9 / 10\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity miss is strong evidence for the depleted mode, but lower\n\t// amounts remain eligible for subsequent probing.\n\testimate := amt / 20\n\tfloor := edge.capacity / 1000\n\tif floor < 1 {\n\t\tfloor = 1\n\t}\n\tif estimate > floor {\n\t\testimate = floor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.99)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tnetwork string, edge *candidateEdge,\n\tamt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tkey := candidateBeliefKey{\n\t\tnetwork: network,\n\t\tedge: edge.key,\n\t}\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[key], edge.capacity, now,\n\t)\n\n\tif b.estimate < amt {\n\t\tb.estimate = edge.capacity * 9 / 10\n\t\tif b.estimate < amt {\n\t\t\tb.estimate = amt\n\t\t}\n\t}\n\n\tb.estimate -= amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.96)\n\tb.updatedAt = now\n\tcandidateStorePair(network, edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tnetworkID string\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tnetworkID: fmt.Sprintf(\"%T:%p\", view, view),\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\treturn math.Max(0.003, math.Min(probability, 0.997))\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The first term models the nearly empty mode. The second models the\n\t// well-funded mode and retains a sharp cliff near channel capacity.\n\tlowMode := 0.475 * math.Exp(-ratio/0.022)\n\thighMode := 0.515 / (1 + math.Exp((ratio-0.90)/0.042))\n\n\treturn candidateClampProbability(0.003 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.997\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.003\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.003 + 0.994*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.72*bounded + 0.28*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(r.networkID, edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\t\tswitch {\n\t\tcase ratio > 0.70:\n\t\t\tprobability *= 0.025\n\t\tcase ratio > 0.50:\n\t\t\tprobability *= 0.12\n\t\tcase ratio > 0.25:\n\t\t\tprobability *= 0.48\n\t\tdefault:\n\t\t\tprobability *= 0.88\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 2_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 5.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.075\n\t\t\tuseCost := 0.13 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tamounts[i] = amounts[i+1] +\n\t\t\toutgoing.fee(amounts[i+1])\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn (amt + d - 1) / d\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tamounts := make([]lnwire.MilliSatoshi, 0, 12)\n\n\t// Balanced shards are tried first because they preserve enough part slots\n\t// to complete the remainder while minimizing liquidity pressure.\n\tamounts = candidateAppendUnique(amounts, minimum)\n\tamounts = candidateAppendUnique(\n\t\tamounts, minimum+minimum/5,\n\t)\n\tamounts = candidateAppendUnique(\n\t\tamounts, minimum+minimum/2,\n\t)\n\tamounts = candidateAppendUnique(\n\t\tamounts, minimum*2,\n\t)\n\n\tlimit := partsLeft\n\tif limit > 8 {\n\t\tlimit = 8\n\t}\n\tfor parts := limit; parts >= 2; parts-- {\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, parts),\n\t\t)\n\t}\n\n\tamounts = candidateAppendUnique(amounts, amt)\n\n\treturn amounts\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\tif shard < minimum || shard > amt {\n\t\t\tcontinue\n\t\t}\n\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogress := float64(shard) / math.Max(float64(minimum), 1)\n\t\tfee := rt.TotalAmount - shard\n\n\t\t// Reliability dominates. Progress provides only a modest preference\n\t\t// for larger shards when their success probability is comparable.\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.16*math.Log1p(progress) -\n\t\t\t4.0*float64(fee)/math.Max(float64(shard), 1)\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\n\t\tif shard == minimum && probability >= 0.72 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.12\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+2.2, 9,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.10\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.95\n\t\tif i >= len(keys)/2 {\n\t\t\tpenalty = 1.25\n\t\t}\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 7,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(\n\t\t\t\tr.networkID, edge, amounts[i], now,\n\t\t\t)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tr.networkID, edge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Unknown-source failures trigger route-wide exploration without\n\t// contaminating persistent liquidity beliefs.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 24,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Mail watcher armed for session `019f96d5-96d5-77c0-a57a-cdd05f58461d`."
|
|
}
|
|
},
|
|
{
|
|
"id": 25,
|
|
"parent": 6,
|
|
"score": 0.2124,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "package main\n\nimport (\n\t\"container/heap\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tgraphdb \"github.com/lightningnetwork/lnd/graph/db\"\n\t\"github.com/lightningnetwork/lnd/lnwire\"\n\t\"github.com/lightningnetwork/lnd/routing\"\n\t\"github.com/lightningnetwork/lnd/routing/route\"\n)\n\nconst (\n\tcandidateFinalCltvDelta = 40\n\tcandidateAttemptLimit = 48\n\tcandidateMaxRouteHops = 20\n)\n\ntype candidateEdgeKey struct {\n\tchanID uint64\n\tfrom route.Vertex\n\tto route.Vertex\n}\n\ntype candidateEdge struct {\n\tkey candidateEdgeKey\n\tcapacity lnwire.MilliSatoshi\n\n\tbaseFeeMsat lnwire.MilliSatoshi\n\tfeeRatePPM lnwire.MilliSatoshi\n\ttimeLockDelta uint16\n\tminHTLC lnwire.MilliSatoshi\n\tmaxHTLC lnwire.MilliSatoshi\n}\n\nfunc (e *candidateEdge) fee(\n\tamt lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tconst million = lnwire.MilliSatoshi(1_000_000)\n\n\treturn e.baseFeeMsat +\n\t\t(amt/million)*e.feeRatePPM +\n\t\t(amt%million)*e.feeRatePPM/million\n}\n\nfunc (e *candidateEdge) usable(amt lnwire.MilliSatoshi) bool {\n\tif amt <= 0 || amt < e.minHTLC || amt > e.capacity {\n\t\treturn false\n\t}\n\tif e.maxHTLC != 0 && amt > e.maxHTLC {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype candidateLiquidityBelief struct {\n\tcapacity lnwire.MilliSatoshi\n\tlowerOK lnwire.MilliSatoshi\n\tupperFail lnwire.MilliSatoshi\n\testimate lnwire.MilliSatoshi\n\tconf float64\n\tupdatedAt time.Time\n}\n\nvar candidateKnowledge = struct {\n\tsync.RWMutex\n\tbeliefs map[candidateEdgeKey]candidateLiquidityBelief\n}{\n\tbeliefs: make(map[candidateEdgeKey]candidateLiquidityBelief),\n}\n\nfunc candidateReverseKey(key candidateEdgeKey) candidateEdgeKey {\n\treturn candidateEdgeKey{\n\t\tchanID: key.chanID,\n\t\tfrom: key.to,\n\t\tto: key.from,\n\t}\n}\n\nfunc candidateClampAmount(\n\tamt, capacity lnwire.MilliSatoshi) lnwire.MilliSatoshi {\n\n\tswitch {\n\tcase amt < 0:\n\t\treturn 0\n\tcase amt > capacity:\n\t\treturn capacity\n\tdefault:\n\t\treturn amt\n\t}\n}\n\nfunc candidateNormalizeBelief(\n\tb candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) candidateLiquidityBelief {\n\n\tb.capacity = capacity\n\tb.lowerOK = candidateClampAmount(b.lowerOK, capacity)\n\tb.estimate = candidateClampAmount(b.estimate, capacity)\n\n\tif b.upperFail < 0 || b.upperFail > capacity {\n\t\tb.upperFail = 0\n\t}\n\tif b.upperFail != 0 && b.lowerOK >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\tif b.estimate < b.lowerOK {\n\t\tb.estimate = b.lowerOK\n\t}\n\tif b.upperFail != 0 && b.estimate >= b.upperFail {\n\t\tb.estimate = b.upperFail - 1\n\t\tif b.estimate < b.lowerOK {\n\t\t\tb.estimate = b.lowerOK\n\t\t}\n\t}\n\n\tb.conf = math.Max(0, math.Min(b.conf, 0.995))\n\n\treturn b\n}\n\nfunc candidateBeliefConfidence(\n\tb candidateLiquidityBelief, now time.Time) float64 {\n\n\tif b.conf <= 0 || b.updatedAt.IsZero() {\n\t\treturn 0\n\t}\n\n\tage := now.Sub(b.updatedAt).Minutes()\n\tif age < 0 {\n\t\treturn 0\n\t}\n\n\t// Recent attempt feedback is valuable, but background payments can\n\t// invalidate directional evidence within a few virtual minutes.\n\tconst halfLifeMinutes = 12.0\n\n\tconf := b.conf * math.Exp(-math.Ln2*age/halfLifeMinutes)\n\tif conf < 0.015 {\n\t\treturn 0\n\t}\n\n\treturn conf\n}\n\nfunc candidatePrepareObservation(\n\tb candidateLiquidityBelief, capacity lnwire.MilliSatoshi,\n\tnow time.Time) candidateLiquidityBelief {\n\n\tif b.capacity != capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tconf := candidateBeliefConfidence(b, now)\n\tif conf == 0 {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: capacity,\n\t\t}\n\t}\n\n\tage := now.Sub(b.updatedAt)\n\tb.conf = conf\n\n\t// Aged bounds remain represented by the estimate, but cease to be\n\t// treated as exact observations.\n\tif age > 8*time.Minute {\n\t\tb.lowerOK = 0\n\t\tb.upperFail = 0\n\t}\n\n\treturn candidateNormalizeBelief(b, capacity)\n}\n\nfunc candidateSnapshot(\n\tedge *candidateEdge) candidateLiquidityBelief {\n\n\tcandidateKnowledge.RLock()\n\tb, ok := candidateKnowledge.beliefs[edge.key]\n\tcandidateKnowledge.RUnlock()\n\n\tif !ok || b.capacity != edge.capacity {\n\t\treturn candidateLiquidityBelief{\n\t\t\tcapacity: edge.capacity,\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc candidateStorePair(\n\tkey candidateEdgeKey, forward candidateLiquidityBelief,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tforward = candidateNormalizeBelief(forward, capacity)\n\tcandidateKnowledge.beliefs[key] = forward\n\n\treverseKey := candidateReverseKey(key)\n\treverse := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[reverseKey],\n\t\tcapacity, forward.updatedAt,\n\t)\n\n\treverse.updatedAt = forward.updatedAt\n\treverse.conf = math.Max(reverse.conf, forward.conf*0.82)\n\treverse.estimate = capacity - forward.estimate\n\n\tif forward.upperFail != 0 {\n\t\treverse.lowerOK = capacity - forward.upperFail + 1\n\t}\n\tif forward.lowerOK != 0 {\n\t\treverse.upperFail = capacity - forward.lowerOK + 1\n\t}\n\n\tcandidateKnowledge.beliefs[reverseKey] =\n\t\tcandidateNormalizeBelief(reverse, capacity)\n}\n\nfunc candidateRecordPass(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif amt > b.lowerOK {\n\t\tb.lowerOK = amt\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\tb.upperFail = 0\n\t}\n\n\t// Under the bimodal model, a successful non-trivial probe is strong\n\t// evidence for the high-liquidity mode.\n\testimate := edge.capacity * 19 / 20\n\tif estimate < amt {\n\t\testimate = amt\n\t}\n\tif b.estimate < estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.94)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordFailure(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\tif b.upperFail == 0 || amt < b.upperFail {\n\t\tb.upperFail = amt\n\t}\n\tif b.lowerOK >= amt {\n\t\tb.lowerOK = amt - 1\n\t}\n\n\t// A liquidity failure generally selects the depleted mode. Preserve\n\t// any proven lower bound, while placing the point estimate close to\n\t// zero rather than near half capacity.\n\testimate := amt / 20\n\tcapFloor := edge.capacity / 1000\n\tif capFloor < 1 {\n\t\tcapFloor = 1\n\t}\n\tif estimate > capFloor {\n\t\testimate = capFloor\n\t}\n\tif estimate < b.lowerOK {\n\t\testimate = b.lowerOK\n\t}\n\tif b.estimate == 0 || estimate < b.estimate {\n\t\tb.estimate = estimate\n\t}\n\n\tb.conf = math.Max(b.conf, 0.985)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\nfunc candidateRecordSettlement(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi, now time.Time) {\n\n\tif edge == nil || amt <= 0 {\n\t\treturn\n\t}\n\n\tcandidateKnowledge.Lock()\n\tdefer candidateKnowledge.Unlock()\n\n\tb := candidatePrepareObservation(\n\t\tcandidateKnowledge.beliefs[edge.key],\n\t\tedge.capacity, now,\n\t)\n\n\testimate := b.estimate\n\tif estimate < amt {\n\t\testimate = edge.capacity * 19 / 20\n\t\tif estimate < amt {\n\t\t\testimate = amt\n\t\t}\n\t}\n\n\tb.estimate = estimate - amt\n\tif b.lowerOK > amt {\n\t\tb.lowerOK -= amt\n\t} else {\n\t\tb.lowerOK = 0\n\t}\n\tif b.upperFail > amt {\n\t\tb.upperFail -= amt\n\t} else {\n\t\tb.upperFail = 0\n\t}\n\n\tb.conf = math.Max(b.conf, 0.95)\n\tb.updatedAt = now\n\tcandidateStorePair(edge.key, b, edge.capacity)\n}\n\ntype candidateRouter struct {\n\tview routing.SimNetworkView\n\tsource route.Vertex\n\tspec *routing.SimPaymentSpec\n\n\tincomingEdges map[route.Vertex][]*candidateEdge\n\tedges map[candidateEdgeKey]*candidateEdge\n\tlocalBalances map[uint64]lnwire.MilliSatoshi\n\n\tsessionLower map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionFailed map[candidateEdgeKey]lnwire.MilliSatoshi\n\tsessionBlocked map[candidateEdgeKey]bool\n\tsessionPenalty map[candidateEdgeKey]float64\n\tedgeUses map[candidateEdgeKey]uint32\n\trouteUses map[string]uint32\n\n\tattempts uint32\n}\n\nfunc newCandidateRouter(\n\tview routing.SimNetworkView, source route.Vertex,\n\tlocalBalances map[uint64]lnwire.MilliSatoshi,\n\tspec *routing.SimPaymentSpec) (routing.SimRouter, error) {\n\n\tif view == nil {\n\t\treturn nil, errors.New(\"network view is nil\")\n\t}\n\tif spec == nil {\n\t\treturn nil, errors.New(\"payment specification is nil\")\n\t}\n\tif spec.Amount <= 0 {\n\t\treturn nil, errors.New(\"payment amount must be positive\")\n\t}\n\tif source == spec.Target {\n\t\treturn nil, errors.New(\"source is payment target\")\n\t}\n\n\tr := &candidateRouter{\n\t\tview: view,\n\t\tsource: source,\n\t\tspec: spec,\n\t\tincomingEdges: make(map[route.Vertex][]*candidateEdge),\n\t\tedges: make(map[candidateEdgeKey]*candidateEdge),\n\t\tlocalBalances: make(map[uint64]lnwire.MilliSatoshi),\n\t\tsessionLower: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionFailed: make(map[candidateEdgeKey]lnwire.MilliSatoshi),\n\t\tsessionBlocked: make(map[candidateEdgeKey]bool),\n\t\tsessionPenalty: make(map[candidateEdgeKey]float64),\n\t\tedgeUses: make(map[candidateEdgeKey]uint32),\n\t\trouteUses: make(map[string]uint32),\n\t}\n\n\tfor chanID, balance := range localBalances {\n\t\tr.localBalances[chanID] = balance\n\t}\n\n\tctx := context.Background()\n\tseen := map[route.Vertex]bool{source: true}\n\tqueue := []route.Vertex{source}\n\n\tfor len(queue) != 0 {\n\t\tnode := queue[0]\n\t\tqueue = queue[1:]\n\n\t\terr := view.ForEachNodeDirectedChannel(\n\t\t\tctx, node,\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\tpolicy := ch.InPolicy\n\t\t\t\tif policy == nil || policy.IsDisabled {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tkey := candidateEdgeKey{\n\t\t\t\t\tchanID: ch.ChannelID,\n\t\t\t\t\tfrom: ch.OtherNode,\n\t\t\t\t\tto: node,\n\t\t\t\t}\n\t\t\t\tedge := &candidateEdge{\n\t\t\t\t\tkey: key,\n\t\t\t\t\tcapacity: lnwire.NewMSatFromSatoshis(\n\t\t\t\t\t\tch.Capacity,\n\t\t\t\t\t),\n\t\t\t\t\tbaseFeeMsat: policy.FeeBaseMSat,\n\t\t\t\t\tfeeRatePPM: policy.FeeProportionalMillionths,\n\t\t\t\t\ttimeLockDelta: policy.TimeLockDelta,\n\t\t\t\t\tminHTLC: policy.MinHTLC,\n\t\t\t\t}\n\t\t\t\tif policy.HasMaxHTLC {\n\t\t\t\t\tedge.maxHTLC = policy.MaxHTLC\n\t\t\t\t}\n\n\t\t\t\tr.incomingEdges[key.to] = append(\n\t\t\t\t\tr.incomingEdges[key.to], edge,\n\t\t\t\t)\n\t\t\t\tr.edges[key] = edge\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tfunc() {},\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 candidateClampProbability(probability float64) float64 {\n\tswitch {\n\tcase probability < 0.005:\n\t\treturn 0.005\n\tcase probability > 0.995:\n\t\treturn 0.995\n\tdefault:\n\t\treturn probability\n\t}\n}\n\nfunc candidatePriorProbability(\n\tamt, capacity lnwire.MilliSatoshi) float64 {\n\n\tif capacity <= 0 || amt <= 0 || amt > capacity {\n\t\treturn 0\n\t}\n\n\tratio := float64(amt) / float64(capacity)\n\n\t// The low mode makes very small probes plausible even when the\n\t// direction is depleted. The high mode stays nearly flat until the\n\t// channel-capacity cliff.\n\tlowMode := 0.47 * math.Exp(-ratio/0.018)\n\thighMode := 0.51 /\n\t\t(1 + math.Exp((ratio-0.91)/0.042))\n\n\treturn candidateClampProbability(0.005 + lowMode + highMode)\n}\n\nfunc candidateLearnedProbability(\n\tb candidateLiquidityBelief, amt,\n\tcapacity lnwire.MilliSatoshi) float64 {\n\n\tif b.lowerOK != 0 && amt <= b.lowerOK {\n\t\treturn 0.995\n\t}\n\tif b.upperFail != 0 && amt >= b.upperFail {\n\t\treturn 0.005\n\t}\n\tif b.estimate == 0 {\n\t\treturn candidatePriorProbability(amt, capacity)\n\t}\n\n\twidth := math.Max(float64(capacity)*0.025, 1)\n\tposition := (float64(amt) - float64(b.estimate)) / width\n\tprobability := 1 / (1 + math.Exp(position))\n\n\tif b.upperFail != 0 {\n\t\tlower := float64(b.lowerOK)\n\t\tupper := float64(b.upperFail)\n\t\tfraction := (float64(amt) - lower) /\n\t\t\tmath.Max(upper-lower, 1)\n\t\tfraction = math.Max(0, math.Min(fraction, 1))\n\n\t\tbounded := 0.005 + 0.99*math.Pow(1-fraction, 2.8)\n\t\tprobability = 0.75*bounded + 0.25*probability\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\nfunc (r *candidateRouter) edgeProbability(\n\tedge *candidateEdge, amt lnwire.MilliSatoshi) float64 {\n\n\tif r.sessionBlocked[edge.key] {\n\t\treturn 0\n\t}\n\n\tif edge.key.from == r.source {\n\t\tif r.localBalances[edge.key.chanID] < amt {\n\t\t\treturn 0\n\t\t}\n\n\t\treturn 0.9995\n\t}\n\n\tfailedAt := r.sessionFailed[edge.key]\n\tif failedAt != 0 && amt >= failedAt {\n\t\treturn 0\n\t}\n\tif lower := r.sessionLower[edge.key]; lower >= amt {\n\t\treturn 0.998\n\t}\n\n\tprior := candidatePriorProbability(amt, edge.capacity)\n\tif prior == 0 {\n\t\treturn 0\n\t}\n\n\tb := candidateSnapshot(edge)\n\tconf := candidateBeliefConfidence(b, r.view.Now())\n\tprobability := prior\n\tif conf != 0 {\n\t\tlearned := candidateLearnedProbability(\n\t\t\tb, amt, edge.capacity,\n\t\t)\n\t\tprobability = conf*learned + (1-conf)*prior\n\t}\n\n\tif failedAt != 0 {\n\t\tratio := float64(amt) / float64(failedAt)\n\n\t\t// Do not blacklist the channel at all lower amounts. Require a\n\t\t// meaningful reduction before retrying, then gradually restore\n\t\t// the bimodal prior for genuinely small probes.\n\t\tswitch {\n\t\tcase ratio > 0.72:\n\t\t\tprobability *= 0.02\n\t\tcase ratio > 0.50:\n\t\t\tprobability *= 0.10\n\t\tcase ratio > 0.30:\n\t\t\tprobability *= 0.32\n\t\tcase ratio > 0.12:\n\t\t\tprobability *= 0.68\n\t\tdefault:\n\t\t\tprobability *= 0.92\n\t\t}\n\t}\n\n\treturn candidateClampProbability(probability)\n}\n\ntype candidateQueueItem struct {\n\tnode route.Vertex\n\tamount lnwire.MilliSatoshi\n\tscore float64\n\trisk float64\n\thops uint16\n}\n\ntype candidateQueue []*candidateQueueItem\n\nfunc (q candidateQueue) Len() int {\n\treturn len(q)\n}\n\nfunc (q candidateQueue) Less(i, j int) bool {\n\tif math.Abs(q[i].score-q[j].score) > 1e-12 {\n\t\treturn q[i].score < q[j].score\n\t}\n\tif q[i].hops != q[j].hops {\n\t\treturn q[i].hops < q[j].hops\n\t}\n\n\treturn q[i].amount < q[j].amount\n}\n\nfunc (q candidateQueue) Swap(i, j int) {\n\tq[i], q[j] = q[j], q[i]\n}\n\nfunc (q *candidateQueue) Push(value any) {\n\t*q = append(*q, value.(*candidateQueueItem))\n}\n\nfunc (q *candidateQueue) Pop() any {\n\told := *q\n\tlast := old[len(old)-1]\n\t*q = old[:len(old)-1]\n\n\treturn last\n}\n\nfunc (r *candidateRouter) findRoute(\n\tdeliver lnwire.MilliSatoshi) (*route.Route, float64, error) {\n\n\tif deliver <= 0 {\n\t\treturn nil, 0, errors.New(\"route amount must be positive\")\n\t}\n\n\tbestScore := make(map[route.Vertex]float64)\n\trequired := make(map[route.Vertex]lnwire.MilliSatoshi)\n\tnext := make(map[route.Vertex]*candidateEdge)\n\n\tbestScore[r.spec.Target] = 0\n\trequired[r.spec.Target] = deliver\n\n\tqueue := &candidateQueue{}\n\theap.Push(queue, &candidateQueueItem{\n\t\tnode: r.spec.Target,\n\t\tamount: deliver,\n\t})\n\n\tsourceRisk := 0.0\n\tfeeScale := math.Max(float64(deliver), 1_000_000)\n\n\tfor queue.Len() != 0 {\n\t\titem := heap.Pop(queue).(*candidateQueueItem)\n\n\t\tscore, ok := bestScore[item.node]\n\t\tif !ok || item.score > score+1e-12 {\n\t\t\tcontinue\n\t\t}\n\t\tif required[item.node] != item.amount {\n\t\t\tcontinue\n\t\t}\n\t\tif item.node == r.source {\n\t\t\tsourceRisk = item.risk\n\t\t\tbreak\n\t\t}\n\t\tif item.hops >= candidateMaxRouteHops {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, edge := range r.incomingEdges[item.node] {\n\t\t\tif !edge.usable(item.amount) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprobability := r.edgeProbability(edge, item.amount)\n\t\t\tif probability <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsending := item.amount\n\t\t\tfee := lnwire.MilliSatoshi(0)\n\t\t\tif edge.key.from != r.source {\n\t\t\t\tfee = edge.fee(item.amount)\n\t\t\t\tif fee < 0 ||\n\t\t\t\t\titem.amount >\n\t\t\t\t\t\tlnwire.MilliSatoshi(math.MaxInt64)-fee {\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tsending += fee\n\t\t\t}\n\n\t\t\triskCost := -math.Log(probability)\n\t\t\tfeeCost := 5.5 * float64(fee) / feeScale\n\t\t\thopCost := 0.075\n\t\t\tuseCost := 0.16 * math.Min(\n\t\t\t\tfloat64(r.edgeUses[edge.key]), 10,\n\t\t\t)\n\t\t\tpenalty := r.sessionPenalty[edge.key]\n\n\t\t\t// Prefer channels with capacity headroom when otherwise\n\t\t\t// similarly probable. This also breaks ties away from paths\n\t\t\t// whose advertised capacity barely fits the shard.\n\t\t\theadroom := float64(item.amount) /\n\t\t\t\tmath.Max(float64(edge.capacity), 1)\n\t\t\tcapacityCost := 0.045 * headroom * headroom\n\n\t\t\tnewScore := item.score + riskCost + feeCost +\n\t\t\t\thopCost + useCost + penalty + capacityCost\n\n\t\t\toldScore, exists := bestScore[edge.key.from]\n\t\t\toldAmount := required[edge.key.from]\n\t\t\tif exists &&\n\t\t\t\t(newScore > oldScore+1e-12 ||\n\t\t\t\t\t(math.Abs(newScore-oldScore) <= 1e-12 &&\n\t\t\t\t\t\tsending >= oldAmount)) {\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbestScore[edge.key.from] = newScore\n\t\t\trequired[edge.key.from] = sending\n\t\t\tnext[edge.key.from] = edge\n\n\t\t\theap.Push(queue, &candidateQueueItem{\n\t\t\t\tnode: edge.key.from,\n\t\t\t\tamount: sending,\n\t\t\t\tscore: newScore,\n\t\t\t\trisk: item.risk + riskCost,\n\t\t\t\thops: item.hops + 1,\n\t\t\t})\n\t\t}\n\t}\n\n\tif _, ok := next[r.source]; !ok {\n\t\treturn nil, 0, errors.New(\"no route found\")\n\t}\n\n\trt, err := r.buildRoute(deliver, next)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn rt, sourceRisk, nil\n}\n\nfunc (r *candidateRouter) buildRoute(\n\tdeliver lnwire.MilliSatoshi,\n\tnext map[route.Vertex]*candidateEdge) (*route.Route, error) {\n\n\tpath := make([]*candidateEdge, 0, 8)\n\tvisited := make(map[route.Vertex]bool)\n\n\tfor node := r.source; node != r.spec.Target; {\n\t\tif visited[node] {\n\t\t\treturn nil, errors.New(\"cycle in selected route\")\n\t\t}\n\t\tvisited[node] = true\n\n\t\tedge, ok := next[node]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"broken path at %v\", node)\n\t\t}\n\n\t\tpath = append(path, edge)\n\t\tif len(path) > candidateMaxRouteHops {\n\t\t\treturn nil, errors.New(\"selected route is too long\")\n\t\t}\n\n\t\tnode = edge.key.to\n\t}\n\n\tif len(path) == 0 {\n\t\treturn nil, errors.New(\"selected route has no hops\")\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, len(path))\n\texpiries := make([]uint32, len(path))\n\n\tlast := len(path) - 1\n\tamounts[last] = deliver\n\texpiries[last] = candidateFinalCltvDelta\n\n\tfor i := last - 1; i >= 0; i-- {\n\t\toutgoing := path[i+1]\n\t\tfee := outgoing.fee(amounts[i+1])\n\t\tif fee < 0 ||\n\t\t\tamounts[i+1] >\n\t\t\t\tlnwire.MilliSatoshi(math.MaxInt64)-fee {\n\n\t\t\treturn nil, errors.New(\"route amount overflow\")\n\t\t}\n\n\t\tamounts[i] = amounts[i+1] + fee\n\t\texpiries[i] = expiries[i+1] +\n\t\t\tuint32(outgoing.timeLockDelta)\n\t}\n\n\thops := make([]*route.Hop, len(path))\n\tfor i, edge := range path {\n\t\tamountToForward := deliver\n\t\toutgoingExpiry := uint32(candidateFinalCltvDelta)\n\n\t\tif i < last {\n\t\t\tamountToForward = amounts[i+1]\n\t\t\toutgoingExpiry = expiries[i+1]\n\t\t}\n\n\t\thops[i] = &route.Hop{\n\t\t\tPubKeyBytes: edge.key.to,\n\t\t\tChannelID: edge.key.chanID,\n\t\t\tAmtToForward: amountToForward,\n\t\t\tOutgoingTimeLock: outgoingExpiry,\n\t\t}\n\t}\n\n\treturn &route.Route{\n\t\tTotalTimeLock: expiries[0],\n\t\tTotalAmount: amounts[0],\n\t\tSourcePubKey: r.source,\n\t\tHops: hops,\n\t}, nil\n}\n\nfunc candidateCeilDiv(\n\tamt lnwire.MilliSatoshi, divisor uint32) lnwire.MilliSatoshi {\n\n\tif divisor <= 1 {\n\t\treturn amt\n\t}\n\n\td := lnwire.MilliSatoshi(divisor)\n\treturn amt/d + lnwire.MilliSatoshi(boolToUint64(amt%d != 0))\n}\n\nfunc boolToUint64(value bool) uint64 {\n\tif value {\n\t\treturn 1\n\t}\n\n\treturn 0\n}\n\nfunc candidateAppendUnique(\n\tamounts []lnwire.MilliSatoshi,\n\tamt lnwire.MilliSatoshi) []lnwire.MilliSatoshi {\n\n\tif amt <= 0 {\n\t\treturn amounts\n\t}\n\n\tfor _, existing := range amounts {\n\t\tif existing == amt {\n\t\t\treturn amounts\n\t\t}\n\t}\n\n\treturn append(amounts, amt)\n}\n\nfunc candidateShardAmounts(\n\tamt lnwire.MilliSatoshi,\n\tpartsLeft uint32) []lnwire.MilliSatoshi {\n\n\tif partsLeft <= 1 {\n\t\treturn []lnwire.MilliSatoshi{amt}\n\t}\n\n\tamounts := make([]lnwire.MilliSatoshi, 0, 12)\n\tdivisors := []uint32{1, 2, 3, 4, 6, 8, 12, 16}\n\n\tfor _, divisor := range divisors {\n\t\tif divisor > partsLeft {\n\t\t\tcontinue\n\t\t}\n\n\t\tamounts = candidateAppendUnique(\n\t\t\tamounts, candidateCeilDiv(amt, divisor),\n\t\t)\n\t}\n\n\tamounts = candidateAppendUnique(\n\t\tamounts, candidateCeilDiv(amt, partsLeft),\n\t)\n\n\treturn amounts\n}\n\nfunc candidateRouteID(rt *route.Route) string {\n\tif rt == nil {\n\t\treturn \"\"\n\t}\n\n\tid := \"\"\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tid += fmt.Sprintf(\n\t\t\t\"%x:%d:%x;\", from[:], hop.ChannelID,\n\t\t\thop.PubKeyBytes[:],\n\t\t)\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn id\n}\n\nfunc (r *candidateRouter) markRouteUsed(rt *route.Route) {\n\tfrom := rt.SourcePubKey\n\tfor _, hop := range rt.Hops {\n\t\tkey := candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\t\tr.edgeUses[key]++\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\tr.routeUses[candidateRouteID(rt)]++\n}\n\nfunc (r *candidateRouter) RequestRoute(\n\tamt lnwire.MilliSatoshi,\n\tinFlightHtlcs uint32) (*route.Route, error) {\n\n\tif amt <= 0 {\n\t\treturn nil, errors.New(\"remaining amount must be positive\")\n\t}\n\tif r.attempts >= candidateAttemptLimit {\n\t\treturn nil, errors.New(\"routing attempt budget exhausted\")\n\t}\n\n\tmaxParts := r.spec.MaxParts\n\tif maxParts == 0 {\n\t\tmaxParts = 1\n\t}\n\tif inFlightHtlcs >= maxParts {\n\t\treturn nil, errors.New(\"maximum payment parts reached\")\n\t}\n\n\tpartsLeft := maxParts - inFlightHtlcs\n\tminimum := candidateCeilDiv(amt, partsLeft)\n\tshards := candidateShardAmounts(amt, partsLeft)\n\n\tvar bestRoute *route.Route\n\tbestUtility := math.Inf(-1)\n\n\tfor _, shard := range shards {\n\t\trt, logRisk, err := r.findRoute(shard)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tprobability := math.Exp(-logRisk)\n\t\tprogressUnits := float64(shard) /\n\t\t\tmath.Max(float64(minimum), 1)\n\t\tfee := rt.TotalAmount - shard\n\t\tfeePenalty := 4.5 * float64(fee) /\n\t\t\tmath.Max(float64(shard), 1)\n\t\thopPenalty := 0.012 * float64(len(rt.Hops))\n\n\t\trouteReuse := float64(r.routeUses[candidateRouteID(rt)])\n\t\treusePenalty := 0.22 * math.Min(routeReuse, 8)\n\n\t\t// Expected progress dominates the choice. This avoids burning many\n\t\t// attempts on tiny shards while still splitting amounts whose full\n\t\t// route probability is poor.\n\t\tutility := math.Log(math.Max(probability, 1e-12)) +\n\t\t\t0.88*math.Log(math.Max(progressUnits, 1)) -\n\t\t\tfeePenalty - hopPenalty - reusePenalty\n\n\t\tif bestRoute == nil || utility > bestUtility {\n\t\t\tbestRoute = rt\n\t\t\tbestUtility = utility\n\t\t}\n\t}\n\n\tif bestRoute == nil {\n\t\treturn nil, errors.New(\"no route found\")\n\t}\n\n\tr.attempts++\n\tr.markRouteUsed(bestRoute)\n\n\treturn bestRoute, nil\n}\n\nfunc (r *candidateRouter) routeData(\n\trt *route.Route) ([]candidateEdgeKey,\n\t[]lnwire.MilliSatoshi) {\n\n\tkeys := make([]candidateEdgeKey, len(rt.Hops))\n\tamounts := make([]lnwire.MilliSatoshi, len(rt.Hops))\n\n\tfrom := rt.SourcePubKey\n\tfor i, hop := range rt.Hops {\n\t\tkeys[i] = candidateEdgeKey{\n\t\t\tchanID: hop.ChannelID,\n\t\t\tfrom: from,\n\t\t\tto: hop.PubKeyBytes,\n\t\t}\n\n\t\tif i == 0 {\n\t\t\tamounts[i] = rt.TotalAmount\n\t\t} else {\n\t\t\tamounts[i] = rt.Hops[i-1].AmtToForward\n\t\t}\n\n\t\tfrom = hop.PubKeyBytes\n\t}\n\n\treturn keys, amounts\n}\n\nfunc candidateFailureIndex(\n\trt *route.Route, source route.Vertex) int {\n\n\tif source == rt.SourcePubKey {\n\t\treturn 0\n\t}\n\n\tfor i, hop := range rt.Hops {\n\t\tif hop.PubKeyBytes == source {\n\t\t\treturn i + 1\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc (r *candidateRouter) recordSessionPass(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tif amt > r.sessionLower[key] {\n\t\tr.sessionLower[key] = amt\n\t}\n\tif failed := r.sessionFailed[key]; failed != 0 && amt >= failed {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.12\n}\n\nfunc (r *candidateRouter) recordSessionFailure(\n\tkey candidateEdgeKey, amt lnwire.MilliSatoshi) {\n\n\tfailed := r.sessionFailed[key]\n\tif failed == 0 || amt < failed {\n\t\tr.sessionFailed[key] = amt\n\t}\n\tif r.sessionLower[key] >= amt {\n\t\tr.sessionLower[key] = amt - 1\n\t}\n\n\tr.sessionPenalty[key] = math.Min(\n\t\tr.sessionPenalty[key]+1.75, 8,\n\t)\n}\n\nfunc (r *candidateRouter) recordSessionSettlement(\n\tkey candidateEdgeKey, amt,\n\tcapacity lnwire.MilliSatoshi) {\n\n\tif lower := r.sessionLower[key]; lower > amt {\n\t\tr.sessionLower[key] = lower - amt\n\t} else {\n\t\tdelete(r.sessionLower, key)\n\t}\n\n\tif failed := r.sessionFailed[key]; failed > amt {\n\t\tr.sessionFailed[key] = failed - amt\n\t} else {\n\t\tdelete(r.sessionFailed, key)\n\t}\n\n\tr.sessionPenalty[key] *= 0.08\n\n\treverse := candidateReverseKey(key)\n\treverseLower := r.sessionLower[reverse] + amt\n\tif reverseLower > capacity {\n\t\treverseLower = capacity\n\t}\n\tr.sessionLower[reverse] = reverseLower\n}\n\nfunc (r *candidateRouter) penalizeUnknownRoute(\n\tkeys []candidateEdgeKey) {\n\n\tfor i, key := range keys {\n\t\tif key.from == r.source {\n\t\t\tcontinue\n\t\t}\n\n\t\tpenalty := 0.95\n\t\tif i >= len(keys)/2 {\n\t\t\tpenalty = 1.20\n\t\t}\n\n\t\tr.sessionPenalty[key] = math.Min(\n\t\t\tr.sessionPenalty[key]+penalty, 7,\n\t\t)\n\t}\n}\n\nfunc (r *candidateRouter) ReportAttempt(\n\t_ uint64, rt *route.Route,\n\tresult routing.SimHtlcResult) error {\n\n\tif rt == nil {\n\t\treturn errors.New(\"reported route is nil\")\n\t}\n\n\tkeys, amounts := r.routeData(rt)\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\n\tnow := r.view.Now()\n\n\tif result.Failure == nil {\n\t\tfor i, key := range keys {\n\t\t\tedge := r.edges[key]\n\t\t\tif edge == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordSettlement(edge, amounts[i], now)\n\t\t\tr.recordSessionSettlement(\n\t\t\t\tkey, amounts[i], edge.capacity,\n\t\t\t)\n\t\t}\n\n\t\tfirst := keys[0]\n\t\tif balance := r.localBalances[first.chanID];\n\t\t\tbalance > amounts[0] {\n\n\t\t\tr.localBalances[first.chanID] =\n\t\t\t\tbalance - amounts[0]\n\t\t} else {\n\t\t\tr.localBalances[first.chanID] = 0\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfailIndex := candidateFailureIndex(\n\t\trt, result.FailureSource,\n\t)\n\n\tif failIndex >= 0 {\n\t\tprefixEnd := failIndex\n\t\tif prefixEnd > len(keys) {\n\t\t\tprefixEnd = len(keys)\n\t\t}\n\n\t\tfor i := 0; i < prefixEnd; i++ {\n\t\t\tedge := r.edges[keys[i]]\n\t\t\tif edge == nil || edge.key.from == r.source {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcandidateRecordPass(edge, amounts[i], now)\n\t\t\tr.recordSessionPass(keys[i], amounts[i])\n\t\t}\n\t}\n\n\tcode := result.Failure.Code()\n\n\tif failIndex >= 0 && failIndex < len(keys) {\n\t\tkey := keys[failIndex]\n\t\tedge := r.edges[key]\n\t\tif edge == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch code {\n\t\tcase lnwire.CodeTemporaryChannelFailure:\n\t\t\tcandidateRecordFailure(\n\t\t\t\tedge, amounts[failIndex], now,\n\t\t\t)\n\t\t\tr.recordSessionFailure(\n\t\t\t\tkey, amounts[failIndex],\n\t\t\t)\n\n\t\tcase lnwire.CodeFeeInsufficient,\n\t\t\tlnwire.CodeIncorrectCltvExpiry:\n\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 20\n\n\t\tdefault:\n\t\t\tr.sessionBlocked[key] = true\n\t\t\tr.sessionPenalty[key] = 12\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Failures without a usable source must trigger substantial route\n\t// exploration, but must not poison persistent channel beliefs.\n\tr.penalizeUnknownRoute(keys)\n\n\treturn nil\n}"
|
|
}
|
|
},
|
|
{
|
|
"id": 26,
|
|
"parent": 0,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed."
|
|
}
|
|
},
|
|
{
|
|
"id": 27,
|
|
"parent": 0,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96e1-4607-7ee2-831f-fa798c96a7a3`."
|
|
}
|
|
},
|
|
{
|
|
"id": 28,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96e4-45eb-7ad2-afad-51f076048803`."
|
|
}
|
|
},
|
|
{
|
|
"id": 29,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96ea-0d10-7372-a1db-57f7f21dacee`."
|
|
}
|
|
},
|
|
{
|
|
"id": 30,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96ed-0a7e-7700-b58c-391205e9a6e2`."
|
|
}
|
|
},
|
|
{
|
|
"id": 31,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": ""
|
|
}
|
|
},
|
|
{
|
|
"id": 32,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96f3-1dd2-75f1-9791-7f3438f31d54`."
|
|
}
|
|
},
|
|
{
|
|
"id": 33,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96f9-304a-7ed2-942a-49a5aa043bc7`."
|
|
}
|
|
},
|
|
{
|
|
"id": 34,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96fc-482e-7560-8d37-03e3aa054546`."
|
|
}
|
|
},
|
|
{
|
|
"id": 35,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f96ff-5386-7941-ab7c-7a1ede3aa32a`."
|
|
}
|
|
},
|
|
{
|
|
"id": 36,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Mail watcher armed for session `019f9702-93fa-7973-9e17-39407be1e60b`."
|
|
}
|
|
},
|
|
{
|
|
"id": 37,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f970b-d154-78d3-b740-0f639e0ff639`."
|
|
}
|
|
},
|
|
{
|
|
"id": 38,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f970e-d3d7-76a1-ab1d-99a93bae97c1`."
|
|
}
|
|
},
|
|
{
|
|
"id": 39,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9715-137c-7cc2-b978-a244b65c2f6c`."
|
|
}
|
|
},
|
|
{
|
|
"id": 40,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9718-2486-75c3-9e5c-50de6aeefc3b`."
|
|
}
|
|
},
|
|
{
|
|
"id": 41,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f971a-d2bc-7ad3-b499-dfcd42b16132`."
|
|
}
|
|
},
|
|
{
|
|
"id": 42,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f971e-6ab7-7633-85ea-61ca9659448c`."
|
|
}
|
|
},
|
|
{
|
|
"id": 43,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "I\u2019m arming the session mail watcher requested by the stop hook."
|
|
}
|
|
},
|
|
{
|
|
"id": 44,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "// The complete drop-in implementation is provided in the preceding response."
|
|
}
|
|
},
|
|
{
|
|
"id": 45,
|
|
"parent": 0,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9725-e9e1-7da0-892e-9a4f2afeb00f`."
|
|
}
|
|
},
|
|
{
|
|
"id": 46,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9729-0658-77e1-940f-0857ce9f3a8c`."
|
|
}
|
|
},
|
|
{
|
|
"id": 47,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f972c-560e-7261-9a7f-0cd25b2b80f9`."
|
|
}
|
|
},
|
|
{
|
|
"id": 48,
|
|
"parent": 0,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f972f-47da-7543-bad0-d690604dd20a`."
|
|
}
|
|
},
|
|
{
|
|
"id": 49,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9732-50ad-77a0-9e67-75525b87d82b`."
|
|
}
|
|
},
|
|
{
|
|
"id": 50,
|
|
"parent": 6,
|
|
"score": 0.0,
|
|
"accepted": false,
|
|
"frontier": false,
|
|
"params": {
|
|
"source": "Watcher armed for session `019f9735-414b-7ff0-a110-f6fe7c4e91db`."
|
|
}
|
|
}
|
|
]
|
|
} |