Skip to content

docs(blockchain): simplify Blend connectivity maintenance and derive its thresholds - #421

Draft
madxor wants to merge 8 commits into
masterfrom
docs/blend-connection-monitoring-revision
Draft

docs(blockchain): simplify Blend connectivity maintenance and derive its thresholds#421
madxor wants to merge 8 commits into
masterfrom
docs/blend-connection-monitoring-revision

Conversation

@madxor

@madxor madxor commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Revises the connection monitoring of the Blend protocol. Touches
docs/blockchain/raw/blend-protocol.md only; revision 1.3.0.

Why

The thresholds that drive Connectivity Maintenance were expressed through the
per-round release estimator μ, which conflated a frequency with a count. μ
collapses to 1 for any N ≥ 10, so the maximum became 30 messages per
window while an honest connection carries about 93, and the minimum landed on
top of the expected value. Read as counts, every honest neighbour is spammy; read
as frequencies, roughly half of them are unhealthy. Either way the pair could not
be implemented as written.

Separately the rule list mixed measurement, classification and reaction across
ten numbered rules with nested cases, two persistent markings that had to be set
and cleared, a pause/resume mechanism, and per-neighbour identifier bookkeeping.

What changed

Thresholds are derived from traffic. A new Expected Connection Traffic
section derives F_1, the messages a connection is expected to carry per round,
from the generation rates, the redundancy parameters and the blending operations
— every connection carries, in expectation, every message the network emits. The
maximum relaxes that by a divergence controller κ_max sized against the two
effects that compound above it: connection-level duplication (×2) and the
bootstrapping rise of F_D toward F_C (×1.94). Their product, 3.87, is
named as a floor and explicitly not a usable value — at 3.87 the threshold sits
on the mean and half of all honest connections cross it. The minimum is an order
of magnitude below expectation, scales with W, and carries no penalty.

The logic is restated around two principles. A ban requires attribution
a neighbour is disconnected and blacklisted only for what its authenticated
identity is responsible for, never for silence or low traffic, which an adversary
can induce without controlling either end of the connection. The connection cap
is hard
, handshakes in progress included. Classification then follows the moving
window, so a connection recovers with no marking to remove, and the ten rules
collapse into measurement, classification and a flat reaction table.

Relaying verifies the header, PoQ included, before releasing. The duplicate
check runs first, so a neighbour cannot force repeated proof verifications by
resending an identifier already seen. Only nullifiers of verified messages are
recorded — recording unverified ones would let a forged header carrying a
genuine nullifier suppress the real message network-wide.

R_C, R_D and T_E are given values; the redundancy parameters are carried
into the nullifier cache estimate, which had silently assumed them to be zero.

Draft because

κ_max = 6 is provisional. Every counted message now costs a public header
verification, so the ceiling on κ_max is the rate at which the slowest targeted
hardware can verify headers, and the spec says so. Measurement is pending on a
Raspberry Pi 5 using the wrapper on chore/blend-header-verification-throughput
in logos-blockchain, which converts the verify_public_header benchmark into
verifications per second and reports the implied κ_max ceiling directly.

Three design questions are deliberately left open and are not addressed here:

  • Slot policy. Nothing reclaims a silent connection while the healthy degree
    is at or above the minimum, so an adversary can park in spare slots for a whole
    epoch; the eviction rule selects the least active connection, which is the same
    unattributable signal the first principle forbids acting on; and the cap counts
    handshakes before the peer's type is known.
  • Duplicate identifiers. The per-neighbour ban is removed on the grounds that
    duplicates count toward the maximum, so flooding trips the attributable spam
    bound. That leaves a single neighbour free to replay one identifier repeatedly
    while staying under the threshold.
  • Double verification. Processing still verifies the proof of quota that
    Relaying has now already checked.

Validation

markdownlint-cli2 and remark clean on the changed file; metadata and
generated-output validators pass. Four pre-existing inline-math escapes elsewhere
in the file are untouched — validate_rendering.py is not part of CI.

🤖 Generated with Claude Code

madxor and others added 8 commits August 27, 2026 11:02
…its thresholds

