Skip to content

fix: sha2-256 CIDv1 (#43) + restore performance benchmarks (#58) - #90

Merged
TheGreatAlgo merged 4 commits into
mainfrom
codec-version
Jul 27, 2026
Merged

fix: sha2-256 CIDv1 (#43) + restore performance benchmarks (#58)#90
TheGreatAlgo merged 4 commits into
mainfrom
codec-version

Conversation

@TheGreatAlgo

@TheGreatAlgo TheGreatAlgo commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #43. Closes #58. Addresses #59.

Two issues, verified against a local Kubo 0.39.0 daemon.

#43sha2-256 returns a CIDv0/dag-pb CID

Reproduced directly against the RPC API:

hash=sha2-256                → QmWD8Rcqds…  (CIDv0, dag-pb by definition)
hash=sha2-256&cid-version=1  → bafkreiamwzk… (CIDv1, raw codec)
hash=blake3                  → bafkr4id5cck… (already CIDv1 — never affected)

Fix: append &cid-version=1 to the /api/v0/add URL (store_httpx.py:716).

Two things worth flagging beyond the issue text:

The fix suggested in the issue would not have worked. #43 proposes reworking the returned CID into one with the correct multicodec. The stored blocks differ, so that is not possible:

CIDv0 block: 0a24 0802 121e <payload> 181e   ← UnixFS dag-pb wrapper, 38 bytes
CIDv1 block: <payload>                       ← raw bytes, 30 bytes

The CIDv1 digest is exactly sha256(payload). Relabelling a CIDv0's codec would yield a CID that does not hash to its own block, so this has to be fixed at add time. Existing code at store_httpx.py:1086 already declined to relabel dag-pb CIDs (if cid.codec.code != self.DAG_PB_MARKER) — a workaround for precisely this.

Severity is higher than cosmetic. _cid_is_verifiable() skips verification for dag-pb CIDs, so verify_content=True on a sha2-256 store was silently passing every block through unverified. The default blake3 is not CIDv0-representable and was never affected, which is why this went unnoticed.

Tests: tests/test_k16_sha2_256_cid_version.py — 14 cases covering the add URL, CID version/codec, digest-matches-stored-block, Kubo's own block store, verifiability, and a full HAMT round trip under sha2-256. Reverting the one-line fix fails 9 of them; the blake3 control cases pass in both states, so they are not passing vacuously.

#58 — Performance benchmarks

tests/test_benchmark_stores.py was entirely commented out, and could not have run as written: it imported FlatZarrStore (no longer exported), pointed at production ipfs-gateway.dclimate.net with a blank X-API-Key, used Dataset.dims.values() (removed in modern xarray), and printed timings without asserting anything.

Rewritten to assert on request counts from the instrumentation module rather than wall-clock time — deterministic, so it holds up in CI. Datasets use a seeded RNG so chunk bytes, CIDs, and counts are stable. Timings are still measured and printed via --benchmark-report for humans, but nothing fails on them.

Coverage: HAMT and sharded write/append/read, sharded-vs-HAMT round trips, read-cache effectiveness, and a concurrency=[1, 8, 32] parametrization (which also addresses #59 — the existing test_k3/test_k9 cover semaphore scoping mechanics, not behaviour across concurrency values).

ZarrHAMTStore write+append   0.46s  cas_loads=11  unique=6   dupes=5   retries=0
ZarrHAMTStore read           0.08s  cas_loads=21  unique=10  dupes=11  retries=0
HAMT read                    0.13s  cas_loads=30  unique=25  dupes=5   retries=0
Sharded read (full)          0.06s  cas_loads=26  unique=26  dupes=0   retries=0
full scan @ mode=sparse      0.09s  cas_loads=45  unique=26  dupes=19  retries=0
full scan @ mode=full        0.10s  cas_loads=26  unique=26  dupes=0   retries=0

A caveat these benchmarks now pin down

ShardedZarrStore defaults to shard_read_mode="sparse" (sharded_zarr_store.py:484). Sparse mode — added in #87 — fetches one entry per chunk to keep point reads cheap (~9x on 30 years of ERA5 dailies, the AEGIS workload) and deliberately does not populate the shard cache.

On a whole-array scan that is the wrong mode by design. Measured at 320 chunks:

read cas_load duplicate fetches shard fetches
HAMT full-scan 334 5
Sharded, sparse (default) 645 313 n/a by design
Sharded, full 332 0 7

In full mode 7 shard fetches serve all 320 chunks with zero duplicates — sharding behaves exactly as #88/#56 describe.

The trap: this gap is invisible below chunks_per_shard, where both modes tie exactly. Benchmarking a whole-array read on the default with a small dataset gives numbers that look fine, then a ~2x regression at production scale. I hit this myself while writing these benchmarks and initially misread it as a broken shard cache.

So every scan benchmark here sets shard_read_mode explicitly, and two tests pin the behaviour:

  • test_sparse_mode_trades_scan_cost_for_point_read_latency — asserts the trade-off in both directions, so the cost is visibly intentional
  • test_sharded_store_defaults_to_sparse_read_mode — pins the default, since it decides which trade-off users get

test_sharded_full_mode_read_batches_chunks_into_shard_fetches asserts the batching property directly (0 < shard_loads < n_chunks, zero duplicates) rather than comparing raw totals, so it fails if per-chunk round trips ever return to the full path.

Note these are network round-trip benchmarks. #88's headline figures measure different quantities — bulk writes against InMemoryCAS, warm-cache get latency, per-request RTT depth — and are not comparable to the totals above.

Also included

  • --benchmark-report pytest flag (tests/conftest.py) to print the tables; assertions run either way
  • uv.lock: syncs the py-hamt entry to 3.5.0, matching pyproject.toml (the lock on main was stale at 3.4.1)

Verification

  • pytest --ipfs427 passed, 1 skipped (up from 418 on main), no xfails
  • coverage report --fail-under=100100% across all 8 modules
  • pre-commit run --all-files — ruff check, ruff format, slotscheck, mypy all pass

Run the benchmarks with:

pytest tests/test_benchmark_stores.py --ipfs --benchmark-report -s

Not addressed

The ShardedZarrStore docstring does not mention that shard_read_mode="sparse" is a poor fit for whole-array scans, or that the difference only appears above chunks_per_shard. That is a docs gap rather than a code bug, so I left it out of this PR — happy to add it here or separately.

Summary by CodeRabbit

  • Bug Fixes

    • Improved content-addressed storage so saved data retains the requested codec and can be verified correctly.
    • Fixed compatibility for SHA-256 content identifiers and full HAMT round trips.
  • Tests

    • Added regression coverage for CID generation, payload integrity, and verification.
    • Added deterministic benchmarks covering reads, writes, caching, sharding, and concurrency.
    • Added an optional benchmark reporting flag for timing and request-count summaries.

Keeps the py-hamt 3.5.0 version sync (main's lock was stale at 3.4.1)
but reverts revision 3 -> 2, which was an artifact of an older local uv
rather than an intended change.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@TheGreatAlgo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 613c172d-5956-4c70-818e-1dd5859bbe6f

📥 Commits

Reviewing files that changed from the base of the PR and between e3f0aac and 4937017.

📒 Files selected for processing (3)
  • py_hamt/store_httpx.py
  • tests/test_benchmark_stores.py
  • tests/test_k16_sha2_256_cid_version.py

Walkthrough

The Kubo add URL now pins CIDv1, with regression tests for codec and payload correctness. The benchmark suite was replaced with deterministic, instrumented pytest coverage for store operations, sharded read modes, caching, and concurrency.

Changes

CID correctness

Layer / File(s) Summary
CID version URL contract
py_hamt/store_httpx.py, tests/test_k16_sha2_256_cid_version.py
Kubo add requests include cid-version=1, with coverage for sha2-256 and blake3.
Payload and verification behavior
tests/test_k16_sha2_256_cid_version.py
Tests verify requested codecs, payload-derived digests, verbatim block storage, and content verification.
HAMT sha2-256 roundtrip
tests/test_k16_sha2_256_cid_version.py
A verified HAMT is persisted and read back under sha2-256 with a CIDv1 root.

Benchmark coverage

Layer / File(s) Summary
Benchmark instrumentation and reporting
tests/conftest.py, tests/test_benchmark_stores.py
Adds the benchmark-report option, deterministic tracing, phase timing, request counters, and formatted reports.
Store benchmark flows
tests/test_benchmark_stores.py
Adds deterministic HAMT and sharded-store write, append, flush, and read benchmarks with request-count assertions.
Read modes, caching, and concurrency
tests/test_benchmark_stores.py
Tests sharded batching, sparse defaults and scan costs, cache reuse, and concurrent reads without retries.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: faolain

Poem

A rabbit hops where CIDs shine,
“CIDv1!” sings down the line.
Shards fetch neatly, caches glow,
Benchmarks count each hop below.
HAMTs round-trip, verified and bright—
Tucked safely in the moonlit night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue [#43] is covered, but [#58] is not: the benchmarks are local/instrumented only and don't implement the required gateway comparison matrix. Add benchmark coverage for the listed remote/local gateway environments and comparison matrix, including bandwidth notes and parallel gateway rows if supported.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main changes: sha2-256 CIDv1 support and benchmark restoration.
Out of Scope Changes check ✅ Passed The PR stays focused on CIDv1 support and benchmark restoration, with no obvious unrelated code paths added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codec-version

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_k16_sha2_256_cid_version.py (1)

29-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Hypothesis coverage for the Kubo add URL.

tests/test_k16_sha2_256_cid_version.py has only example-based tests, while Hypothesis is already available in the test suite. Add a property-based test over pin_on_add and valid chunker values that asserts cid-version=1 is present in every parsed Kubo add URL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_k16_sha2_256_cid_version.py` around lines 29 - 41, Add a
Hypothesis property-based test in test_k16_sha2_256_cid_version.py covering all
pin_on_add settings and valid chunker values, constructing the corresponding
KuboCAS add URL and parsing its query parameters. Assert that every parsed URL
contains cid-version=1, while preserving the existing hasher coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_benchmark_stores.py`:
- Around line 283-347: Correct the benchmark’s physical chunk-count calculation
by multiplying the per-dimension block counts in ds.chunks rather than summing
them, and update the dataset parameters or chunks_per_shard so the actual count
is strictly greater than the shard threshold and spans multiple shards. Revise
the nearby comments/docstring to state the accurate count, and keep the
shard-load assertions based on the corrected n_chunks value.

---

Nitpick comments:
In `@tests/test_k16_sha2_256_cid_version.py`:
- Around line 29-41: Add a Hypothesis property-based test in
test_k16_sha2_256_cid_version.py covering all pin_on_add settings and valid
chunker values, constructing the corresponding KuboCAS add URL and parsing its
query parameters. Assert that every parsed URL contains cid-version=1, while
preserving the existing hasher coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c04236da-96d5-4221-85dc-e6b2ea340bdf

📥 Commits

Reviewing files that changed from the base of the PR and between c7d3cd9 and e3f0aac.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • py_hamt/store_httpx.py
  • tests/conftest.py
  • tests/test_benchmark_stores.py
  • tests/test_k16_sha2_256_cid_version.py

Comment thread tests/test_benchmark_stores.py Outdated

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Four actionable issues found.
Posted 4 inline comment(s).

Comment thread py_hamt/store_httpx.py
# verify_content would silently pass every block through unverified.
# Requesting v1 makes Kubo store the raw bytes under the codec asked
# for, which is what blake3 (not CIDv0-representable) already got.
self.rpc_url: str = f"{rpc_base_url}/api/v0/add?hash={self.hasher}&chunker={self.chunker}&pin={pin_string}&cid-version=1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
cid-version=1 only makes leaf chunks raw. Once data exceeds the configured chunk size (1 MiB by default), Kubo returns a UnixFS dag-pb root (layout documentation); save() then declines to apply the requested codec and verify_content still skips verification. Handle/reject multi-chunk inputs or use an endpoint that stores the payload as one block, and add a payload-larger-than-chunker regression test.

Comment thread tests/test_benchmark_stores.py Outdated

xr.testing.assert_identical(xr.concat([ds, ds], dim="time"), actual)

assert write.cas_loads > 0, "write did no content-store work"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
This is not measuring write request counts: cas_load.total records loads, not /add saves, and checking only > 0 cannot catch an increase from 11 requests to thousands. The sharded write benchmark has the same problem because shard_cache.total counts cache accesses. Instrument save/POST requests and assert a stable count or upper bound.

Comment thread tests/test_benchmark_stores.py Outdated
# Both must return the same data, or the comparison is meaningless.
xr.testing.assert_identical(hamt_result, sharded_result)

n_chunks = sum(len(chunks) for chunks in ds.chunks.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
ds.chunks describes 20 × 1 × 1 = 20 primary-array chunks here, while this sum produces 22; with chunks_per_shard=50, the fixture also creates only one primary shard. Because shard_loads uses shard_cache.total (hits plus misses), a per-chunk 20-call regression still passes 20 < 22. Force multiple shards, compute chunk count with math.prod, and assert cache misses/network fetches.

Comment thread tests/test_benchmark_stores.py Outdated
root = hamt.root_node_id

async with KuboCAS(
rpc_base_url=rpc, gateway_base_url=gateway, concurrency=concurrency

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW
The test passes concurrency but never observes whether it changes request parallelism. If KuboCAS ignored the argument and serialized all requests, data identity, unique_cids > 0, and zero retries would still pass for every parameter. Measure simultaneous HTTP requests with a blocking/mock transport and assert the configured limit.

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c7d3cd9) to head (4937017).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #90   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            8         8           
  Lines         3166      3168    +2     
=========================================
+ Hits          3166      3168    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@da-code-reviewer da-code-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Automated Review

Found three performance-regression checks that do not exercise the behavior they claim to protect.
Posted 3 inline comment(s).


xr.testing.assert_identical(xr.concat([ds, ds], dim="time"), actual)

assert write.work_units > 0, "write did no storage work"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
The sharded write benchmark does not count writes. work_units sums CAS loads and shard-cache accesses, neither of which records /api/v0/add; this assertion still passes if writes amplify from a handful of POSTs to thousands. Use CountingTransport here and assert an upper bound on tally.adds, as the HAMT benchmark does.

results[mode] = report(phase.result)
xr.testing.assert_identical(ds, scanned)

assert results["sparse"].cas_loads > results["full"].cas_loads, (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW
Only the scan-cost half of the advertised sparse/full trade-off is tested. Both phases load the entire dataset, so a regression that makes sparse point reads decode/cache full shards still passes as long as sparse scans remain more expensive. Add an isolated point-read comparison with instrumentation for decoded/cached shard work, or narrow the test's claim.

)

# The ceiling is honoured: never more requests in flight than configured.
assert tally.peak_in_flight <= concurrency, (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM
The 8 and 32 concurrency cases never exercise their limits. The fixture exposes only about four overlapping requests, so peak_in_flight <= concurrency is non-binding for both values and the later > 1 check would accept a hard-coded limit of two. Drive more than 32 blocked uncached loads and assert the observed ceiling for each parameter.

@TheGreatAlgo
TheGreatAlgo merged commit 74ef73d into main Jul 27, 2026
3 checks passed
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.

Performance Benchmarks Find solution to IPFS RPC API sha2-256 hasher only returning dag-pb in CIDv0

2 participants