Build1 distinct publisher3 min readUpdated
A proposed change to py-libp2p's routing table caps peers per /24 inside each bucket. Signed records never addressed eclipse, because withholding data needs no forgery.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A proposed change to py-libp2p's routing table caps peers per /24 inside each bucket. Signed records never addressed eclipse, because withholding data needs no forgery.
Follow any of these and your For You feed starts watching them — no settings page required.
py-libp2p has a pull request, libp2p/py-libp2p#1399, that changes how a peer earns a slot in a Kademlia k-bucket: `KBucket.add_peer` now rejects a candidate when its globally-routable /24 (IPv4) or /48 (IPv6) subnet already holds `MAX_PEERS_PER_SUBNET` peers in that bucket, default 2 [11]. It closes issue #1383, and it matters because the DHT's existing integrity story, signed records, was never the control that stopped an eclipse [11][4][5].
The distinction is worth being precise about. py-libp2p speaks Kademlia, where each node keeps a routing table of other peers bucketed by how far their IDs sit from its own, and lookups walk toward a target by asking successively closer peers [1][2]. Record signing answers "did the owning key produce this value," so forged values are not available to an attacker [4]. An eclipse answers a different question: membership. According to the dev.to write-up, an attacker who occupies enough of the closest-K slots for a target key never forges anything, and instead returns perfectly valid signed records that are never the whole set [3][5]. Withholding has no signature, and neither does silence [5]. The victim stays online and stays connected [3].
So the surface is admission [6]. In vanilla Kademlia a peer gets a slot mostly by being live and having an ID that lands in range, and IDs are cheap enough to grind in bulk, so a Sybil fleet parked in one attacker-controlled subnet satisfies the rule [10]. The resource the attacker would actually have to spend, distinct network positions, was not priced at all [10]. The code comment accompanying the change makes the economic argument explicit: a rented cloud block is typically a /24 rather than a scattering of unrelated addresses [12].
The author drove the fork's real `KBucket.add_peer` at k = 20 with Sybil peers from attacker-controlled /24s, comparing `MAX_PEERS_PER_SUBNET` at 0, the pre-#1399 behaviour, against the default of 2, and describes it as a component-level measurement isolating the admission rule [8]. Before, one rented /24 owns the whole bucket; after, ten genuinely distinct networks are needed for the same result, which is what 20 slots at 2 per subnet arithmetically requires [9][13]. That is a real change in cost, and it is also the honest limit of the claim. It is a measurement of who gets admitted, not of how many lookups an attacker captures, even though the write-up frames the harness question as capture rate [8][17].
There is a separate harness: an eclipse-attack simulation living at `tests/examples/attack_simulation/eclipse_attack/`, which stands up honest `KadDHT` nodes, floods their routing tables and poisons DHT entries from malicious peers, and uses a `RealAttackMetrics` collector to run real lookups and record success rate as the attack takes hold [7]. That is the artifact that could turn the subnet number into an operational one.
Two things to watch. First, whether full bucket capture is the right bar: at 2 peers per subnet, half a bucket costs 5 subnets, so partial capture scales down proportionally [14]. Second, whether the limit is scoped to globally-routable space only, which is what the rule as written says [11], and how relays, NAT and shared egress interact with it. The post was written for DEV's Summer Bug Smash, powered by Sentry [15].
Ranked by verification strength, evidence, and original report placement.
The author built an eclipse-attack simulation inside py-libp2p at tests/examples/attack_simulation/eclipse_attack/, which spins up a network of honest KadDHT nodes, introduces malicious peers that flood the honest nodes' routing tables and poison DHT entries, and uses a RealAttackMetrics collector to run real lookups against the network and record the success rate as the attack takes hold.
The author drove the fork's real KBucket.add_peer (k = 20) with Sybil peers from attacker-controlled /24s, running it with MAX_PEERS_PER_SUBNET set to 0 (the pre-#1399 behaviour) versus its default of 2, and describes this as a component-level measurement that isolates exactly what the admission rule changes.
libp2p/py-libp2p#1399, which closes issue #1383, makes KBucket.add_peer reject a new peer when its globally-routable /24 (IPv4) or /48 (IPv6) subnet already holds MAX_PEERS_PER_SUBNET (default 2) peers in that bucket.
The code comment for the change states that /24 matches realistic attacker economics because a rented cloud block is typically a /24, not a scattering of unrelated addresses.
The write-up states that the point of the harness is to turn "eclipse is possible" into a number: what fraction of lookups an attacker can capture, and how that changes once the routing table stops admitting them so cheaply.
py-libp2p speaks Kademlia, a distributed hash table where every node keeps a routing table of other nodes, bucketed by how far their IDs sit from its own.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable code references, unpublished results
The mechanism claims are unusually specific for a single-source story: a named pull request and issue, the quoted MAX_PEERS_PER_SUBNET constant and _subnet_key implementation, an explicit bucket size (k = 20), and a test harness path. The conceptual core — signing authenticates content, eclipse manipulates membership — is self-evidently sound and needs no external corroboration. What is missing is the empirical half: the RealAttackMetrics lookup-capture rates the harness is built to produce never appear, the component-level experiment reports no result table, and no maintainer, reviewer, or second publisher confirms the change's status. The headline number therefore rests on arithmetic from stated parameters rather than on reported measurement.
One proposed change, no confirmed uptake
Adoption evidence is limited to two artifacts in one codebase: the #1399 admission-rule change and the author's eclipse simulation under tests/examples. Nothing in the source states that #1399 is merged, released, or running on any deployed py-libp2p network, and there is no downstream user, dependent project, or maintainer signal. The change is explicitly described as touching no protocol or wire format, and go-libp2p is cited as already having comparable behaviour, but neither fact evidences uptake of this implementation.
Headline outruns the reported evidence
The analysis itself is measured and hedges appropriately — it labels the experiment component-level and enumerates the change's exemptions and divergences from go-libp2p. The overstatement is in framing: the title's 'Now it takes ten' presents a proposed, status-unknown change as an in-force defence, and the piece promises to turn 'eclipse is possible' into a number without publishing that number, leaving bucket arithmetic to carry the claim. Residual risk is also left unpriced: an attacker with ten distinct subnets, or abundant IPv6 /48s, still qualifies, and 'ten networks' is a modest cost increase rather than elimination.
Contest entry promoting the author's own patch
Two disclosed incentives shape the framing. The post opens by identifying itself as a submission to DEV's Summer Bug Smash: Smash Stories, powered by Sentry, a sponsored contest that rewards compelling bug narratives. Separately, the author is describing their own py-libp2p work — the simulation harness and the admission-rule change — so the piece functions as advocacy for a pending contribution. Neither incentive is hidden and the technical detail is falsifiable, which limits the discount, but both push toward a decisive headline and toward presenting the measurement as more conclusive than the published data supports.
Sound reasoning, single self-interested source
Confidence is bounded by cluster structure rather than by argument quality. There is exactly one publisher and one author, who is also the party proposing the change, and the supplied body is truncated. The threat model and the signing-versus-membership distinction are high-confidence, and the arithmetic consequence of k=20 with a cap of 2 is deterministic. Everything downstream of that — the effectiveness of the rule against real eclipse attempts, the false-rejection cost for legitimate cloud-clustered peers, and whether the change ships — remains unverified in this record.
build
py-libp2p bounds a dev-only /sdp endpoint that believed whatever Content-Length it was told1 distinct publisher
build
A green test suite that proved nothing: aiortc's mangled cert slot versus libp2p certhash pinning1 distinct publisher
build
Four indexes, none of them covering: the 78-second page and the one index that fixed it1 distinct publisher
build
The fence was fine, the test was green, and the injection still worked1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 20, 2026