Skip to content

Releases: AviaOne/tenderseed

Tenderseed v3.0.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 13:10

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 stack key takes
    cosmos or tm2. 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 is seed_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_strict is 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_period runs 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 when config.toml is
    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
    app entry 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.toml is 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-id answers 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_namespace is now refused when the metrics endpoint is
    enabled.
    A configuration that set metrics_listen_addr and left
    metrics_namespace empty 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_peers of 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_peers of 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 seeds now 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_id that 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_period between zero and nine seconds is now
    refused.
    Nine seconds is what one dial costs there at worst, so below it
    the se...
Read more

Tenderseed v2.2.2

Choose a tag to compare

@github-actions github-actions released this 01 Sep 12:03

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.sh reported 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.
  • :latest only moves forward. The container workflow pushed :latest on
    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_period still named GetSelection, 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_local was 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.sum joins the build dependencies in the Makefile. A dependency bump that
    touches only go.sum would have left make believing 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

Choose a tag to compare

@github-actions github-actions released this 01 Sep 11:17

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. success no longer covers an address the book refused
    to hold, which is now answered_unlisted; and a collision observed after a
    dial is now skipped_collision rather 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

Choose a tag to compare

@github-actions github-actions released this 01 Sep 09:59

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 from peer_check_period and
    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_addr is set, and registration failures are reported as
    configuration errors rather than raised.
  • A verification sweep line at info level, once per period, carrying the
    same outcomes, for operators without Prometheus.
  • golangci-lint runs in CI, pinned to a fixed version so an upgrade of
    the tool cannot turn the build red on untouched code. The lint target was
    declared in the Makefile and executed nowhere.
  • The three integer limits are validated. A negative
    max_num_inbound_peers or max_num_outbound_peers, or a
    max_packet_msg_payload_size of 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 of Stop.
  • The shutdown signal is trapped after the switch is running. A signal
    arriving during Switch.Start found IsRunning false 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.
  • make no 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_period is about 2.7 times the duration of one sweep, not five
    times. Corrected in config.go, FORK.md section 4 and the README.
  • FORK.md gains section 3.6, which documents the interaction between the sweep
    and the Attempts counter 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

Choose a tag to compare

@github-actions github-actions released this 01 Sep 01:32

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 build on a checked out tag now announces that tag. VERSION was
    empty in the Makefile, so building from a v2.1.0 checkout produced a binary
    announcing 2.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 :latest with 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.
  • version and help no longer write to disk. They created the home
    directory and a config.toml as a side effect. The configuration is now
    loaded only by the commands that need it, start and show-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 NewSeed was attached to var 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

Choose a tag to compare

@github-actions github-actions released this 23 Aug 11:46

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 version prints the version the binary announces to peers.
  • Release builds derive that version from the Git tag, so a v2.1.0 tag 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_period is zero.
  • An unspecified laddr such as 0.0.0.0 is 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.As replaces a direct type assertion on the dial path.

Cleanup

  • Config.go and MkDir.go are now lowercase, and the MkdirAll wrapper is
    gone in favour of os.MkdirAll.
  • The generated config.toml writes 5m and 10m instead of 5m0s and
    10m0s, and documents that peer_check_workers = 0 means 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

Choose a tag to compare

@AviaOne AviaOne released this 21 Aug 14:14

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.