The connection monitoring thresholds were expressed in terms of the per-round
release estimator, which conflated a frequency with a count: the maximum was
below the traffic an honest connection actually carries, and the minimum sat on
top of it. Both are now derived from the traffic a connection is expected to
carry, and the maintenance logic is restated around two principles instead of a
list of rules that mixed measurement, classification and reaction.

Thresholds. F_1 gives the messages a connection is expected to carry per round,
from the generation rates, the redundancy parameters and the blending
operations, since every connection carries in expectation every message the
network emits. The maximum relaxes that by a divergence controller sized to the
two effects that compound above it, connection-level duplication and the
bootstrapping rise of F_D toward F_C; their product is a floor on the controller
and not a usable value for it. The minimum is set an order of magnitude below
expectation and carries no penalty, because a tighter one is not safe: the
traffic a connection carries depends on the topology and on the position of the
node within it.

Logic. A ban now requires attribution: a neighbor is disconnected and
blacklisted only for what its authenticated identity is responsible for, never
for silence or for traffic below the minimum, which an adversary can induce
without controlling either end of the connection. The connection cap is hard and
includes handshakes in progress. Classification follows the moving window, so a
connection recovers without a marking to remove.

Relaying verifies the public header, proof of quota included, before releasing a
message, and the duplicate check runs first so a neighbor cannot force repeated
proof verifications by resending a known identifier. Only nullifiers of verified
messages are recorded, otherwise a forged header could suppress the genuine
message carrying the same nullifier.

R_C, R_D and T_E are given values, and the redundancy parameters are carried
into the nullifier cache estimate that had silently assumed them to be zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y, not by connection

Three gaps found reviewing the previous revision.

The edge path was unmetered. Header verification now includes the proof of
quota and happens before a message is relayed, but edge messages were neither
counted nor rate limited, and a failed verification only closed the connection.
With the specification's own example of 300 concurrent edge connections, each
sending one message before closing, an adversary could impose hundreds of
proof verifications per second at no cost and without ever being attributable.
A core node now accepts at most a fixed number of edge connections per round.
The limit deliberately ignores identity: an edge node is not in the set
returned by the SDP protocol and can present a fresh identity per connection,
so only a rate that ignores identity bounds the work. The identity is
blacklisted on a failed verification as well, which an adversary evades by
rotating, but which is cheap and stops one that does not. The verification
budget both limits imply is now stated, so it can be checked against measured
hardware.

The observation window was tied to the connection, which the neighbor
controls. A neighbor could disconnect and reconnect just before each window
completed, and so never accumulate a full window: never classified unhealthy,
never eligible for replacement, holding a connection slot indefinitely against
a hard cap. The count and the window now accumulate against the authenticated
identity for the epoch, so a reconnecting neighbor resumes where it left off
and the grace given to a new neighbor is granted once per identity per epoch.
The retained set is bounded by the core nodes of the epoch.

A failure of the transport was listed as grounds for a ban without saying what
one is. It now means a TLS record that fails authentication or a violation of
the framing of the stream, and explicitly not the loss of the connection,
which any network in between can cause and which is treated as silence.

Also: the spread of honest bootstrapping traffic was computed as a Poisson
count, but duplication delivers each message twice rather than generating
independent arrivals, so the standard deviation is 27 rather than 19 and the
maximum sits about 7 deviations above the mean, not 10; the minimum is
justified against that spread instead of being asserted; the relay rule now
says a message is not returned to the neighbor it came from, which the traffic
estimate depends on; and that estimate is stated as the upper bound it is.
…retire the edge ceiling

Follow-up to the previous revision, from a second reading of it.

Binding the observation window to the neighbor's identity left unsaid what
happens to the rounds it was not connected for. Read as a plain trailing
window, a neighbor returning from a transient network failure is immediately
unhealthy, and so eligible for replacement, which is the opposite of treating a
dropped connection as silence rather than misbehavior. The window now advances
only while the neighbor is connected: absent rounds are not observations, are
not counted as silence, and do not age earlier messages out.

