Adamant-im / Adamant-im/ipfs-node

[Composite] Discover peer nodes instead of listing them in every config

Open
#28 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Composite task Decentralization Infrastructure IPFS
Dominant language
TypeScript
Stars
301
Forks
3
Avg merge
23h 4m
Merged PRs (30d)
6

Description

Summary

Let peer nodes learn about each other instead of requiring every operator to edit nodes whenever a node joins, and make placement behave correctly while their views of the network differ.

Details

Adding a node today means editing nodes on every existing node. That is not just inconvenient — it is load-bearing. Placement derives a file's holders from the CID with rendezvous hashing, and the guarantee that "every node computes the same holders without coordinating" holds only because operators keep those lists identical by hand.

The membership set is therefore consensus-critical, not a convenience. If node A knows {A,B,C,D,E} and node F knows {A,B,C,D,F}, they compute different holders for the same CID: A places copies with its four, F looks for the file at its four, and the two sets need not overlap.

Everything reads that set through one seam, getReplicationPeers() in src/storage/service.ts, which is what makes the source replaceable. What has to change alongside it are the rules built on top.

What breaks when views differ, and what does not

Handover stays safe. A node releases its copy only after every node it believes to be a holder confirms over the protocol that it has the file. If its view is wrong, the probed nodes answer "no" and the copy stays. The invariant is "never drop without live proof that the required copies exist", which does not depend on views agreeing. Divergence makes handover conservative, not dangerous.

Targeted retrieval degrades. A node dials the holders it computes for a CID. If the real holders are outside its view, the dials do not help and it falls back to bitswap over whatever peers it has, which is the 408 this targeting was added to remove.

Repair accounting becomes wrong. Under-replication is measured against the local view, so a node either places copies nobody needed or believes a file is fine when its copies have fallen below the target. The real copy count stops being managed and drifts.

A new node cannot hold anything. Replication intake is authorized against the same list, so a node nobody has listed can serve reads but can never take responsibility for a file.

Membership changes move the designated set

Even with perfect agreement, adding or removing a node re-ranks some CIDs. Rendezvous hashing keeps that to the fraction that actually changes rank rather than reshuffling everything, but a file stored before the change can end up with its real holders outside its current designated set. Copies have to migrate: repair places them at the new holders, and handover then releases the old ones, which is safe because it requires proof.

This is why live membership refresh alone does not make targeted retrieval sufficient. Retrieval needs a fallback that does not depend on placement being current.

A new node should be useful immediately

A node that has just joined can serve reads the moment it holds a block, and that is the property worth having first: it improves availability without anyone having to trust it.

Half of this already works, and not by accident. A block fetched to answer a read is written to the local blockstore and is kept until space runs short, so a new node becomes an extra serving copy of everything that has been read through it, without being counted for anything.

The half that does not work is accepting a copy on an established node's initiative, and the obstacle is placement rather than authorization: the pushing node computes holders from its own member list, which does not contain the newcomer, so it never selects it. Relaxing the authorization check changes nothing on its own.

The reverse direction has an interim answer in #26: a peer that refuses responsibility for a newcomer's file is asked to hold an unpinned copy instead, so the file spreads and survives the newcomer being lost. That copy is not counted, no repair job watches it, and it goes as soon as its peer needs the space. It buys time; it is not durability, and discovery is what replaces it.

So a node that is not yet established should be able to accept and serve copies while those copies do not count towards the required number. It holds an extra copy, reads get faster, and durability accounting stays conservative until the node has proven it stays around. What counts as established is a separate question from what a node is allowed to do.

Anchoring membership in the ADAMANT blockchain

Gossip converges, but it converges on whatever the peers happen to agree on, and it gives no reason to believe a member is real. The chain answers both: it is already an ordered, replicated log that every node can read the same way.

The shape:

  • A node holds an ADM address derived from its own passphrase, and that address is its identity.
  • It publishes a type: 9 state transaction — the protocol's key/value record — carrying its libp2p peer id and multiaddr.
  • Every node reads the chain and derives the same member list, which is exactly what rendezvous hashing needs and what a gossiped list cannot promise.

Three things this has to get right:

The binding must go both ways. The transaction is signed by the ADM key and claims a peer id, but the ADM key and the libp2p key are different keys. Without a matching proof in the other direction, anyone can publish someone else's peer id and inherit its standing. A node should present a signature over its peer id from its ADM key during the libp2p handshake, so "this peer is that address" is provable from either side.

