Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR introduces SWIP-39, a smart neighbourhood management system for decentralized service networks. The proposal aims to solve the "one operator, one node in a neighbourhood" problem through a balanced assignment mechanism that ensures fair load distribution and prevents sybil attacks.
Key changes include:
- A comprehensive specification for balanced neighbourhood registry with random assignment
- Smart contract implementation for managing node registration and neighbourhood assignments
- Mathematical formulations for neighbourhood depth calculation and overlay address validation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Thanks for the well-thought-out SWIP — the design is elegant and clearly addresses Sybil resistance and balanced assignment. I had a few questions and points I’d like to discuss for further clarity and robustness:
|
|
Could _upgradeDepth() become too expensive to execute as the number of assigned nodes grows? The _upgradeDepth() function doubles the assignment and remaining lists, copies all existing nodes to their new positions using bitwise logic, and clears/rebuilds state — all in a single call. If the number of nodes reaches high volumes (e.g. 1,000+ or 10,000+), this could approach or exceed the block gas limit, making the function fail or stall the system. Proposed solutions: |
|
Can the committers array become inefficient with a large number of registrants (e.g. 20k nodes)? The committers[] array is iterated over in _expire(), _findEntryFor(), and _removeCommitter() using for loops. If a large number of nodes register, or if expired entries are not promptly cleared, the gas cost of these operations can grow linearly and become prohibitively expensive. Proposed solutions: |
I did not consider it realistic, since each registrant entry expires in max 256 blocks, that is in a matter of <4 game rounds and they lose their deposit if they refuse to pay, so likely all the potential players may organically wait out.
but they need to be removed at some point.... and I am not sure how a mapping that needs to be reindexed after every entry removed, will solve this. |
well, maybe. To be honest, there is also another way. We do not need to allow, just any length of the committer list. The length represents the queue, and the length of valid entries are the ones in the queue you can skip. This effectively quantifies the tries that you got (effectively mining) but also the realistic probability that that someone will come in and change the neighbourhood you (thought you were) assigned to. If this probability is high (there is a lot of nodes that can submit mined overlays), then it can easily happen, that whenever an assigned neighbourhood is read off, nodes will frontrun. So it would just make sense to limit this skip queue to a fix constant number. But this means that the committers list should effectively have a limited length. Now if we siply reject registrations beyond this limit, then the shorter this length, the harder it is for the same amount of currently aspiring nodes to commit. Now in order to avoid that the registration tx needs to be continuously retried (due to it most likely be frontrun by competing resistrants), we should introduce another proper FIFO queue (that is unlimited but does not need iteration). In this case the validity period starts when you enter the limited queue.
Not sure I get how these structures would be useful: index needs reindexing or keeps inactive entries, head pointer just delays the problem and so does the inactive flag. |
i. the mining step is just offloading computation rather than POW, strategic placement is prevented by random allocation, economic disincentives to be quantified forwith |
agree with this, some discussion around implementing binary trie or similar datastructure which will ensure uniform gas usage while providing for the necessary functionality |
|
very good swip, a few thoughts for discussion and expansion in the doc:
|
If this is a response to whether "depth" and "level" are interchangeable, then I'm afraid I don't understand. To what extent are they interchangeable? What does "has various levels" mean?
Here "the tree" means the tree of all bitstrings (of length <= 256)? What does it mean for a position in the tree to become available? The way the proposal is written suggests that only bitstrings of length Here is my attempt to make sense of this: given a set Note that assigning addresses uniformly at random already has a weak version of this property, which is that if |
|
Some semantics, for a term “Ether address” that is mentioned in SWIP multiple times, its technically incorrect term, needs to be “Ethereum address” as Ether is currency and address doesn’t belong to Ether but to network. |
|
Here is my attempt to make sense of this: given a set S of overlay addresses, each address a has a shortest prefix p ( a ) not shared by any other address in the set. Take the subtree T ( S ) of the tree of all bitstrings spanned by the set of prefixes p ( a ) . Then it makes sense to ask if T ( S ) is balanced as a binary tree. It sounds as though this is the type of "balancing" you might be after. One can then cook up a metric measuring how far T ( S ) is from being balanced and always prefer to assign addresses that reduce this distance.
this is correct i believe. for the second part: yes, but i think it it is too weak and that we must pursue an onboarding/off-boarding queue approach cc: @zelig 👁️ |
this is correct i believe. for the second part: yes, but i think it it is too weak and that we must pursue an onboarding/off-boarding queue approach
|
Refine the SWIP-39 document to clarify the protocol mechanism for balanced neighbourhood registry and node assignment. Enhance sections on architecture, model, and data structure to improve understanding of the system's operational principles.
Refactor SWIP-39 to clarify balanced neighbourhood registry and address assignment process.
Clarified descriptions and corrected typos in the SWIP-39 document regarding Sybil attacks, neighbourhood assignments, and node registration processes. Enhanced explanations of data structures and their roles in maintaining balance and coverage in the network.
Refine language and clarify concepts in SWIP-39, focusing on balanced neighbourhood registry and node assignment processes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… errors Move back to swip-39.md (single-file layout). Display math now sits in its own paragraphs as GitHub requires; also fixes align->aligned, missing row breaks, a stray alignment tab, an unclosed inline $, an undefined \idx macro use, and the <u HTML-tag collision in 0<u. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…feiture; data handover scope note - Abstract: prefix derivation is a read-only calculation, no reservation held by the registry; a taken neighbourhood simply yields a new target - Rebalancing: donor's neighbourhood is taken over by its sister (a balanced removal), donor re-enters the commit queue with blockheight set by the deregister call, and must relocate within the validity window or forfeit its stake; a fresh donor is then drawn - New 'Data handover' subsection scoping content migration out of this SWIP, deferring to upcoming durability guarantees / cold storage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the document with the restructured version: scope/terminology/ non-goals, balance invariant with preservation proofs, domain-separated randomness with rejection sampling and anti-grinding measures, threat model and security analysis, gas analysis, migration plan, and worked examples with trie and traversal diagrams. Adapted to agreed decisions: join is exactly two transactions (register, activate) with the target prefix a read-only computation and no reservation held; donor relocation via sibling takeover, commit-queue re-entry at the deregister blockheight, relocate-or-forfeit with redraw; data handover explicitly out of scope, deferred to durability/cold storage. Implementation notes carry the ICBT traversal table and diagrams, targetPrefix/selectDonor pseudocode, commit-queue expiry logic, ICBT as a container contract used by the registry, and the staking-contract separation (freezing/slashing stays with staking). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Merge Scope and Goals into one 'Scope and goals' chapter with a single terminology section - State machine: rename Idle to Active, add explicit Expired state for a donor that missed its relocation deadline - Drop request IDs: registrations keyed by staking identity (one live registration per identity); seed derived from identity and entropy block alone - Allow any number of concurrent pending departures, each with its own donor and deadline; pending-departure leaves excluded from split candidates - Shorten public API: target, activate, expireReg, deregister, expireDereg, getPrefix, nodeFor - Replace all Bee references with Swarm / swarm node client terminology Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…renderer) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
First: the PR was heavily restructured on 2026-07-27 (two-transaction join, no 1. Why do we need on-chain topology? Because assignment must be litigable. The redistribution game checks, at claim 2. Sorted ring + sparse buckets — formalize and compare. Happy to add a comparison subsection to the SWIP; here is the summary. The layout
Where the ring genuinely wins: iterating a neighbourhood's members in overlay order 3. High-level operations. Now in the SWIP: join = 4. Random/balanced assignment without A and R. Reading "A and R" as the ordinal mapping and the reservation from the old draft: 5. Comparison with compacted binary trie. A compacted (path-compressed) trie saves storage when keys are sparse and clustered 6. Depth transitions. Walked through in the SWIP (§Balance invariant: preservation under 7. Complexity / gas. §Gas and performance analysis: selection O(d) reads, activation/departure O(d) 8. The earlier notes (shrink, queue, gas of updates).
|
Promised in the PR #74 review reply: a comparison subsection in the implementation notes. Sorted ring + sparse buckets rebuilds the ICBT once selection counts are added and hits an O(N) re-keying cliff at depth transitions; path compression buys nothing on a tree that the invariant keeps dense by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@significance Dan — your Aug/Sep comments all predate the 2026-07-27 restructure. Several of them are now addressed in the text: the binary trie you endorsed is in as the ICBT (implicit complete binary trie, with a layout-comparison subsection), de/registration runs through commit queues with bounded expiry, and there is now a proper Terminology section separating these tree depths from storage depth. Still open from your list: activity/liveness tracking (squat-attack eviction), quantified economic disincentives (explicitly deferred to deployment/staking spec), key decoupling, and onboarding adherence proofs. Please re-read the current version and leave a proper GitHub review (approve / request changes with the open points) rather than comments — it would help move this toward Accepted. |
…ts as its two readings Replace the stored splitCount/donorCount pair with a single leafCount n(i). Split and donor counts are complementary within a level (they sum to the depth-d slot count 2^(d-l)) and are read off n(i) and the current d. Spell out counter maintenance as pseudo code: every join, direct departure and donor draw is one ±1 root walk; a completed relocation writes no counter. Show that depth transitions cost no writes, and why the leaf count rather than a split count reduced modulo 2^(d-l) is stored (the residue cannot tell an all-leaves subtree from an all-pairs one). Pending-departure exclusion is applied by rejection at selection, marked (?) for review. Also fix the donor's target: the departing prefix itself, not its sibling, matching the worked example. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The SWIP text still contains an unresolved (?) ambiguity and it contradicts the PR description about appended/generated Solidity material.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Lite
| The implementation SHOULD also generate random valid sequences of joins, direct departures, donor relocations, expiries, and redraws, comparing contract state against a simple off-chain model after each completed transition. | ||
|
|
||
| The exact Solidity ABI, client API paths, economic parameters, and deployment addresses remain to be supplied before this SWIP can advance beyond Draft. |
|
|
||
| The four removal cases map onto these as follows: case 1 clears the root record; cases 2 and 3 are `removeLeaf` of the departing leaf (at depth $d$ and $d+1$ respectively — in both the sibling is an active leaf); case 4 is `removeLeaf` of the drawn donor at draw time, followed by `replaceLeaf` at the departing prefix when the donor activates. So a join, a direct departure, and a donor draw each write one root path of at most $d+2$ counters, and a completed relocation writes none. There is no other write to the tree. | ||
|
|
||
| The eligibility rule that a leaf with a pending departure is not a split candidate is applied at selection, not in the counter: `targetPrefix` treats a descent that lands on a pending leaf as a rejection and re-samples with the next rank drawn from $\rho$. This is exact rejection sampling over the non-pending candidates, and it degrades no worse than an explicit exclusion would — if every candidate is pending, neither yields a target until a relocation completes. **(?)** |

UPDATED AUGUST 2026
First: the PR was heavily restructured on 2026-07-27 (two-transaction join, no
reservation held, identity-keyed registrations, concurrent departures, ICBT as a
separate container contract) — several of the points below are answered in the text
now, so please re-read the current version before diving into the old one.
Point-by-point:
1. Why do we need on-chain topology?
Because assignment must be litigable. The redistribution game checks, at claim
time, that a node plays in the neighbourhood it was assigned — that check has to run
in the contract, against state the contract trusts. Off-chain assignment can be
neither enforced (nothing stops a node self-selecting) nor verified (the contract
has no source of truth to check an overlay against). The same goes for the two
enforcement events: forfeiting an expired registration and slashing a defaulted
donor — both need the assignment state on-chain to be adjudicable. And grind-proof
randomness (stake locked before entropy known, seed from a committed block height)
only means anything if the commit itself is on-chain. What stays off-chain is
everything that can: target-prefix computation is a read-only call, mining is local,
and the join costs exactly two transactions.
2. Sorted ring + sparse buckets — formalize and compare.
Happy to add a comparison subsection to the SWIP; here is the summary. The layout
(doubly-linked list in overlay order +
mapping(prefix at staking depth => [first node, count])) is good at what rings are good at: O(1)-writeinsertion/removal once the position is known, O(1) neighbourhood membership query,
cheap ordered iteration. It is weak exactly where this SWIP lives:
layout either scans buckets — O(2^d) — or maintains hierarchical per-subtree
counts to support O(log N) rank selection. The moment you add the counts
hierarchy (your "buckets hierarchy?" bullet concedes this), you have rebuilt the
ICBT: the trie is the counting structure, with the ring's information implicit
in it.
when
lazy-migration scheme with its own bookkeeping. The ICBT never re-keys:
derived, indexes are stable, a depth transition is zero writes.
(path to root, ~30 slots at a million nodes). Ring: O(1) link writes + O(log N)
anyway for whatever counting structure supports selection. We are comparing
log-vs-log; the constant matters less than the re-keying cliff above.
Where the ring genuinely wins: iterating a neighbourhood's members in overlay order
(we never need this — one node per leaf by construction) and finding the successor
of an arbitrary address (the ICBT does it in O(log N) via
nodeFor, good enough fora view function). So: formal comparison in the SWIP yes, layout change no.
3. High-level operations.
Now in the SWIP: join =
register+activate(§Join protocol), departure =deregister(+ donor redraw path) (§Departure and rebalancing), neighbourhoodqueries =
getPrefix/nodeFor(read-only). Redistribution eligibility is oneprefix check against the assignment record — the staking contract calls
getPrefix(identity)and compares against the claimed neighbourhood. If a specificoperation list is wanted verbatim in the issue's terms, point me at it.
4. Random/balanced assignment without A and R.
Reading "A and R" as the ordinal mapping and the reservation from the old draft:
the restructured version already dropped both. There is no reservation
(target prefix is a read-only computation, nothing locked, activation revalidates
against current state) and no request IDs / ordinal indirection (registrations are
keyed by staking identity; the seed is H(domain ‖ identity ‖ blockhash)). If A and R
meant something else, tell me what and I'll answer that instead.
5. Comparison with compacted binary trie.
A compacted (path-compressed) trie saves storage when keys are sparse and clustered$d$ or $d+1$ to be occupied, i.e. the trie is always complete to
— but our key population is dense by construction: the invariant forces every
prefix at depth
within one level. There is nothing to compact — path compression on a complete tree
adds skip-pointers that must be maintained on every split/collapse and saves zero
levels. The implicit heap layout additionally removes all pointer storage: parent,
children, sibling are arithmetic on the index, so a "node" is just its counter
slots. Compaction pays off for arbitrary key sets; balanced assignment is precisely
the regime where it cannot. Will add this as a paragraph to the comparison
subsection.
6. Depth transitions.
Walked through in the SWIP (§Balance invariant: preservation under$N=2^D$ boundary cases for splitCount/donorCount).$d$ , no$2^{d+1}-N$ and $N-2^d$ and both$N=2^D$ example would
insertion/removal; §Counting: the
The short version: transitions are emergent, not an event — no stored
migration, the counters at the root already equal
hit the boundary values exactly at powers of two. If a worked
help, I can add one next to the existing worked examples.
7. Complexity / gas.
§Gas and performance analysis: selection O(d) reads, activation/departure O(d)$2^\ell$ hashes, $\ell \approx \log_2 N + 1$ . Benchmarks are
writes, storage O(N), registration/expiry O(1) amortized (monotonic queue head,
bounded per call — no unbounded iteration anywhere). Mining is the real cost and it
is off-chain: expected
listed as a reference-implementation deliverable; concrete numbers per depth once
there is a contract to measure.
8. The earlier notes (shrink, queue, gas of updates).
completes immediately; the donor path holds the departing node active until a
donor lands, with forfeiture + redraw on default. Withdrawal of stake itself is
the staking contract's business (strict separation in the SWIP).
block heights, head-advancing
expirebounded per call.point 2 for why the proposed alternative doesn't beat it once selection is
accounted for.
this comment was added here #74 (comment)