Skip to content

feat(addressbook): prune stale entries by last-seen time - #5513

Merged
martinconic merged 7 commits into
masterfrom
feat/addressbook-pruning
Jul 23, 2026
Merged

feat(addressbook): prune stale entries by last-seen time#5513
martinconic merged 7 commits into
masterfrom
feat/addressbook-pruning

Conversation

@martinconic

@martinconic martinconic commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have filled out the description and linked the related issues.

Description

Implements address book pruning (#5491).

The address book recently gained a wrapping verifiedAddress{Address, Verified} struct (v2.8.0, #5477). This PR extends it with a last-seen timestamp and uses it to drop peers we have not seen for over a month, preventing the address book from accumulating stale, unreachable peers indefinitely.

Changes

  • pkg/addressbook
    • Add LastSeen (unix seconds) to verifiedAddress.
    • Put stamps LastSeen with the current time on every write (initial insertion and any record update).
    • New Seen(overlays ...swarm.Address): read-modify-write that bumps only the timestamp. Overlays that are absent are skipped, and the write is throttled: an overlay already seen within the last 24h is left untouched, so per-sighting calls don't amplify disk IO (on mainnet, sightings would otherwise mean tens of thousands of redundant writes a day).
    • Put and Seen are serialized by a mutex, so a concurrent Put is never rolled back by Seen's stale read.
    • Entries last seen more than 30 days ago are pruned when the store is opened, inside the constructor, before the store is shared with any writer. Entries with LastSeen == 0 are kept for a later run. Best-effort: a pruning failure never blocks node startup.
    • Inject a clock for testability.
  • pkg/hive: hearing about a peer we already know over gossip is a sighting in its own right, so Seen is called for every known peer before timestamp validation, whether or not the re-presented record is newer than the one we hold. Peers mint their bzz.Address once and re-gossip it unchanged for their whole uptime, so this is the path that keeps gossip-only peers fresh; the existing Put only fires when the record is genuinely newer. Failures are debug-logged and non-fatal.
  • pkg/topology/kademlia: connected peers are "constantly seen" without generating gossip, so the manage loop marks every currently connected peer as seen every 15 minutes. Failures are warning-logged and non-fatal.
  • pkg/statestore/storeadapter: migration step 10 stamps last_seen = now() onto existing entries that lack it, decoding into the current entry type (whose last_seen is omitempty) so all other fields round-trip unchanged. Without this, the first prune after upgrade would wipe the whole address book.

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

@martinconic martinconic self-assigned this Jun 22, 2026
@martinconic
martinconic force-pushed the feat/addressbook-pruning branch from 155323e to 1716cdb Compare June 23, 2026 07:49
@martinconic
martinconic marked this pull request as ready for review June 23, 2026 07:53
@gacevicljubisa

gacevicljubisa commented Jun 24, 2026

Copy link
Copy Markdown
Member

Here, prune here runs once, at startup only, and it can lead that if we have some stable connections over bigger period of time, where we haven't updated LastSeen, it will remove those on next restart. But those are our most valuable connections.

Maybe we could use manage() method in kademlia, where we could trigger prune addressbook every 24h (those older then 30 days), but excluding all connected peers, and prune should not start before warmup is done (when node is started). Also, we could stamp last contact at the disconnect, so the prune clock starts from when the peer was actually lost.

One gap is left, what do to with the peer we only hear over hive gossip, but we didn't connect to?

Comment thread pkg/statestore/storeadapter/migration.go Outdated
Comment thread pkg/topology/kademlia/kademlia.go Outdated
Comment thread pkg/topology/kademlia/kademlia.go Outdated
@acud

acud commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Here, prune here runs once, at startup only, and it can lead that if we have some stable connections over bigger period of time, where we haven't updated LastSeen, it will remove those on next restart. But those are our most valuable connections.

Maybe we could use manage() method in kademlia, where we could trigger prune addressbook every 24h (those older then 30 days), but excluding all connected peers, and prune should not start before warmup is done (when node is started). Also, we could stamp last contact at the disconnect, so the prune clock starts from when the peer was actually lost.

This is what we've converged to with @janos as a naive starting point to ship.

One gap is left, what do to with the peer we only hear over hive gossip, but we didn't connect to?

For now let's leave them in. The first line of defense is to get rid of garbage which is in the statestore. A peer which is seen over hive and is not connected to is still a potential peer you'd like to connect to. I agree that over time yes, probably you don't wanna keep those. I suggest to ship this first and see it works, then evolve into more well defined scenarios.

@martinconic

martinconic commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Added two fixes:

  1. Hive never refreshes LastSeen. Peers mint their bzz.Address once and re-gossip it unchanged, so CheckTimestamp rejects it as not-newer and Put never fires again — LastSeen freezes at first-seen, and gossip-only peers get pruned at 30 days. Fix: call UpdateLastSeen on ErrTimestampStale/ErrTimestampTooSoon (both imply a known peer). Other rejections don't refresh. Safe against reviving dead peers, since hive only gossips currently-connected peers.
  2. UpdateLastSeen is a lost update, and fix 1 makes it reachable. Get→mutate→Put with no lock; hive runs a goroutine per gossip message, so it now races its own Put for the same overlay. Worst case: stale write-back desyncs the addressbook from the chequebook registry (swap-on only) until re-mint or restart. Fix: mutex on the store, taken by Put and UpdateLastSeen. Regression test fails without it.

@martinconic
martinconic requested a review from acud July 13, 2026 10:55
Comment thread pkg/addressbook/addressbook.go Outdated
Comment thread pkg/addressbook/addressbook.go Outdated

// mu serializes the read-modify-write in UpdateLastSeen against Put, so a
// concurrent Put is not rolled back by a stale copy of the entry.
mu sync.Mutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't see why this is needed. this is not business critical information and i think it is fine with this hypothetical use case happening every now and then, if ever.

Comment thread pkg/addressbook/addressbook.go
Comment thread pkg/hive/hive.go Outdated
Comment thread pkg/hive/lastseen_test.go Outdated
Comment thread pkg/node/node.go Outdated

// Prune addressbook entries whose overlays have not been seen recently, so
// the address book does not accumulate stale peers indefinitely.
if err := addressbook.Prune(time.Now().Add(-addressbookPruneAfter)); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you prune only on startup - there's no need to expose the method over an interface - just make sure that addressbook ctor runs the pruning internally.

Comment thread pkg/topology/kademlia/kademlia.go Outdated
@martinconic
martinconic requested a review from acud July 15, 2026 15:38
Comment thread pkg/hive/hive.go
continue
}

// Hearing about a peer we already know is a sighting in its own right,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems Seen() now fires even when the incoming record is later rejected for being in the future (ErrTimestampInFuture) or invalid (ErrTimestampInvalid). These aren't meaningful sightings — a bogus record from a third party shouldn't refresh a legitimate peer's last-seen. The old placement after CheckTimestamp, gated on ErrTimestampStale || ErrTimestampTooSoon, was more precise.

Was moving it before CheckTimestamp intentional?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was part of the addressing of my previous comments. The address still passes some validation until we reach this point. And it could be that the new address was minted and sent not out of malicious behavior. I am fine whatever you two decide about this - have it before or after does not matter from my side, originally I think it was inside the error check after the CheckTimestamp and I wanted to avoid a situation where the errors dictate whether the peer gets marked seen yes/no. Before or after is better from my perspective, I don't really mind which one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(left side here)

@acud acud left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minus the comment about exporting Prune (which should anyway be a method, not a pure function), this looks good from my side

Comment thread pkg/addressbook/addressbook.go Outdated
…uning

Resolved a conflict in pkg/addressbook/addressbook_test.go: master's #5534
added mockCorruptedStore and TestGetCorruptedNilAddress next to this branch's
Seen and prune tests. The two additions are independent, so both are kept.
@martinconic
martinconic requested a review from sbackend123 July 17, 2026 10:43

@gacevicljubisa gacevicljubisa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work.

@acud suggested not have 24h update check, so at least in that case, please remove this information from the PR desc before merge.

@gacevicljubisa

Copy link
Copy Markdown
Member

Nice work.

@acud suggested not have 24h update check, so at least in that case, please remove this information from the PR desc before merge.

Additionally I would still suggest 24h throtle to have it, becuse we have noticed increased disk writes when we we wrote TImestamp on every handshake... If my query is good, for this mainnet node, we would have more then 90k of seen events in a day, 99% redudant.

@acud

acud commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Nice work.
@acud suggested not have 24h update check, so at least in that case, please remove this information from the PR desc before merge.

Additionally I would still suggest 24h throtle to have it, becuse we have noticed increased disk writes when we we wrote TImestamp on every handshake... If my query is good, for this mainnet node, we would have more then 90k of seen events in a day, 99% redudant.

hmm... ok.
but a sidenote - we cannot ignore a stale timestamp and potentially also a "too soon" timestamp - the issuing client might not know it has issued "too soon" and stale timestamp is misleading - we cannot treat this error as an error in this case - as you may receive an older timestamp via gossip, that's legit behavior and should count as "seen".

@gacevicljubisa

Copy link
Copy Markdown
Member

Nice work.
@acud suggested not have 24h update check, so at least in that case, please remove this information from the PR desc before merge.

Additionally I would still suggest 24h throtle to have it, becuse we have noticed increased disk writes when we we wrote TImestamp on every handshake... If my query is good, for this mainnet node, we would have more then 90k of seen events in a day, 99% redudant.

hmm... ok. but a sidenote - we cannot ignore a stale timestamp and potentially also a "too soon" timestamp - the issuing client might not know it has issued "too soon" and stale timestamp is misleading - we cannot treat this error as an error in this case - as you may receive an older timestamp via gossip, that's legit behavior and should count as "seen".

Yes. I totaly agree. This metric is capturing just "stale" and "too soon", and real "seen" will be even more then this.

@martinconic
martinconic merged commit a512398 into master Jul 23, 2026
18 of 19 checks passed
@martinconic
martinconic deleted the feat/addressbook-pruning branch July 23, 2026 13:22
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.

4 participants