From 75dda1e7d686c7b1802df67da51069682cab3bea Mon Sep 17 00:00:00 2001 From: Worph Date: Sat, 25 Jul 2026 21:30:46 +0200 Subject: [PATCH] new NZB cid support --- internal/cid/cid-rank-vectors.json | 7 ++++++ internal/cid/rank.go | 36 +++++++++++++++++++++++++----- internal/cid/rank_test.go | 25 ++++++++++++++++++--- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/internal/cid/cid-rank-vectors.json b/internal/cid/cid-rank-vectors.json index a772ee7..84de7f4 100644 --- a/internal/cid/cid-rank-vectors.json +++ b/internal/cid/cid-rank-vectors.json @@ -19,6 +19,7 @@ "ladder": { "40": "dag-pb (IPFS block)", "30": "sha2-256 / sha3-256 / sha3-384 - real content digests", + "25": "nzb-posting - content-derived (Message-ID set digest), but not itself fetchable", "20": "BitTorrent infohashes - swarm interop", "10": "midhash256 - the record address", "5": "opaque locators (nzb-release, url) - never outrank a real digest", @@ -56,6 +57,12 @@ "rank": 30, "note": "REAL meta-hash fixture" }, + { + "name": "nzb-posting", + "cid": "bagbsaeraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "rank": 25, + "note": "codec 0x1003 + REAL sha2-256 multihash (0x12) over the normalised, sorted Message-ID set - NOT a locator, so it sits ABOVE tier 5, but it addresses no fetchable block (bitswap can never satisfy a want for it - the digest is over an id set, not over any block's bytes), so it stays BELOW a real content digest (30): once meta-share materialises the media and re-seeds it, that genuine sha2-256 cid must win the canonical election, not this one. Rank MUST match on codec BEFORE falling through to the mh switch, or this scores 30 (same trap as nzb-release/url, in reverse: those need a codec check because their mh is identity 0x00; this needs one because its mh is REAL sha2-256 and would otherwise tie with tier 30)." + }, { "name": "btih-v2-codec-eq-mh", "cid": "bag3sdnzbeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/internal/cid/rank.go b/internal/cid/rank.go index a4cc6a6..6dfe90b 100644 --- a/internal/cid/rank.go +++ b/internal/cid/rank.go @@ -49,16 +49,29 @@ const ( // these — matching on mh would see 0x00 and rank them 0. CodeNzbRelease = 0x1005 // custom — self-describing Newznab release locator CodeURL = 0x1006 // custom — identity-multihash CID wrapping an http(s) URL + + // CodeNzbPosting is the INVERSE trap of the two above: its multihash is a + // REAL sha2-256 digest (over the normalised, sorted article Message-ID + // set of a Usenet posting we scanned ourselves — never a locator, no + // indexer host embedded), so it would silently fall into RankDigest via + // the mh switch unless matched on the codec FIRST. It is content-derived + // (so it outranks the opaque locators) but addresses no fetchable block — + // the digest is over an id set, not over any block's bytes, so it can + // never be bitswap-fetched under itself — so it must stay BELOW a real + // digest (once the media is materialised and re-seeded, that genuine + // sha2-256 cid has to win the canonical election, not this one). + CodeNzbPosting = 0x1003 // custom — self-scanned Usenet posting identity ) // Rank tiers, named so the ladder reads as a ladder. const ( - RankIPFS = 40 // dag-pb — broadest network reach - RankDigest = 30 // sha2-256 / sha3-* — real content digests - RankBtih = 20 // BitTorrent infohashes — swarm interop - RankMidhash = 10 // the record address — internally meaningful, externally invisible - RankLocator = 5 // opaque locators — never outrank a real digest - RankUnknown = 0 // weak/unknown digests, unparseable input + RankIPFS = 40 // dag-pb — broadest network reach + RankDigest = 30 // sha2-256 / sha3-* — real content digests + RankNzbPosting = 25 // content-derived, but the digest addresses no fetchable block + RankBtih = 20 // BitTorrent infohashes — swarm interop + RankMidhash = 10 // the record address — internally meaningful, externally invisible + RankLocator = 5 // opaque locators — never outrank a real digest + RankUnknown = 0 // weak/unknown digests, unparseable input ) // Decode parses a multibase-base32 CIDv1 string ("b…") into its content @@ -112,6 +125,11 @@ func Decode(cidStr string) (codec uint64, mhCode uint64, err error) { // - dag-pb (chunked IPFS file) wins: best retrieval over the IPFS network. // - sha2-256 / sha3 digests follow: standard content hashes (a raw IPFS // block of the file IS its sha2-256, so they share this tier). +// - nzb-posting is content-derived (a digest over a Usenet posting's article +// Message-ID set) so it outranks the opaque locators below, but the +// digest addresses no fetchable block, so it stays below a real content +// digest — once the media is materialised and re-seeded under a genuine +// sha2-256 cid, that cid must win the election, not this one. // - BitTorrent infohashes (btih v2, per-file v1/v2) outrank midhash256 for // swarm interop, not strength. // - midhash256 is the floor among real *digests*: internally meaningful, @@ -142,6 +160,12 @@ func Rank(cidStr string) int { if codec == CodeNzbRelease || codec == CodeURL { return RankLocator } + // nzb-posting: codec-only, checked BEFORE the mh switch below — its + // multihash is a real sha2-256 and would otherwise match CodeSha256 there + // and rank 30, tying with genuine content digests. + if codec == CodeNzbPosting { + return RankNzbPosting + } switch mh { case CodeSha256, CodeSha3_256, CodeSha3_384: return RankDigest diff --git a/internal/cid/rank_test.go b/internal/cid/rank_test.go index 0e42f6e..373dffa 100644 --- a/internal/cid/rank_test.go +++ b/internal/cid/rank_test.go @@ -150,12 +150,31 @@ func TestRank_Tiers(t *testing.T) { func TestRank_StrictOrdering(t *testing.T) { ipfs := Rank(buildCID(CodecDagPB, CodeSha256, 32)) sha := Rank(cidSha256) + posting := Rank(buildCID(CodeNzbPosting, CodeSha256, 32)) btih := Rank(buildCID(CodeBtihV2, CodeBtihV2, 32)) mid := Rank(buildCID(CodeMidhash256, CodeMidhash256, 32)) unknown := Rank(cidMd5) - if !(ipfs > sha && sha > btih && btih > mid && mid > unknown) { - t.Fatalf("rank ordering broken: ipfs=%d sha=%d btih=%d mid=%d unknown=%d", - ipfs, sha, btih, mid, unknown) + if !(ipfs > sha && sha > posting && posting > btih && btih > mid && mid > unknown) { + t.Fatalf("rank ordering broken: ipfs=%d sha=%d posting=%d btih=%d mid=%d unknown=%d", + ipfs, sha, posting, btih, mid, unknown) + } +} + +// TestRank_NzbPostingMatchesCodecNotMultihash pins the inverse trap: +// nzb-posting's multihash is a REAL sha2-256 (unlike nzb-release/url, whose mh +// is identity 0x00), so Rank must match on the codec BEFORE falling into the +// mh switch — otherwise this ties with a genuine content digest at 30 instead +// of sitting at 25. +func TestRank_NzbPostingMatchesCodecNotMultihash(t *testing.T) { + posting := buildCID(CodeNzbPosting, CodeSha256, 32) + if got := Rank(posting); got != RankNzbPosting { + t.Errorf("Rank(nzb-posting) = %d, want %d (RankNzbPosting) — codec check must precede the mh switch", got, RankNzbPosting) + } + // A real sha2-256 with the SAME digest bytes must still rank 30 — the + // codec, not the digest, is what distinguishes the two. + realDigest := buildCID(CodecRaw, CodeSha256, 32) + if got := Rank(realDigest); got != RankDigest { + t.Errorf("Rank(sha2-256) regressed to %d, want %d", got, RankDigest) } }