mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-18 13:07:58 +02:00
The arm exp-012 could never build. A third-party node's observations injected from a file with no payment sent, so the value of routing knowledge is finally separated from the cost of acquiring it. Served the same observations from the same server, on ten sealed hard-tier files: atomic1 +0.055 (CI excludes zero, sign p=0.016), mx_c3 +0.031 with attempts nearly halved at 8.1 -> 4.4, and lnd -0.029 with attempts going UP, 30.9 -> 33.8. Free, accurate, correctly-scoped information makes lnd worse. Splitting the stream says why, and it is the program's central thesis arriving from a new direction. Successes help everyone. Failures split the field: they help the interval routers and they are the whole of lnd's loss at -0.039, CI excluding zero, worse on 9 of 10 files. An interval router files a failure as an AMOUNT BOUND and will still route half that amount tomorrow, so a served failure is pure information. lnd files it as a penalty on the pair, and a penalty is not amount-aware -- it suppresses the corridor for every amount, so a stranger's failure at a stranger's amount steers lnd off corridors that were fine for what it actually wants to send. I expected mission control's collapse of channels onto node pairs to be the culprit and it is not: 761 directed edges, 761 distinct pairs, no parallel channels, nothing collapses. The damage is in how a failure is represented, not in how it is keyed. The champions could not consume anything at all, because nothing in the SimRouter contract ever asked a candidate to accept third-party knowledge. Hence the two importer variants, each its ancestor plus one method that routes every observation through the same belief update a real attempt makes. Both score identically to their originals cold, so the only thing that changed is the capability. Also adds gen_served_weights.py, which builds the server-side scenario files. The server must be a different node than the consumer or the exercise collapses back into self-warming, which exp-012 part 4 already measured as harmful.
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build served-weight files from a THIRD PARTY node's observations.
|
|
|
|
exp-012 could never separate the value of routing knowledge from the cost of
|
|
acquiring it, because every arm it could construct bought its knowledge with
|
|
payments, and payments drain the corridors they teach about. Served weights
|
|
arrive over an API and cost the consumer nothing.
|
|
|
|
This builds that arm. For each scenario file it writes a companion file with
|
|
the same graph, the same liquidity seed and the same payment set but a
|
|
DIFFERENT source node -- a server that has been paying and is willing to
|
|
share what it saw. Running that file exports observations; the consumer then
|
|
imports them without sending a payment of its own.
|
|
|
|
The server must be a different node than the consumer, or the exercise
|
|
collapses back into self-warming: exp-012 part 4 measured that a node warmed
|
|
from its own vantage fills exactly the pairs crossing its own local channels
|
|
with stale claims, and lnd's attempt count tripled as it thrashed around its
|
|
own poisoned first hop.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--tier", required=True,
|
|
help="directory of scenario files")
|
|
parser.add_argument("--out", required=True,
|
|
help="directory for the server-side scenario files")
|
|
parser.add_argument("--server", default=None,
|
|
help="server node reference. Default picks a node "
|
|
"that is neither the consumer nor any target, so "
|
|
"the server's vantage genuinely differs")
|
|
args = parser.parse_args()
|
|
|
|
out = Path(args.out)
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
|
|
for path in sorted(Path(args.tier).glob("*.json")):
|
|
scen = json.loads(path.read_text())
|
|
|
|
server = args.server
|
|
if server is None:
|
|
# Synthetic topologies name nodes by index. Pick one that is
|
|
# neither the consumer nor a target it will pay, so that the
|
|
# server's own local channels -- the ones whose observations
|
|
# must never be served -- do not coincide with the consumer's.
|
|
taken = {str(scen["source"])}
|
|
taken |= {str(s["target"]) for s in scen["scenarios"]}
|
|
num_nodes = scen.get("topology", {}).get("num_nodes", 0)
|
|
# Synthetic node references are 1-based.
|
|
for candidate in range(1, num_nodes + 1):
|
|
if str(candidate) not in taken:
|
|
server = str(candidate)
|
|
break
|
|
|
|
if server is None:
|
|
raise SystemExit(f"no server node available for {path.name}")
|
|
|
|
scen["source"] = server
|
|
(out / path.name).write_text(json.dumps(scen, indent=1))
|
|
|
|
print(f"wrote {len(list(out.glob('*.json')))} server files to {out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|