From 6a3f927413d4fd44b708a306e5e053f6660ec357 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 13 Sep 2026 16:06:57 +0200 Subject: fix(node): a transfer id names a lease, or it names nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_do_file_request` read `tr` as a boolean. Present meant "this is a leased transfer, skip the leaseless ceiling", and nothing asked whether this node had ever granted such a lease — `slots.touch(tr)` was called beside it and its answer, `False` if it is not granted, was discarded. So any non-empty string bought the whole library with no ceiling of any kind: not the per-member cap, not the node-wide one, not the leaseless bound that exists to bound a client claiming to be browsing. The queue held only the clients that chose to wait. `_lease_of` decides it now, and the three answers differ on purpose: - **granted**, and of *this* session — served, and touched so the sweeper does not reclaim a transfer that is plainly moving. The session is checked as well as the id, because touching another connection's lease refreshed its idle timer. - **queued** — refused with `lease_not_granted`, on the upload path too, before anything reaches the operator's disk. A member reading while queued is the cap not applying. - **unknown** — bounded by the leaseless ceiling rather than refused. That is also what a reconnect looks like from here, where the session's leases died with the old connection and the client is re-opening them, and it leaves the residual §5.5 already states: a client that lies gets that bound's worth of files at a time, not the group. Noted once per connection so the residual is visible rather than merely documented. Nothing changes for the shipped client: the transfer store awaits `lease.acquire()` before it reads a byte, so the refused case is one it never enters. §5.5 gains a paragraph saying the node decides which of the two a request is — the document described the accounting without ever saying it was enforced, which is how it came not to be. `test_lease_enforcement.py` drives the real handlers over a real index; six of its nine cases fail against the previous source, each on the property. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW --- docs/MESHBAY_DESIGN.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'docs') diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 07e208b..9f045c4 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1021,6 +1021,18 @@ The rule stated as a sentence someone can check by reading: > there, it does not take a slot.** The two sets are the same three call sites, > which is what makes this verifiable rather than a judgement call at each new one. +**Which of the two a request is, is the node's answer and not the client's.** +`tr` is drawn by the client, so it is a claim: the node resolves it against its +own record and serves the chunk as a leased transfer only when that record is a +**granted** lease **of this connection**. A lease it holds but has not granted is +refused with a code — a member reading while queued is the cap not applying — and +an id it has no record of is treated as leaseless and bounded below, because that +is also what a reconnect looks like from here, where the session's leases died +with the old connection and the client is re-opening them. Read as a bare +presence check, the field made every cap on this page decoration: any non-empty +string skipped the ceiling, and the queue held only the clients that chose to +wait. + **Why the exemption is expressed in concurrency and not in size or bytes**, which is the durable part of this decision: -- cgit v1.2.3