The edge connection ceiling and the acceptance rate governed the same thing,
and the rate made the ceiling unreachable: with a limit of eight acceptances
per round and connections lasting a single round, the ceiling of three hundred
could never bind. The ceiling is gone and the concurrency it used to express is
now implied by the rate and the timeout.

The acceptance limit said it applied to connections accepted per round, which
no node can do: core and edge neighbors are told apart by the Neighbor
Distinction Process, which reads the authenticated identity once the connection
is established. The limit is now stated where it can be applied, matching what
the implementation already does.

The blacklist had grown two policies under one name. They are now two
structures, because the identities they hold are different in kind: the
blacklist holds core nodes, named by the SDP set, which is bounded and costly
to join, so entries are long lived and need no ceiling; the edge quarantine
holds ephemeral identities that are free to replace, so entries expire with the
epoch and the structure has a fixed size. Healthiness is likewise defined only
for core-to-core connections now, since those are the only ones classified.
… give the quarantine an effect

Replacement fired only when the healthy peering degree had fallen below the
minimum, which left the slots above the minimum unreclaimable. An adversary
holding every slot between the minimum and the maximum delivered nothing, was
never banned because silence is not attributable, and was never displaced
because the node was not short of healthy connections. Replacement now fires
when the node is at the cap and any connection is unhealthy, and only unhealthy
connections are eligible, so a healthy one is never displaced. The distinction
this rests on is now explicit in the rule above it: an unhealthy connection is
never closed as a penalty, but reclaiming the slot it holds is not a penalty.

The edge quarantine was given no effect. An identity was added to it and
nothing said what that caused; it now refuses further connections from that
identity, and such a refusal does not consume the acceptance budget that
bounds verification work.

Also: the trade the acceptance limit makes is stated, since it converts an
unbounded exhaustion of a node's ability to verify into a bounded denial of
its edge service, which is why an edge node sends to several core nodes; the
work of establishing a connection is called out as not bounded by that limit,
being far below the cost of a proof verification; the count of connections
with edge nodes is dropped from the notation, having lost its last user with
the ceiling it belonged to; and the revision history now describes what this
revision does rather than what its first draft did.
…o itself

The maximum was a statistical threshold over an observation window, sized so
that honest traffic would rarely reach it. That framing was wrong, and the
arithmetic it forced showed why: with a threshold placed a few standard
deviations above the mean, a node evicts and blacklists honest neighbors at
whatever rate the tail of the distribution delivers them, and the margin needed
to make that rare collided with what a Raspberry Pi 5 can verify.

A limit that is part of the protocol configuration is not a threshold to be
inferred. Every node knows it, so an honest node holds back anything above it
rather than sending it, and a count above it is a node declining to comply. It
is established by the round in which it happens, exactly as a message whose
header fails to verify is, and needs neither a window to confirm it nor a
margin to excuse it. The blacklist rests on that footing rather than on a tail
probability.

The limit is per neighbor per round, and the budget of a node is the limit
times the peering degree plus one: a share for each neighbor it owes traffic
to, and one for the edge nodes it serves. Edge messages are block proposals
already counted in F_D, so they add nothing to what the network carries; the
share exists because a node that is the entry point for several in one round
would otherwise spend a neighbor's allowance on them. This is also why the
peering degree is now fixed by the protocol rather than chosen by the operator:
a node judged against a limit derived from its degree could otherwise widen
that limit by declaring a larger one.

The value is taken from the traffic and checked against the hardware, not the
other way round. Against the 3.1 messages a connection carries per round, a
limit of 8 defers in about one round in two hundred, where 6 defers in one in
twenty-six; deferral is paid for in the latency of the proposals being carried.
The resulting budget of 56 per round is 56 verifications per second, about a
third of the 157 per second a Raspberry Pi 5 sustains on one core. Deriving the
limit from that capacity instead would put it near 22 per neighbor and let an
adversary saturate a core while staying within the contract.

Releasing now states the send-side obligation, including that a deferred
message is released later than the delaying logic chose for it, which is the
one case where that section's upper bound on delay does not hold. The minimum
keeps its observation window, silence not being observable in a single round.
…e what it requires of consensus

