lnd/simulation/gen_served_weights.py
Olaoluwa Osuntokun f82f4db410 simulation/lab: exp-016, free knowledge helps the champions and hurts lnd
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.
2026-07-26 23:06:50 -07:00

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()