A balance is a usable Sybil cost, but it is rentable. A balance can be acquired before an attack and withdrawn after it, so the criterion should be a balance held over time, which the transaction history already shows, together with the age of the registration. What deters Sybil is a cost that lasts, not a cost paid once.

The chain carries an ordering, which is worth more than the list itself. Registrations are ordered, so the member set has a version: the composition as of a given height. That lets nodes agree not only on who is a member but on which moment to compute placement for, which is what makes copy migration after a membership change deterministic rather than racy.

One consequence to state plainly: node addresses become public, and so do their balances.

Until this exists, membership is whatever the gossip converged on, and the accompanying rules have to stay tolerant of disagreement.

Guaranteed delivery of a deletion to every holder is also out of scope, but note that #27 depends on it: a signed deletion request only removes content from the nodes it reaches.

Checklist

  • Let a node join by pointing it at one or two existing nodes, without editing the configuration of any of them
  • Exchange the known-node set between peers, and converge on it
  • Keep getReplicationPeers() as the single source of membership, so placement, retrieval, repair, handover, and intake all follow it
  • Accept and serve copies from a node that is not yet established, without counting them towards the required number
  • Define what promotes a node to counted, and record when a node was first seen
  • Migrate copies when membership changes: repair places them at the new holders, handover releases the old ones
  • Give retrieval a fallback that does not assume placement is current
  • Measure under-replication against live confirmations rather than against the local list
  • Bound the cost of membership exchange, so it does not become network-wide chatter
  • Register a node on the ADAMANT chain and derive the member list from it
  • Prove the peer id and the ADM address belong together, in both directions
  • Decide the Sybil criterion from held balance over time and registration age
  • Compute placement against a stated chain height, so membership changes are not racy
  • Ask a peer whether it will take a copy before choosing it, so an unproven or full node costs one message
  • Document what a divergent view can and cannot cause

Notes

Builds on the placement and libp2p control plane from #22 and #26. getReplicationPeers() is the seam to replace; the four consumers behind it do not need to change.

Complete Sybil resistance and guaranteed deletion delivery are not implemented by this issue. It must still define the measurable admission criterion used by versioned membership and the delivery assumptions consumed by #27 and #23.

Verification

  • Start a node configured with a single existing peer and confirm every node learns about it without a configuration change
  • Confirm the new node serves reads for content it received, while the required copy count still ignores it
  • Confirm a file uploaded before the new node joined stays retrievable from every node, including the new one
  • Confirm copies migrate to the new designated holders after membership changes, and that the old holders release theirs only once the new ones confirm
  • Stop a node and confirm the others stop counting it without losing copies
  • Give two nodes deliberately different views and confirm no file loses its copies

Coordination with health checkpoints and legacy configuration

Membership is also an input to the cluster-freshness contract in #23. Every checkpoint attestation must name the exact accepted membership version or the explicit static-config mode used for peer coverage and placement.

Preserve the current nodes configuration as a backward-compatible membership source during rollout. Static and dynamically discovered fleets may coexist, but two nodes with materially different membership views must not claim the same fully attested health round unless a documented compatibility rule proves the required overlap.

Public peers, unproven cache nodes, and bridge nodes researched in #31 must remain uncounted by default. Reachability alone must not promote a node into placement, deletion delivery, or health quorum.

Additional checklist
  • Version the accepted membership view and include that version or static mode in placement, repair-cycle, and health-checkpoint state
  • Preserve static nodes configuration during a staged migration and define mixed static/dynamic behavior
  • Keep unproven, public, and cache-only peers outside counted durability and health quorum
Additional verification
  • Give two nodes different membership versions and confirm they cannot report the same fully attested checkpoint accidentally
  • Run a mixed static/dynamic test fleet and confirm no current deployment loses configured peers or copies during migration

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with getReplicationPeers() in src/storage/service.ts and the placement and libp2p control-plane work referenced in #22 and #26. Trace its placement, retrieval, repair, handover, and intake consumers before defining the membership model. Done requires the listed discovery, migration, divergent-view, uncounted-copy, health-version, and legacy-configuration verification scenarios to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, typescript
Domain
backend, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.