The limit was chosen against steady-state traffic, where a connection carries
3.1 messages per round. While the total stake cannot be reliably inferred it
carries about 6, and at a limit of 8 that defers in 22% of rounds and by up to
three rounds, each added to a block proposal at every hop it takes. Raising the
limit to 12 defers in 0.9% of them and never by more than one round.

Deferral is a delay and not a breach, so the value trades latency against
verification work rather than against any risk of a false accusation. The
resulting budget of 84 per round is 84 verifications per second, which is 54%
of one Raspberry Pi 5 core or 13% of its four; a node may spend more than one
core on the work when the traffic warrants it, and in any case the budget is a
ceiling that adversarial traffic could reach, not a load — the work actually
performed is about 19 verifications per second in the steady state and 36 while
bootstrapping.

The limit bounds what a node sends, not what the network produces, and the
difference accumulates in the sender's queue. That queue is stable only while
(F_C + F_D) * beta_max stays below the limit, which is now stated as a
requirement Blend places on Cryptarchia: the number of proposals admitted per
slot must be bounded, or the limit must cover the worst case consensus allows.
Beyond that point the queue diverges at every node at once, since every
connection carries the same traffic, and the approach is steep rather than
gradual. The requirement cannot be met by treating proposals differently from
cover messages, the two being indistinguishable on the wire.

It binds hardest while bootstrapping, and the reason is worth recording: the
traffic follows the number of proposals, but the total stake inference counts
occupied slots and the lottery activates a slot however many leaders win it, so
the estimate does not respond to the quantity that drives the traffic. The
overshoot that costs Blend the most is the one the correction is least
sensitive to.
…n as a floor

The rate was cited with the CPU governor named, on the assumption that
'ondemand' understated the hardware and that a run at 'performance' would
raise it. It does not: the two agree to within run-to-run noise, 157 per
second on one core either way, because verification is a continuous load and
the governor holds the clock at its maximum in both cases. Governors matter
for bursty work, which this is not.

The figure is therefore the sustained rate of the hardware and not a floor,
and the caveat is replaced by what was actually established: 6.4 ms per
verification, 157 per second on one core, 625 across four, scaling at 3.99x,
which also shows the work to be bound by computation rather than memory.

No parameter changes. The budget of 84 verifications per second remains 54%
of one core and 13% of the board.
The traffic model added the cover and data rates. It should not: a generated
data message removes an unreleased cover message from the release schedule, so
that a node which wins a leadership emits no more than one which does not. Its
emission rate is what hides the win, and adding a proposal to the schedule
rather than substituting it into the schedule would not hide it. The rate a
connection carries is governed by the larger of the two, not their sum.

The correction runs through the derivation. A connection carries 3.0 messages
per round rather than 3.1, and 90 per observation window rather than 93; the
minimum falls to 9; a node carries 18 per round from its neighbors; and the
nullifier cache needs 63 MB rather than 65, that estimate having made the same
addition since before this revision.

It also removes the reason the per-round limit was raised to 12. That was
sized against a bootstrapping regime in which a connection was thought to carry
6 messages per round, which does not occur: while the proposal rate stays below
the cover rate the traffic does not grow at all, since each additional proposal
displaces a cover message that would have been sent anyway. A connection
carries 3 messages per round whether one slot in thirty has a leader or every
slot does, and deferral is negligible at 8 as well as at 12.

The value is kept at 12, on the ground that now decides it: the traffic grows
only once the cover schedule is exhausted, and 12 tolerates a proposal rate of
four times the cover rate where 8 tolerates 2.7 times it. The quantity being
hedged against is bounded by nothing this specification controls, and the
verification work is a small fraction of what the target hardware sustains.

The dependency on consensus is restated accordingly. The tolerance up to the
cover rate is a consequence of the same indistinguishability the protocol
exists to provide, and beyond that point the queue diverges steeply and at
every node at once — a mean queue of two messages at three and a half times the
cover rate, of eighteen at 3.9, and unbounded at 4.2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant