Skip to content

Files v2 (umbrella)#716

Open
cheggaaa wants to merge 29 commits into
mainfrom
files-v2
Open

Files v2 (umbrella)#716
cheggaaa wants to merge 29 commits into
mainfrom
files-v2

Conversation

@cheggaaa

@cheggaaa cheggaaa commented Jul 1, 2026

Copy link
Copy Markdown
Member

Files v2 — umbrella

Tracking/umbrella branch for the Files v2 effort (Linear SYN-12): filenode v2, S3-direct broker, and networkSign, per the SDK design doc 07c-filenode-v2-networksign.md. Individual sub-tasks land here as separate commits; this PR stays draft until the epic is ready to merge.

Landed so far

SYN-15 — space header fileprotoVersion

  • Add SpaceFileProtoVersion enum (Unspecified=0, V2=2) and fileprotoVersion field (Bump github.com/ipfs/go-blockservice from 0.5.1 to 0.5.2 #9) to SpaceHeader; plumb FileProtoVersion through SpaceCreatePayload/SpaceDerivePayload into every builder. The field lives in the signed, immutable header → tamper-evident and recovered verbatim on cold restore. Callers/SDK set the value; unset stays Unspecified so legacy consumers are unaffected.

Default new spaces to v1 header

  • Add StoragePayloadForSpaceDeriveV1 (deterministic v1 derive builder) and switch CreateSpace/DeriveId/DeriveSpace to the v1 builders (v1 embeds acl/settings, validates via byte-compare — the path one-to-one spaces already use in production).

⚠️ Known caveat (tracked, not yet resolved)

Switching derive to v1 changes the derived space id for the same keys (the header bytes, hence the CID, differ). Safe on this greenfield branch, but an in-place legacy/anytype-heart upgrade would need an id-continuity strategy (dual-derive lookup or id-alias) before shipping. A 3-repo audit (any-sync + coordinator + node) confirmed v0/v1 headers otherwise coexist correctly end-to-end.

Notes

  • any-sync-coordinator / any-sync-node currently pin any-sync v0.11.20; they won't see fileprotoVersion until they bump the dependency (harmless — headers are stored/verified as raw bytes).

Checklist

  • go build ./...
  • commonspace, spacepayloads, spacestorage/migration, headsync tests pass
  • Remaining Files v2 sub-tasks (filenode v2 gating, payloads object, S3/networkSign)

🤖 Generated with Claude Code

cheggaaa and others added 2 commits July 1, 2026 13:10
Add StoragePayloadForSpaceDeriveV1 (deterministic v1 derive builder) and switch
CreateSpace, DeriveId and DeriveSpace to the v1 builders. v1 headers embed the acl
and settings payloads and validate via byte-compare (needCheckSpaceId=false),
matching the one-to-one path already shipping in production.

Note: v1 derive produces a different derived space id than v0 for the same keys
(the header bytes, hence the CID, differ). Safe on greenfield; existing v0-derived
spaces would need an id-continuity strategy before an in-place upgrade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the SpaceFileProtoVersion enum (Unspecified=0, V2=2) and a fileprotoVersion
field to the SpaceHeader proto, and plumb it through SpaceCreatePayload and
SpaceDerivePayload into every builder. The field lives in the signed, immutable
header, so it is tamper-evident and recovered verbatim on cold restore.

Gates the v2 file protocol (filenode v2 + S3-direct + networkSign) per SYN-15.
Callers set the value (e.g. the SDK mints spaces at V2); unset stays Unspecified,
so legacy any-sync consumers are unaffected. OneToOne spaces stay Unspecified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

New Coverage 58.2% of statements
Patch Coverage 74.5% of changed statements (193/259)

Coverage provided by https://github.com/seriousben/go-patch-cover-action

cheggaaa and others added 27 commits July 1, 2026 13:34
Add the NodeTypeFileV2 node type ("fileV2") and a second, independent chash
container built from the v2 file-node pool at RF=2 (FileV2ReplicationFactor).

New NodeConf methods:
- FileV2Peers() lists the v2 filenodes (mirrors FilePeers).
- FileV2NodeIds(spaceId) returns the RF=2 responsible v2 filenodes for a space.
  Unlike NodeIds, the current account is NOT excluded, so the full responsible
  pair is visible (e.g. to derive the leader).

The tree (sync) and v1-file containers are untouched. Leader selection and
per-space responsibility consumers are out of scope. Propagated to service,
testconf StubConf and the regenerated mock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ath (SYN-16)

Add FileV2API=6 to the coordinator NodeType proto enum and map it to
nodeconf.NodeTypeFileV2 in nodeconfsource. Without this the v2 file type only
survived local YAML configs and was dropped on the coordinator-distributed path.

The enum->string switch keeps its no-default behavior: an older client that does
not know FileV2API (or any future type) silently drops the unknown value while
keeping the node's known types, so it stays correctly routed in the tree/file
pools and never crashes. New enum values are therefore backward-compatible.

Note: the coordinator server (any-sync-coordinator) still needs the reverse
string->enum mapping to actually serve fileV2 nodes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Define the filenode v2 (FileV2) broker dRPC contract: a byteless broker
that authorizes uploads, signs durable-custody receipts, and issues signed
download URLs — never transferring bytes itself.

All four RPCs are batch (Upload / RequestSign / RequestDownload / SpaceInfo):
each request carries a shared spaceId + repeated items, and each response is a
repeated per-item result keyed by the echoed rootCid/spaceId with a flat status
enum, so one bad item never fails the whole batch. A two-layer error model
keeps whole-RPC failures (auth/malformed/batch-too-large) on the drpc error
while per-item business outcomes travel in-body.

New proto package filesyncv2 / service FileV2 (route /filesyncv2.FileV2/*),
distinct from v1 filesync/File, plus the fileprotov2err registry at error
offset 800 (see docs/rpc-error-offsets.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the process-global rpcerr registry as a shared cross-repo namespace:
the mechanism, allocation rules (offsets are multiples of 100, local codes
0-99, any-sync core < 1000 / downstream repos >= 1000), the globally reserved
low codes, and a full table of offsets in use across the ecosystem
(any-sync 100-800, any-sync-node 1000, anytype-push-server 1200,
bobrik-clusterctl 1300). Add an ErrGroup doc comment pointing to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lFilter (SYN-18)

Two opt-in, backward-compatible hooks for participants that head-sync a
whole space but materialize only selected tree types (filenode-v2 broker):

- TreeFullSyncRequest.probe: the responder replies with the tree's root
  and current heads only, no change bodies. Old responders ignore the
  flag and stream the full tree. Plumbed via BuildTreeOpts.Probe.
- treesyncer.PullFilter: optional extension of the registered TreeSyncer,
  consulted by objectsync on a head update for a locally-missing tree;
  head updates already carry the raw root change, so the filter can
  classify the tree (cleartext changeType) and decline the pull with no
  extra round trips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(sync): selective-sync hooks — tree probe requests + optional PullFilter (SYN-18)
…SYN-31)

Spaces chash-shard across the NodeTypeFileV2 pool (RF=2), and a filenode
must reject — and never activate/sync — spaces outside its responsibility
set. Give that rejection a dedicated retryable routing signal instead of
overloading Forbidden, so client SDKs can re-route to the responsible pair:

* whole-RPC ErrCodes.NotResponsible = 4 (registered in fileprotov2err at
  offset 800+4) — for the single-space RPCs where the entire request is
  misrouted;
* per-item ErrCode.ErrNotResponsible = 6 — for SpaceInfo, whose batch may
  legally mix spaces owned by different pairs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Downloads start PUBLIC-read: chat-message images must render with zero
broker round-trips, so the client constructs durable object URLs straight
from the rootCid — {publicReadBaseUrl}/blob/{spaceId}/{rootCid} — and GETs
the CDN. The base URL is distributed by the filenode itself (not nodeconf)
via a new node-level Info RPC that clients cache: no spaceId, no
responsibility gate, no auth beyond the connection. InfoRequest is
deliberately empty so future node metadata (capabilities, limits) extends
it without a wire break; an empty publicReadBaseUrl means no public read —
use RequestDownload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(fileprotov2): NotResponsible error codes + node-level Info RPC (SYN-31)
mockConf did not implement the FileV2Peers/FileV2NodeIds methods added
to nodeconf.NodeConf on this branch, causing a panic in TestInbox_Fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proto version 13 aligns with the v0.13.X release line and covers the
files v2 work (v2 filenodes, chash routing, space header fileprotoVersion).
Version 13 is already present in defaultCompatibleVersions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per docs/proto-versioning.md the default compatibility range is self ±1.
v0.13.X accepts V-1=12, V=13, V+1=14. Drops bridge v9, which is no longer
needed now that v0.12 has shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…entity

One shared account (signing) key signs fileV2 durability receipts for
the whole NodeTypeFileV2 fleet (per-node signing was rejected: receipts
live in CRDT rows for years and must survive fleet churn). Clients need
its public identity — the fileNetworkId — from the network config, so:

- nodeconf.Configuration gains FileNetworkId (yaml fileNetworkId,
  network string encoding, decode with crypto.DecodeNetworkId)
- coordinatorproto NetworkConfigurationResponse gains fileNetworkId=5
- nodeconfsource maps it off the proto, so coordinator-refreshed
  configs carry it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New commonfile/fileproto/filep2p service filesyncp2p.FileP2P, served
read-only by clients to LAN peers for direct file fetch:

- FileCheck(spaceId, rootCids) -> None/Partial/Full availability
- ObjectRead(spaceId, rootCid, offset, length) -> (data, totalSize)

A client stores one immutable CARv2 object per file, byte-identical to
the S3/public-read object, so a peer is just an ALTERNATE SOURCE of that
same object: ObjectRead returns object byte ranges exactly like an HTTP
Range GET, and the fetcher's existing seed/verify path is reused. Only
full holders serve ObjectRead; FileCheck reports which. Requests are
root-scoped (no global block index). Blocks are ciphertext; the fetcher
verifies hash==cid. Whole-RPC errors in filep2perr at rpcerr offset 900
(v1=200, v2=800). Route prefix /filesyncp2p.FileP2P/ coexists on one mux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Hq5ube4stoD1XM5QAU931
filesyncp2p.FileP2P already declares ErrorOffset=900 (the next free core
band); record it in docs/rpc-error-offsets.md, the source of truth for
who owns which offset. Adds a filep2perr test that co-registers the
neighbouring v1/v2 error groups so a colliding offset would panic at
init — a green run proves the 900 band is clear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Hq5ube4stoD1XM5QAU931
filep2p: peer-to-peer file transfer protocol (SYN-48)
…storage limits (SYN-19)

The v2 file-limits API: brokers pull absolute account-pool bounds per
(spaceId, uploader identity) and push per-(space, identity) durable
usage reports; the coordinator aggregates the identity total. In-flight
never reaches the coordinator; values are absolute and idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoLHZmQFxyhxDkkwJ7Y1jX
SpaceInfo now surfaces the requesting identity's shared account pool
(accountLimitBytes / accountUsageBytes) next to the per-space view —
files are charged to their uploader, all spaces share one pool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoLHZmQFxyhxDkkwJ7Y1jX
feat(coordinatorproto): files-v2 storage limits API — FileLimitsGet / FileUsageReport (SYN-19)
Replaces plaintext spaceId lists in LAN peer discovery with per-space
HMAC tokens keyed by a discovery key derived (HKDF) from the space's
first ACL read key. Peers learn only the intersection of their space
sets; a valid token simultaneously hides the space from non-members
and proves the sender's membership, so strangers on the LAN can no
longer enumerate spaces, correlate devices across sessions, or poison
the local peer table with spaces they don't hold.

Request tokens are padded with random fillers to fixed buckets and
sorted to hide the true space count; response tokens are keyed by the
caller's nonce (challenge-response). Gated by ProtoVersion 13; v1
fallback must be version-gated, never error-triggered.

Bench (1000 spaces/peer, 500 shared): ~1.5ms CPU for a full round
trip, plus 0.66ms one-time discovery-key derivation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(clientspaceproto): SpaceExchangeV2 — token-based LAN space discovery
All connectable peers negotiate DiffType_V3 since ProtoVersion 7 and the
minimum compatible version is 12, so the V2 paths are dead code:
- drop app/olddiff and the ldiff DiffContainer (single diff now)
- DiffManager keeps only the V3 diff; V2 range requests are rejected
- statestorage keeps a single hash (old hash is no longer written)
- keep the DiffType negotiation as the future upgrade mechanism, see
  the upgrade recipe comment on DiffManager
- statestorage: keep mirroring the hash into the legacy "oh" key so a
  previous release (rollback or mixed-fleet coldsync) doesn't misread
  NewHash as empty via its legacy-format fallback; transitional, remove
  once downgrades below this version are out of support
- diffmanager: guard FillDiff against empty Heads entries, set all
  elements with a single variadic Set (one range recalculation instead
  of N), document why FillDiff and UpdateHeads deliberately use
  different empty-root conditions, and note the V4 fallback-on-error
  requirement in the upgrade recipe
- add a hash-stability regression test pinned to the golden value
  produced by the pre-removal dual-diff DiffManager
refactor(headsync): remove old diffsync (DiffType_V2) support
@cheggaaa
cheggaaa marked this pull request as ready for review July 16, 2026 13:52
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