Releases: AviaOne/tenderseed
Release list
Tenderseed v3.0.0
This is the first release in which one tenderseed binary serves two p2p stacks.
A seed declares the stack of the chain it serves in config.toml; an absent key
means Cosmos, which is what every version up to v2.2.2 did. On the Cosmos side
this release carries one behaviour change and no other: everything else in the
diff is code moved behind a boundary so that neither stack imports the other.
Added
- A second p2p stack, TM2, the stack of gno.land. The
stackkey takes
cosmosortm2. One binary, one process, one chain, as before. An
unrecognised value is refused at start, where an unrecognised key is only
reported: a misspelled key costs one setting, a misspelled stack would start
the network code of the wrong chain and fail far from its cause. - A TM2 seed answers from its own address book, not from its connections. A
core TM2 node keeps a persistent store of the addresses it has discovered, but
never answers from it: a discovery request is served from the peers it happens
to be connected to at that moment, capped at thirty, whatever the store
holds. This seed answers from what it has reached itself, up to 250
addresses, which is the selection size the Cosmos side of this same binary
already hands out. It holds more than it serves: an address counts as fresh
only while the seed can prove it again inside the freshness window, so what
lies above that ceiling stays in the book, unserved, waiting its turn to be
proven. The Cosmos side already serves a subset of a larger book, so this is
the same arrangement rather than a new one. - A TM2 seed closes every connection that has lasted long enough. Nothing
in the core TM2 stack closes an idle connection, so a seed that never hangs up
fills its own inbound slots and stops being reachable for the next new node.
The delay isseed_disconnect_wait_period, the key the Cosmos side already
uses for the same decision, and it applies to every connection rather than
only to the peers the seed answered: an inbound peer that never asks for
anything holds a slot just as long, and an outbound peer that is never dialled
again can never prove its reachability anew. A peer that asks repeatedly gets
no longer a stay than one that asks once, and a peer the seed had nothing to
answer is closed like any other. addr_book_strictis honoured on TM2. The core skips the routability test
on purpose, to keep loopback addresses usable in local clusters. A public seed
that keeps that behaviour hands out addresses nobody can dial. Filtering
happens on the way in as well as on the way out, so an unroutable address is
not stored, not dialled, and does not come back at the next restart.- Verification state on top of the core's peer file. The core records an
address and the moment it was last mentioned, never whether it answers. The
seed's book adds what verification needs and nothing more: consecutive
failures, last attempt, last success. No buckets, no promotion, no
new-versus-old bias. It keeps the core's file format and its thousand-address
ceiling, the three added fields being ignored by anything that does not know
them, so one file holds one state and either side can read it. - A verification sweep on TM2, on
peer_check_period. Stale addresses are
handed to the switch rather than dialled directly, so a single dialler keeps
the duplicate-IP rule and a single accounting. While it runs it is also the
only thing that hands addresses over at all. The switch reads its outbound
limit once, when an address is handed to it, and never again; the queue it
feeds is neither bounded nor deduplicated; nothing empties it and nothing
reports its depth. Handing over from anywhere else therefore deposited work
into a place with no bottom, where this sweep's own addresses waited behind
it, were marked as tried long before they were dialled, counted as failed,
and dropped although they were alive. So the sweep paces every hand over, and
what is learned in between is kept and dialled on the next pass. A zero
peer_check_periodruns no sweep and gives that pacing up along with the
verification, which is what restoring the upstream behaviour means here.
One pass hands over what
the free outbound slots and the period can really take, oldest news first, and
an attempt is recorded only for an address actually handed over: the switch
silently skips an address it is already connected to and discards whatever
exceeds its outbound limit, and counting either as an attempt would count a
failure against an address that was never tried. An address that fails five
times in a row leaves the book. A success stays good for three periods, so a
sweep may miss once without emptying what the seed can answer. -stack, a top-level flag. It sets the stack whenconfig.tomlis
created. Without it the stack could only be declared in a file that the very
first command creates, after that same command had already generated a node
identity in the other format. Unlike the other top-level flags it does not
override an existing file: it may confirm what the file says and is refused
when it contradicts it, because the stack has a shape on disk and overriding
it silently would leave the key file and the configuration disagreeing.
Changing the stack of an established home is a new home, not a flag.app_version, a TM2-only key. The handshake announces a version set whose
appentry belongs to the chain and not to this binary. It is empty by
default, which is what gno.land announces today.- Counters for the TM2 seed, under the same shape the Cosmos side uses: one
series for the decisions, by outcome and by the stage that took them, and one
for the size of the book and of its servable part. Every reachable pair is
published at zero, so a share can be read from the first scrape. Nothing
upstream reports any of this: the core counts peers, bytes and messages, never
what a seed chose to serve or refuse. - The generated
config.tomlis written to be read. Keys are grouped,
banners separate the groups, and the ones an operator may have to change come
first instead of wherever the alphabet put them. Only a file being created is
concerned: an existing one is never rewritten, and nothing reads a
configuration by position. show-node-idanswers in the format of the configured stack. The two
identities are not interchangeable, and showing one where the other is
expected hands an operator a seed address no peer can dial.- Bounds on what one peer may do to a TM2 seed. Three, none of which exist
in that stack, and all three of which the Cosmos side gets from its own core,
which refuses an address list nobody asked for outright. A list of addresses
is now taken only from a peer this seed asked, one request buying one answer:
an unsolicited answer is the single thing a stranger controls whole, when it
comes, how often, and what it carries. What one answer may add to the book is
capped at what the seed itself serves, the core validating an answer without
ever counting its entries. And a peer that asks again immediately is not
answered twice, a request being ten bytes where an answer is thousands and a
sort of the whole book under lock. Below all three, the receive ceiling of
the discovery channel is set far under the core's: that ceiling is local to
each side of a connection and never compared in the handshake, and it is what
decides how much work a stranger can have this seed assemble, decode and
resolve before any rule of the seed runs at all. Two counters report the
first and the third.
Changed
- An empty
metrics_namespaceis now refused when the metrics endpoint is
enabled. A configuration that setmetrics_listen_addrand left
metrics_namespaceempty used to start and publish series under no name; it
now refuses to start and says why. The refusal belongs to both stacks,
because one configuration key means the same thing on each. - A
max_num_outbound_peersof zero is now refused. The reason is not the
one this note first gave. On the Cosmos stack the key is never read in seed
mode at all, its only upstream reader being a routine a seed does not run, so
zero would have changed nothing there. It is read on gno.land, where it
bounds the sweep and the served set, and a key must not mean two things. - A
max_num_inbound_peersof zero is now refused. Both stacks compare the
connections they hold against this value before accepting one, so zero
accepts nobody: a seed that serves no one while every line it logs looks
normal. - On Cosmos, the number of handshakes carried out at once is now bounded by
max_num_inbound_peers. The core accepts connections without any such
ceiling and compares the inbound limit only once a handshake has completed,
so a peer cost a full key exchange before anything counted it. The core
exposes a ceiling for this and nothing was passing it. An operator sees no
difference below the limit they already set. - On gno.land, a malformed entry in
seedsnow stops the seed at start up,
naming what is wrong. It used to be logged as an error while the seed started
anyway, with one seed fewer than its operator wrote. The Cosmos side already
refused it; both stacks now behave the same. - A moniker or a
chain_idthat is not printable ASCII is now refused.
Both cores refuse a node info whose moniker is not, at the far end of a
handshake, so such a seed saw every connection fail with nothing naming the
cause. It is refused at start up instead, naming the key. The chain
identifier is checked for the same reason: an empty moniker is replaced by
one built from it. - On gno.land, a
peer_check_periodbetween zero and nine seconds is now
refused. Nine seconds is what one dial costs there at worst, so below it
the se...
Tenderseed v2.2.2
This release carries no behaviour change. Every point below is a place where
the code and its documentation had drifted apart, plus two lines that make an
existing invariant structural instead of contractual. The binary does the same
thing on the network as v2.2.1.
Fixed
bench.shreported zero failed checks whatever happened. It counted a
per-address line emitted at debug level, while the default log level is info,
so the figure was structurally zero unless the home under test had been
switched to debug. The verification figures now come from the sweep summary
line, which is emitted at info level, and the script says that a run shorter
than one period yields no such line.:latestonly moves forward. The container workflow pushed:lateston
every version tag, so a patch released on an older line would have overwritten
it with the older build, silently. The tag is now published only when it is
the newest one, and the job says so when it declines.- The outcome counter cannot publish a pair the summary line would not
report. Both sinks were guarded differently: the internal counter by
membership of the outcome list, the Prometheus one by nothing. A future caller
passing an unlisted pair would have created a series no summary line could
ever mention, which is what that line promises is impossible. One guard now
covers both.
Documentation
- The comment anchoring
peer_check_periodstill namedGetSelection, the
very draw that v2.2.1 replaced and documented as the wrong one. It now names
neither, since the ceiling is the same for both, and points at the sweep for
the bias. - FORK.md said an evicted address never comes back through
GetSelection. True,
but named a function the sweep no longer calls: an evicted address is out of
the book, so no selection can return it. skipped_localwas documented as three exits and covers five, the two extra
ones being the defensive nil guards at the top of verify. Both the README
table and FORK.md now say so.
Build
go.sumjoins the build dependencies in the Makefile. A dependency bump that
touches onlygo.sumwould have leftmakebelieving the binary was current.
Updating
Only the binary is replaced. Node identity, configuration and address book are
untouched. No configuration key was added, removed or renamed, and no metric
series changed. See the update procedure in the README.
Tenderseed v2.2.1
This release closes the audit of v2.2.0. Two of its points did not survive a
reading of the pinned upstream source and are recorded below as examined; one it
raised as merely probable turned out to be the most serious of the set. None of
them was a crash, a data loss or a deadlock.
Changed
- The sweep now re-verifies what the seed actually serves. A seed answers a
request for addresses with a selection biased towards the addresses this
reactor promoted. The sweep was drawing an unbiased selection, that is
sampling the whole book uniformly, where promoted addresses are a small
minority. On a book of a thousand entries holding thirty promoted ones, a
promoted address came up in roughly one sweep out of five, and it is precisely
the address served first. Nothing ever demotes a promotion, so that population
is the only one the sweep exists for. The new buckets keep their coverage from
the arrival path, and the bias never shortens the selection: when the old
buckets cannot supply their share, the difference is claimed from the new
ones, so an early book yields a full selection holding all of its promoted
addresses. - The outcome counters gain a stage label, and two outcomes are split. The
stage says whether a decision was taken as an address was offered to the queue
or as it left it, which is what bounds the traffic actually saved: a skip on
the way out is a dial that would certainly have happened, a skip on the way in
only avoided an offer.successno longer covers an address the book refused
to hold, which is nowanswered_unlisted; and a collision observed after a
dial is nowskipped_collisionrather than being folded in with the three
exits that dial nothing, because it is the only one of the four this fork
changed and its value is the count of unfair marks avoided on live addresses.
One series, two labels, ten reachable pairs, still summing to the number of
decisions taken. Anything reading the old outcome names needs updating; this
is the only release in which those series change.
Added
- An unknown configuration key is reported instead of ignored. A misspelled
key was silently dropped and its default applied, which is the likeliest way
an operator loses a setting. It is reported on standard error and never
refused, so an older binary still reads a file written for a newer one. - The queue length at the tick joins the sweep summary line. A queue already
full when the sweep offers its selection means the sweep is the flow being
dropped rather than the arriving one, which is the opposite of what the drop
was meant for. Nothing is changed about the queue until that figure has been
read in production.
Fixed
- The listening socket is released when the metrics counters fail to build.
v2.2.0 fixed the same defect on the path where the switch fails to start and
introduced this one next to it.
Examined, not reproduced
- The duplicate rejection filter was reported as depending on the exact shape
of the upstream error. Searching the pinned source for a pointer to that
type returns nothing outside tests: all three production sites build it by
value, so matching by value covers every existing path. No code change. - Keying the verification state by node identity rather than by full address
was reported as an inconsistency. The documentation was wrong, not the code:
the upstream book is itself keyed by identity throughout, and a verdict
applies to a book entry. Keying on the full address would hold two verdicts
for one entry. FORK.md section 3.6 now says so and says why.
Documentation
- The release notes of v2.2.0 claimed the close error of the configuration file
was checked. It is explicitly discarded, which is the right call for a file
opened to read, but the note said the opposite. - The comment on the outcome counter promised that every path takes exactly one
decision. Shutdown does not: the rest of a batch is abandoned without a
verdict, and nothing should be counted for it. - FORK.md states what earns a counter: one outcome per behaviour whose value can
be interpreted, never one per branch of the code. - The severity gap between an unusable metrics namespace, which refuses to
start, and a metrics port already taken, which is only logged, is stated as
the choice it is: the first value will never become valid, the second can
resolve itself, and shared infrastructure should not stop serving peers over a
metrics port.
Updating
Only the binary is replaced. Node identity, configuration and address book are
untouched. No configuration key was added, removed or renamed. Dashboards and
alerts reading the verification series must be updated, see Changed above. See
the update procedure in the README.
Tenderseed v2.2.0
This release closes the audit of v2.1.1. Every point was checked against the
code before anything was changed, and the two that carried the most weight were
confirmed in their conclusion but wrong in their cause, which changed what was
written. None of them was a crash, a data loss or a deadlock.
Changed
- Verification remembers its verdicts. A failing address is now re-tried on
an exponential schedule, 2^n seconds capped at 4 hours, the same formula the
upstream crawler uses, instead of being re-dialled at every sweep for the 35
hours upstream takes to evict it. On a full book that was on the order of a
thousand futile connections an hour. An address just verified is no longer
dialled again immediately when another peer mentions it. The window during
which a successful verdict is trusted is derived frompeer_check_periodand
stays strictly below it, so the periodic re-verification the seed exists for
is never what gets skipped. No new configuration key. See FORK.md section 3.6. - A dial collision is no longer counted against the address. A peer that
connects to us while we are dialling it is reported as a duplicate rejection
by the transport and by the switch. That was marked as a failed attempt on a
live address, on a counter shared with the upstream crawler. Every other
rejection remains a verdict and is still marked.
Added
- Counters for the verification itself, exported on the existing metrics
endpoint under<namespace>_seed_verify_dials_total, with one label carrying
six outcomes that sum to the number of decisions taken. Nothing upstream
counts connection attempts. They are only registered when
metrics_listen_addris set, and registration failures are reported as
configuration errors rather than raised. - A
verification sweepline at info level, once per period, carrying the
same outcomes, for operators without Prometheus. golangci-lintruns in CI, pinned to a fixed version so an upgrade of
the tool cannot turn the build red on untouched code. Thelinttarget was
declared in the Makefile and executed nowhere.- The three integer limits are validated. A negative
max_num_inbound_peersormax_num_outbound_peers, or a
max_packet_msg_payload_sizeof zero or less, is now refused at startup with
the name of the key instead of being passed to CometBFT as is.
Fixed
- The listening socket is released when the switch fails to start. That
path stopped the metrics server but left the transport listening, unlike both
paths ofStop. - The shutdown signal is trapped after the switch is running. A signal
arriving duringSwitch.StartfoundIsRunningfalse and would have closed
the transport under a switch that was still starting. - The metrics server compares its sentinel error with
errors.Is. - The close error of the configuration file is checked.
makeno longer misses a source file added in a deeper directory:
$(wildcard internal/**/*.go)is not recursive in GNU make, it means*.
Documentation
- The worst case cost of a dial was wrong, and three places quoted it. A
dial costs at most 7 seconds, not 4: one second to connect, then two
consecutive three second handshake deadlines. A sequential sweep of a full
selection takes about 29 minutes, not 17, and about 3m40 with 8 workers, not
2.peer_check_periodis about 2.7 times the duration of one sweep, not five
times. Corrected inconfig.go, FORK.md section 4 and the README. - FORK.md gains section 3.6, which documents the interaction between the sweep
and theAttemptscounter of the address book, and why the sweep never made
an address evictable meaningfully sooner than upstream would have.
Updating
Only the binary is replaced. Node identity, configuration and address book are
untouched. No configuration key was added, removed or renamed. See the update
procedure in the README.
Tenderseed v2.1.1
This release closes the nine points raised by the v2.1.0 audit. None of them was
a crash, a data loss or a deadlock. Every point was checked against the code
before anything was changed, and one of them turned out not to reproduce at all.
Fixed
make buildon a checked out tag now announces that tag.VERSIONwas
empty in the Makefile, so building from av2.1.0checkout produced a binary
announcing2.0.0, and only the CI passed the linker flag. The Makefile
now takes the version from an exact tag on HEAD, and falls back to the value
compiled into the source when HEAD carries no tag.- A manual run of the container or release workflow is skipped unless it was
started from a tag. Started from a branch, the container job tagged the
image with the branch name and overwrote:latestwith a build announcing
no version; the release job would have named a release after the branch and
announced that name as the version. - An unsolicited list of addresses is no longer verified. The upstream
reactor already refuses such a list, stops the sender and bans it for a day.
This fork then verified and promoted the very addresses upstream had just
refused. See FORK.md section 3.5. versionandhelpno longer write to disk. They created the home
directory and aconfig.tomlas a side effect. The configuration is now
loaded only by the commands that need it,startandshow-node-id.- The listening socket is released when the seed stops.
Switch.OnStop
stops the peers and the reactors but never touches the transport. - The godoc comment of
NewSeedwas attached tovar Version. - Two local variables were renamed out of snake_case.
Examined, not reproduced
metrics_namespace was reported as able to panic at startup through
MustRegister. On the pinned dependencies it cannot. prometheus/common applies
UTF-8 name validation by default, so bad-name, bad name, 1bad and an empty
namespace all register without complaint. The only value that does panic, one
holding invalid UTF-8, cannot reach that code from a configuration file: TOML
decoding replaces the offending byte first. No code change.
Documentation
- The address book figures are restated. 1263 and 568 addresses measure the
first hours after an empty start, not what an operator sees afterwards. In
steady state the book holds ten to twenty addresses, nearly all of them
verified reachable, against 4 and 6 addresses of which none were verified for
the upstream binary on the same seeds. Both regimes are now given, together
with the figure anyone can reproduce: what a new node collects from a seed in
90 seconds. FORK.md sections 5.3 and 5.4. - The README no longer pins a released version in its prose or its examples, so
it does not go stale on the next tag. - FORK.md claimed the container workflow was triggered manually. It fires on a
version tag.
Updating
Only the binary is replaced. Node identity, configuration and address book are
untouched, as in v2.1.0. See the update procedure in the README.
Tenderseed v2.1.0
This release closes the fourteen issues left open after the v2.0.0 audit. Every
change was made against the code as it stands, not against the issue text, and
each one was verified before being committed.
Updating from v2.0.0
Only the binary is replaced. This was tested end to end: an installation created
by v2.0.0 was reused by this build, and config/node_key.json and
config/config.toml came back byte for byte identical, the node identity was
unchanged, and the address book held exactly the same entries before and after.
See the update procedure in the README.
New
tenderseed versionprints the version the binary announces to peers.- Release builds derive that version from the Git tag, so a
v2.1.0tag can no
longer produce a binary announcing something else. - Release artefacts are built and published by CI on a version tag, in the same
format as before: one flat archive per architecture plus SHA256SUMS.
Changed behaviour, one item
log_level now applies to the seed's own log lines, not only to the address
book, the switch and the PEX reactor. Under log_level = "none" the startup
banner is still printed, everything else is silent. Under info, the default,
nothing changes. log_level also accepts warn, which the documentation
previously omitted.
log_level is now validated before the banner is printed, so an invalid value
fails immediately with a message on standard error.
Fixed
- The verification queue is no longer filled when
peer_check_periodis zero. - An unspecified
laddrsuch as0.0.0.0is no longer registered as the seed's
own address, since the book compares full address strings and such an entry
could never match. - The metrics server is shut down when the switch fails to start.
- Stopping is a no-op when the switch never started, which closes the window
between the signal trap and startup. errors.Asreplaces a direct type assertion on the dial path.
Cleanup
Config.goandMkDir.goare now lowercase, and theMkdirAllwrapper is
gone in favour ofos.MkdirAll.- The generated
config.tomlwrites5mand10minstead of5m0sand
10m0s, and documents thatpeer_check_workers = 0means the default of 8
rather than disabling anything. Both forms already parsed identically, and an
existing file is never rewritten. - The unused logger parameter of the metrics server is gone.
- FORK.md names the denominator of every ratio in section 5.
Closed without a code change
Setting SeedMode and ListenAddress on the p2p config would change nothing:
in CometBFT v0.40.0 neither is read from P2PConfig. Seed mode reaches the PEX
reactor through pex.ReactorConfig, and listening is done by
Transport.Listen. A comment now records this where the question arises.
Tenderseed v2.0.0
First release of the AviaOne fork of tenderseed, rebuilt on CometBFT v0.40.x.
Correction, added with v2.1.1: the figures below measure the first hours
after an empty start, not steady state. In steady state the book holds ten to
twenty addresses, nearly all of them verified reachable. See the v2.1.1 notes
and FORK.md sections 5.3 and 5.4.
Why this matters. Two seeds running the upstream binary for years held 4 and
6 addresses. After switching to this build, the same two seeds reached 1263 and
568 addresses in four hours, of which 28 and 40 were verified reachable.
Upstream never calls MarkGood, so a seed hands out whatever it was told, alive
or not, and it leaves seed_disconnect_wait_period at zero, so crawled peers are
dropped on the first round. Both are fixed here. See FORK.md for the evidence.
Drop-in replacement. Same binary name, same flags, same home layout, and a
partial config.toml is still valid. Your node_key.json is untouched.
One behaviour change. An absolute -config path is now honoured as given
instead of being joined to the home directory.
Install. Download the archive for your architecture, then:
sudo install -m 0755 tenderseed /usr/local/bin/tenderseed
Verify the download against SHA256SUMS.