Skip to content

feat: Bitswap improvements for Kubo compatibility#1321

Open
sumanjeet0012 wants to merge 38 commits into
libp2p:mainfrom
sumanjeet0012:improvement/bitswap
Open

feat: Bitswap improvements for Kubo compatibility#1321
sumanjeet0012 wants to merge 38 commits into
libp2p:mainfrom
sumanjeet0012:improvement/bitswap

Conversation

@sumanjeet0012

@sumanjeet0012 sumanjeet0012 commented May 3, 2026

Copy link
Copy Markdown
Contributor

What was wrong?

Issue: py-libp2p's Bitswap file operations were not compatible with Kubo (Go-IPFS)
and the broader IPFS network. Files added by py-libp2p could not be fetched by Kubo
nodes, and vice versa. Additionally, there was no support for the Bitswap 1.3.0 Payment Extension, making the node incapable of participating in paid data retrieval, and the DHT records lacked cryptographic verification required by Kubo peers.

Several root causes were identified:

1. Wrong leaf block encoding (CODEC_RAW instead of dag-pb + UnixFS)

MerkleDag.add_file() and add_bytes() stored leaf chunks as raw blocks. Kubo wraps every leaf in UnixFS Data(type=File, data=chunk) inside a dag-pb PBNode. Using CODEC_RAW produced completely different CIDs for the same content.

2. Flat DAG structure (no balanced layout)

create_file_node() put all chunks as direct links on the root node — a flat 1-level tree. Kubo uses a balanced tree with a maximum of 174 links per node.

3. Wrong DAG-PB wire encoding (field ordering)

encode_dag_pb() used PBNode.SerializeToString() which emits Data before Links. The DAG-PB spec requires Links before Data for canonical encoding.

4. No persistent block storage

MemoryBlockStore was the only BlockStore implementation, meaning all fetched blocks were lost when the process exited.

5. No transparent caching layer

MerkleDag called bitswap.get_block() / bitswap.add_block() directly with no service layer, meaning fetched blocks were not auto-cached locally.

6. No stream input support

add_file() only accepted a file path string.

7. Magic integers in Bitswap message construction

create_wantlist_entry(cid, want_type=1) used raw integers with no type safety.

8. DHT record signing/verification not compatible with Kubo

DHT value records lacked proper signing and verification, causing incompatibility with Kubo's DHT implementation.

9. Missing Bitswap 1.3.0 Payment Support

There was no support for payment-gated file retrieval, block pricing engines, or payment ledgers.


How was it fixed?

Bitswap: Kubo CID compatibility

New create_leaf_node(data) in dag_pb.py:
Wraps each chunk in UnixFS Data(type=File) + PBNode — matches Kubo's RawLeaves=false default.

New balanced_layout(leaves) in dag_pb.py:
Groups leaves into batches of 174, builds a tree level by level.

Fixed encode_dag_pb() in dag_pb.py:
Manually constructs wire bytes with Links before Data.

Bitswap 1.3.0 Payment Extension

New Payment Architecture:
Fully implemented BitswapPaymentClient, PaymentLedger, and PaymentGatedDecisionEngine for tracking spent payments and managing paid data. Added BlockPricingEngine to allow dynamic block pricing strategies. Extracted payment logic into PaymentExtension using the IBitswapExtension interface.

Bitswap: batch fetching

Enhanced get_blocks_batch() in client.py:
Sends all CIDs in a single wantlist message per batch, waits for all responses on the same stream.

New: FilesystemBlockStore

New FilesystemBlockStore in block_store.py:
Stores each block as a file at <base>/<cid[:2]>/<cid[2:]>. Drop-in replacement for MemoryBlockStore.

New: BlockService

New block_service.py:
Transparent local→network fallback layer between MerkleDag and BitswapClient. Auto-caches network-fetched blocks locally.

New: add_stream() + chunk_stream()

New chunk_stream(stream: io.IOBase) in chunker.py and add_stream() in MerkleDag:
Reads one chunk at a time from any io.IOBase stream.

New: Wantlist / Message dataclasses

New wantlist.py with 6 typed dataclasses (WantType, BlockPresenceType, WantlistEntry, Wantlist, BlockPresence, BitswapMessage).

DHT: record signing and verification & ProviderQueryManager

Enhanced DHT record handling with proper signing and verification for compatibility with Kubo's DHT implementation. Added ProviderQueryManager for robust DHT-based provider discovery and caching in Bitswap.


Files changed

File Change
libp2p/bitswap/dag_pb.py create_leaf_node(), balanced_layout(), fixed encode_dag_pb() canonical ordering
libp2p/bitswap/dag.py Updated add_file(), add_bytes(), new add_stream(), BlockService routing
libp2p/bitswap/client.py Enhanced get_blocks_batch(), modularized with IBitswapExtension
libp2p/bitswap/chunker.py New chunk_stream(stream: io.IOBase)
libp2p/bitswap/block_store.py New FilesystemBlockStore
libp2p/bitswap/block_service.py New fileBlockService
libp2p/bitswap/wantlist.py New file — typed dataclasses
libp2p/bitswap/payment_extension.py New file — Bitswap 1.3.0 Payment implementation
libp2p/bitswap/messages.py create_wantlist_entry() accepts WantType | int
libp2p/bitswap/__init__.py Exports all new types
libp2p/kad_dht/ DHT signing/verification for Kubo compatibility, ProviderQueryManager
libp2p/records/ New filesrecord.py, utils.py
libp2p/peer/envelope.py, peer_record.py Updated for record signing
examples/bitswap/bitswap.py Updated example
examples/bitswap_payment_example.py New file — Example of payment-gated server

Test files added

File What it tests
test_unixfs_encoding.py dag-pb leaf encoding + balanced DAG layout
test_canonical_dag_pb.py DAG-PB canonical wire ordering (Links before Data)
test_filesystem_blockstore.py FilesystemBlockStore persistence + round-trip
test_block_service.py BlockService local hit / miss / auto-cache / announce
test_io_stream.py chunk_stream() + add_stream() with BytesIO, GzipFile, file handles
test_wantlist.py WantType, Wantlist, BitswapMessage, to_proto() / from_proto()

To-Do

  • Clean up commit history
  • Add or update documentation related to these changes
  • Add entry to the release notes

sumanjeet0012 and others added 12 commits April 14, 2026 15:55
…compatibility with ipfs kubo

- Added support for signed records in the DHT by introducing `make_signed_put_record` function.
- Updated `ValueStore` to create signed records when storing values.
- Enhanced `Envelope` class to handle raw payload types for peer records.
- Introduced utility functions for signing and verifying DHT records.
- Updated protobuf definitions to include author and signature fields in records.
- Improved logging and debug messages for better traceability.
…ce DAG-PB encoding

Co-authored-by: Copilot <copilot@github.com>
… layout

Co-authored-by: Copilot <copilot@github.com>
… in MerkleDag

Co-authored-by: Copilot <copilot@github.com>
…s and implement add_stream method in MerkleDag for handling io.IOBase streams

Co-authored-by: Copilot <copilot@github.com>
…improve chunk_stream documentation, and add Wantlist functionality

Co-authored-by: Copilot <copilot@github.com>
- Introduced `test_block_service.py` to validate BlockService behavior including local hits, network fetches, auto-caching, and block storage.
- Created `test_filesystem_blockstore.py` to manually test FilesystemBlockStore for basic operations, persistence, and directory structure.
- Added `test_io_stream.py` to verify io.IOBase input support with chunk_stream and MerkleDag.add_stream functionalities.
- Implemented `test_unixfs_encoding.py` to ensure add_file and add_bytes produce dag-pb leaf blocks and validate balanced layout tree structures.
- Developed `test_wantlist.py` to test Wantlist and Message dataclasses, including backward compatibility and public API exports.
- Updated type hints in `make_service` function to allow for None.
- Specified type hints for lists of bytes in block retrieval tests.
- Added assertions to check for non-null `unixfs` in various tests to ensure proper decoding of DAG PB blocks.
- Enhanced type hints for observer and subscriber peers in Gossipsub tests.
- Improved type hints for candidate lists in opportunistic grafting tests.
- Added type ignore comments for factory Meta classes to suppress type checker warnings.
- Updated import statements for ID to include type ignore comments in interop utilities.
@sumanjeet0012 sumanjeet0012 force-pushed the improvement/bitswap branch from 475086a to 6acceb2 Compare May 3, 2026 11:04
sumanjeet0012 and others added 10 commits May 3, 2026 22:41
…handling, and update tests for dag-pb leaf blocks

Co-authored-by: Copilot <copilot@github.com>
…ndling in DAG fetching

Co-authored-by: Copilot <copilot@github.com>
…leDag

Co-authored-by: Copilot <copilot@github.com>
…aching in Bitswap

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>

@yashksaini-coder yashksaini-coder 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.

@sumanjeet0012 Strong PR overall — the encoding root-causes are correct and well-tested (verified locally: 238/238 bitswap tests pass, wire format confirmed Links-before-Data). The PR description is exemplary.

Leaving a few comments inline. Three I'd flag as blocking before merge:

  1. Breaking API change in add_file default (wrap_with_directory=True) — old callers get a directory CID instead of a file CID for the same call. Either revert the default or call this out loudly in the newsfragment.
  2. verify_record only handles Ed25519 — silently fails for RSA/Secp256k1 peers, breaking DHT interop with non-Ed25519 nodes.
  3. DEFAULT_CHUNK_SIZE = 63 KiB - 32 doesn't match Kubo's 256 KiB default — files added by py-libp2p won't have the same root CID as ipfs add file.bin unless Kubo is told --chunker=size-65504. The PR header claims "Kubo CID compatibility"; this caveat needs to be documented.

Medium-priority items (perf and hygiene) inline. Everything else is comfortable post-merge cleanup.

Nice work on the manual DAG-PB outer envelope — limiting hand-rolling to 0x12 <varlen> <linkbytes> then 0x0a <varlen> <unixfs> while still using protobuf for the inner messages is the minimal, correct approach.

Comment thread libp2p/bitswap/dag.py Outdated
@@ -187,7 +272,7 @@ async def add_file(

dir_data = create_directory_node([(filename, cid, file_size)])
dir_cid = compute_cid_v1(dir_data, codec=CODEC_DAG_PB)

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.

Breaking API change (re: the wrap_with_directory=True default a few lines up at the parameter decl): defaulting this to True silently changes the behavior of add_file(path) for every existing caller — they'll now get a directory CID where they previously got a file CID, and fetch_file returns a (bytes, filename) tuple where it returned bytes before. This is invisible in the new tests because they all pass wrap_with_directory=False.

Suggest defaulting to False for back-compat, or version-bump and add a clear migration note to newsfragments/1321.feature.rst (currently doesn't flag this as breaking).

Comment thread libp2p/records/utils.py Outdated

"""
try:
public_key = Ed25519PublicKey.from_bytes(author_public_key)

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.

Interop bug for non-Ed25519 peers. This hard-codes Ed25519 deserialization, so DHT records signed by RSA or Secp256k1 peers will silently fail verification (caught by the try/except and returned as False, indistinguishable from a genuinely invalid signature).

Compare with libp2p/peer/envelope.py:198-216 (pub_key_from_protobuf) which dispatches on KeyType for all three types. Either dispatch the same way here, or accept a PublicKey object and let the caller deserialize.

Kubo defaults to Ed25519 today, so this won't surface in basic interop tests, but it's a real correctness gap.

Comment thread libp2p/bitswap/chunker.py Outdated
DEFAULT_CHUNK_SIZE = 63 * 1024
# 63 KB minus 32 bytes to leave room for the dag-pb leaf envelope overhead,
# ensuring wrapped blocks never exceed MAX_BLOCK_SIZE (63 * 1024).
DEFAULT_CHUNK_SIZE = 63 * 1024 - 32

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.

Mismatch with Kubo's default chunk size. Kubo's ipfs add uses size-262144 (256 KiB) by default. With this 63 KiB default, py-libp2p will produce a different root CID than ipfs add file.bin for the same content — contradicting the PR's "Kubo CID compatibility" headline.

Leaf CIDs match Kubo (because of RawLeaves=false + dag-pb wrapping), but the root over a multi-chunk file won't. Either:

  • Document this clearly in newsfragments/1321.feature.rst ("CIDs match ipfs add --chunker=size-65504"), or
  • Match Kubo's 256 KiB default and split large messages at the wire layer instead of capping the chunk size.

Comment thread libp2p/bitswap/dag.py Outdated
chunk = leaf_raw
logger.debug(f"[DAG] Leaf {idx + 1}: raw block {len(chunk)} bytes")

file_data += chunk

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.

O(n²) bytes concat. file_data += chunk over potentially thousands of leaves means each += allocates a new bytes object and copies all previous data. For a 100 MB file at 63 KB chunks (~1626 leaves), this allocates roughly 80 GB of intermediate strings.

The fix is one line — accumulate into a bytearray (or list + b"".join(parts) at the end). This is the single biggest perf win available in the PR.

Same pattern in _read_message (client.py:1017-1047) and encode_dag_pb (dag_pb.py:125-149).

await trio.to_thread.run_sync(
lambda: path.parent.mkdir(parents=True, exist_ok=True)
)
await trio.to_thread.run_sync(path.write_bytes, data)

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.

Non-atomic writes. path.write_bytes(data) writes in place. If the process crashes mid-write, the next startup finds a truncated file at a CID path. get_block will then return corrupted bytes that fail verification only if the caller checks.

Standard fix: write to path.with_suffix('.tmp'), then os.replace(tmp, path) — atomic on POSIX, durable on most filesystems.

Comment thread libp2p/bitswap/dag.py Outdated
# Ensure the result is a plain dict (not a coroutine from a mock)
if isinstance(result, dict):
return result
except Exception:

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.

Test infrastructure leaking into production code. The getattr probe + isinstance(result, dict) check + bare except Exception: pass is a workaround for MagicMock returning a coroutine object. This silently masks real failures in production.

Suggest defining a Protocol for the batch interface, typing self.bitswap with it, and removing the runtime probing entirely. Tests can then mock the protocol explicitly.

Comment thread libp2p/bitswap/client.py

# Send all CIDs in a single wantlist to the peer
if peer_id:
await self._send_wantlist_to_peer(peer_id, batch)

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.

Swallowed exception in _send_wantlist_to_peer causes hangs. That helper (further down in this file) catches all exceptions, logs "Failed to send wantlist to peer", and returns. When called from this batch path, if host.new_stream or _write_message fails for one CID in the batch, the corresponding _pending_requests[cid].wait() below will block until trio.fail_after(timeout) cancels it — adding the full timeout (default 30s) to every per-batch failure.

Fix: propagate the failure from _send_wantlist_to_peer, or event.set() with a sentinel so the waiter can fail fast.

Comment thread libp2p/bitswap/dag_pb.py
# We manually construct the wire format to enforce the correct ordering.

# Add links
result = b""

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.

Minor: same O(n²) concat pattern as fetch_file reassembly. With 174 links on an internal node and large CIDs, each result += ... allocates a fresh bytes. Trivial fix: build into a bytearray and convert to bytes at return.

Comment thread libp2p/bitswap/chunker.py
yield chunk


def chunk_stream(

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.

Nit: this new chunk_stream should also be added to the module's __all__ (further down in this file). Doesn't affect direct imports, but it breaks from chunker import * and confuses some IDE auto-import tools.

Comment thread libp2p/bitswap/dag.py Outdated
ordered_leaf_cids: list[bytes] = []

def _collect_leaves_local(cid_bytes: bytes, depth: int = 1) -> None:
"""Traverse locally-fetched blocks to collect leaf CIDs."""

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.

Minor: _collect_leaves_local is unbounded recursion. For a balanced 174-fanout DAG over a 100 MB file the depth is ~2, so this never trips Python's default 1000-frame limit in practice. But a maliciously crafted DAG (e.g. a chain of single-link nodes) would crash with RecursionError. Convert to an explicit stack if you want to harden this.

@Winter-Soren

Copy link
Copy Markdown
Contributor

PR #1321 AI Review

1. Summary of Changes

  • This PR introduces a large Bitswap/DAG feature set: batch wantlist fetching, BlockService, FilesystemBlockStore, stream ingestion (chunk_stream, add_stream), provider discovery manager, and Kubo-oriented DAG-PB/CID interoperability updates.
  • It also includes Kademlia record-signing changes, transport/test refactors, and multiple docs/newsfragment updates.
  • Related issue references present in branch history/newsfragments include #1321 and several prior issues merged in this branch lineage.
  • Affected architecture areas: libp2p.bitswap, libp2p.kad_dht, record/signature handling (libp2p.records), plus tests and docs.
  • Breaking change note is present in existing release notes/newsfragments around legacy CID helper migration (not this review's primary blocker, but user-facing and important).

2. Strengths

  • Strong feature depth for Bitswap interoperability and performance, with substantial test additions in tests/core/bitswap/.
  • Clear separation of responsibilities with new components (ProviderQueryManager, BlockService, Wantlist).
  • Good effort toward typed APIs and user-facing docs/newsfragments.
  • newsfragments/1321.feature.rst is present and formatted as expected.

3. Issues Found

Critical

  • None found with high confidence.

Major

  • File: libp2p/bitswap/provider_query.py

  • Line(s): Provider query path inside _query_single

  • Issue: ProviderQueryManager claims DHT provider discovery but calls self.dht.provider_store.get_providers(cid_bytes), which is a local provider-store read, not a network provider lookup. This can prevent remote provider discovery and silently degrade to fallback behavior.

  • Suggestion: Replace local-store read with the actual async DHT provider lookup path (KadDHT.find_providers(...)/equivalent network query API), then cache those remote results.

  • File: libp2p/kad_dht/value_store.py

  • Line(s): _store_at_peer

  • Issue: put() creates signed records (make_signed_put_record), but outbound PUT_VALUE RPC in _store_at_peer sets only record.key and record.value; signature/author fields are not propagated. This can break signed-record interoperability and weaken authenticity guarantees.

  • Suggestion: Build outbound RPC records from the signed record object (including signature/author fields) and add tests asserting these fields are present in serialized outbound Message.record.

Minor

  • No additional minor issues reported; review focused on behavior and correctness over cosmetic changes.

4. Security Review

  • Risk: Signed DHT record metadata not transmitted in outbound PUT_VALUE flow.
  • Impact: Medium (authenticity/interoperability degradation; possible rejection by stricter peers).
  • Mitigation: Ensure author and signature fields are serialized in outbound record messages and covered by unit/integration tests.

5. Documentation and Examples

  • Docs/newsfragment coverage is generally good for this PR scope.
  • However, provider discovery behavior documentation should match implementation: if discovery is advertised as DHT-network based, implementation/tests must prove network query behavior rather than local cache-only reads.

6. Newsfragment Requirement

  • Required newsfragment found: newsfragments/1321.feature.rst.
  • No blocker on newsfragment presence for PR #1321.

7. Tests and Validation

  • New tests are substantial, especially under tests/core/bitswap/.
  • Major test gap: current provider-query tests mostly mock local provider-store behavior and do not verify real DHT network provider lookup path.
  • Major test gap: no assertion that outbound PUT_VALUE includes signed record fields (author, signature).
  • Local validation results:
    • make pr failed in this environment due ruff issues under references/rust-libp2p-references/.../fix-unreachable-pub.py.
    • make linux-docs built docs but ended with environment-specific failure (xdg-open: No such file or directory).

8. Recommendations for Improvement

  • Switch provider discovery from local provider_store.get_providers reads to true DHT network querying and keep cache layer as an optimization.
  • Propagate full signed record fields in outbound DHT PUT_VALUE.
  • Add integration test proving provider discovery finds remote providers not already in local store.
  • Add unit test around _store_at_peer serialization ensuring signed fields are present.
  • Re-run CI checks in a clean Linux CI environment to separate repository issues from local tooling constraints.

9. Questions for the Author

  • Was ProviderQueryManager intentionally designed to read only local provider store state, or should it perform network DHT lookup?
  • Should signed PUT_VALUE interoperability with Kubo peers require transmitting author/signature on every outbound store operation?
  • Do you have an integration test scenario where a provider is discovered only via remote DHT query (not pre-populated local store)?

10. Overall Assessment

  • Quality Rating: Needs Work
  • Security Impact: Low to Medium
  • Merge Readiness: Needs fixes
  • Confidence: High

sumanjeet0012 and others added 5 commits May 6, 2026 23:19
… lookups and always send signed records.

Co-authored-by: Copilot <copilot@github.com>
- Added PaymentGatedDecisionEngine to handle payment-required logic for block serving.
- Introduced PaymentTerms, PaymentAuthorization, PaymentReceipt, and PaymentRejection messages in the new bitswap_1_3_0.proto.
- Enhanced existing MerkleDag class to store internal nodes with a callback for payment gating.
- Created BitswapPaymentClient_1_3 to manage client-side payment authorizations and receipts.
- Updated balanced_layout function to support payment gating and internal node storage.
- Added necessary protobuf definitions and generated Python files for Bitswap 1.3.0.
@acul71

acul71 commented May 17, 2026

Copy link
Copy Markdown
Contributor

Thanks @sumanjeet0012, full review here:

AI PR Review: #1321 — feat: Bitswap improvements for Kubo compatibility

PR: #1321
Branch: improvement/bitswap (26 commits ahead of origin/main, 0 behind at review time)
Reviewer: AI review (prompt: downloads/py-libp2p-pr-review-prompt.md)
Review version: 0


1. Summary of Changes

This PR is a large feature change centered on Bitswap / Merkle DAG interoperability with Kubo (go-ipfs) and supporting infrastructure:

Area What changed
DAG-PB / UnixFS Leaf blocks use dag-pb + UnixFS wrapping (create_leaf_node), balanced tree layout (balanced_layout, max 174 links), canonical wire encoding (Links before Data in encode_dag_pb).
Bitswap client Batch wantlist fetching, typed wantlist API (wantlist.py), optional ProviderQueryManager for DHT provider discovery in get_block().
Storage / caching FilesystemBlockStore, BlockService (local-first + auto-cache), chunk_stream / add_stream for io.IOBase inputs.
Kademlia / records Signed DHT put records, multi-key-type verify_record, outbound PUT_VALUE carries signature/author.
Tests / release Extensive new tests under tests/core/bitswap/ and tests/core/kad_dht/; newsfragments/1321.feature.rst.

Related issue: #1321 (same title/body as the PR; tracks Kubo compatibility work). The PR body does not include an explicit Fixes #1321 / Closes #1321 line — recommend adding one for auto-linking on merge.

Breaking / behavior changes (user-facing):

  • MerkleDag.add_file(..., wrap_with_directory=True) by default (was effectively file-root CID before).
  • MerkleDag.fetch_file() now returns tuple[bytes, str | None] (data + optional filename).
  • Default chunk size remains ~63 KiB − 32 B, not Kubo’s default 256 KiB — root CIDs differ from default ipfs add unless chunker is aligned.

Modules touched: libp2p.bitswap.*, libp2p.kad_dht.*, libp2p.records.*, libp2p.peer.envelope, examples/bitswap/bitswap.py.


2. Branch Sync Status and Merge Conflicts

Branch sync status

  • Status: ℹ️ Ahead of origin/main (no commits behind at review time).
  • Details: branch_sync_status.txt0 26 (0 commits on main not in HEAD; 26 commits on HEAD not in main).

Merge conflict analysis

No merge conflicts detected. Test merge of origin/main into the PR branch completed cleanly (merge_conflict_check.log).


3. Strengths

  • Root-cause analysis is correct: Raw leaf codec, flat DAG, and protobuf field ordering were real Kubo incompatibilities; the manual DAG-PB envelope (0x12 links then 0x0a data) is the right minimal fix.
  • Good layering: BlockService and FilesystemBlockStore separate concerns without forcing all callers to adopt them (MerkleDag(bitswap) still works).
  • Substantial automated tests: 238 bitswap tests pass locally; new coverage for BlockService, provider query manager, signed PUT_VALUE, multi-key verify_record, stream ingestion, and UnixFS layout.
  • Follow-up on prior review: Commits after May 6 address Winter-Soren’s provider-discovery and signed-record propagation findings (find_providers path, _store_at_peer uses message.record.CopyFrom(signed_record), multi-key verify_record).
  • Newsfragment present: newsfragments/1321.feature.rst with user-facing bullet list.
  • Local quality gates: make lint, make typecheck, and full make test all pass (2842 passed, 16 skipped).

4. Issues Found

Critical

None identified that would corrupt protocol state or introduce obvious remote code execution. CI docs job is failing (see §8) — treat as merge blocker until fixed.

Major

  • File: libp2p/bitswap/dag.py
  • Line(s): 202
  • Issue: Breaking API defaultwrap_with_directory=True changes the default CID returned by add_file(path) from file-root to directory-wrapper. Existing callers that do not pass wrap_with_directory=False get a different root CID and different fetch_file semantics. Reviewer @yashksaini-coder flagged this as blocking; it remains unresolved.
  • Suggestion: Default to False for backward compatibility, or document as breaking in newsfragments/1321.breaking.rst (and PR body) and add a migration note. All new tests pass wrap_with_directory=False, so regressions in default behavior are untested.

libp2p/bitswap/dag.py — lines 197–203

    async def add_file(
        self,
        file_path: str,
        chunk_size: int | None = None,
        progress_callback: Callable[[int, int, str], None] | None = None,
        wrap_with_directory: bool = True,
    ) -> bytes:

  • File: libp2p/bitswap/chunker.py, newsfragments/1321.feature.rst
  • Line(s): 16–16 (chunker); newsfragment line 4
  • Issue: “Kubo CID compatibility” is overstated for multi-chunk files. DEFAULT_CHUNK_SIZE = 63 * 1024 - 32 does not match Kubo’s default size-262144 (256 KiB). Leaf blocks can match Kubo (RawLeaves=false + dag-pb), but root CIDs for chunked files differ unless Kubo uses --chunker=size-65504. The newsfragment claims “identical CIDs to Kubo's ipfs add” without this caveat.
  • Suggestion: Narrow the newsfragment claim (e.g. “wire-compatible dag-pb layout; root CID matches Kubo when using the same chunk size”) or adopt/document Kubo’s 256 KiB default and enforce MAX_BLOCK_SIZE at the wire layer.

libp2p/bitswap/chunker.py — lines 13–16

# Default chunk size: 63 KB (py-libp2p accepts less than 64 KB)
# 63 KB minus 32 bytes to leave room for the dag-pb leaf envelope overhead,
# ensuring wrapped blocks never exceed MAX_BLOCK_SIZE (63 * 1024).
DEFAULT_CHUNK_SIZE = 63 * 1024 - 32

  • File: libp2p/records/utils.py
  • Line(s): 73–76 (docstring)
  • Issue: Documentation build failure — Sphinx/docutils error on verify_record docstring (Unexpected indentation / block-quote warning). CI: tox (3.10, docs) and Read the Docs fail on this branch.
  • Suggestion: Fix the author_public_key parameter description (avoid indented parenthetical under Args; use a single line or blank line before continuation). Re-run make linux-docs.

  • File: PR description / GitHub linking
  • Line(s): N/A
  • Issue: No explicit Fixes #1321 (or Closes #1321) in the PR body. Issue feat: Bitswap improvements for Kubo compatibility #1321 exists and matches scope; newsfragment uses 1321.feature.rst.
  • Suggestion: Add Fixes #1321 to the PR description for maintainer workflow and auto-close.

Minor

  • File: libp2p/bitswap/dag.py
  • Line(s): 813
  • Issue: O(n²) reassemblyfile_data += chunk in fetch_file allocates repeatedly; significant cost for large multi-chunk files (~1626 chunks × 63 KiB).
  • Suggestion: Use bytearray or list + b"".join().

libp2p/bitswap/dag.py — lines 809–814

            else:
                chunk = leaf_raw
                logger.debug(f"[DAG] Leaf {idx + 1}: raw block {len(chunk)} bytes")

            file_data += chunk
            bytes_fetched += len(chunk)

  • File: libp2p/bitswap/dag_pb.py
  • Line(s): 125–149
  • Issue: Same O(n²) pattern in encode_dag_pb (result += ...).
  • Suggestion: Build with bytearray or pre-sized buffer.

  • File: libp2p/bitswap/client.py
  • Line(s): 522–523
  • Issue: _send_wantlist_to_peer swallows all exceptions after logging. In batch get_blocks_batch, a failed wantlist send can leave waiters blocked until the full per-batch timeout.
  • Suggestion: Propagate failure, set pending events with error, or fail fast.

libp2p/bitswap/client.py — lines 522–523

        except Exception as e:
            logger.error(f"Failed to send wantlist to peer {peer_id}: {e}")

  • File: libp2p/bitswap/dag.py
  • Line(s): 169–181
  • Issue: Test/mock workaround in productiongetattr + bare except Exception: pass around get_blocks_batch masks real failures.
  • Suggestion: Type bitswap with a Protocol for batch fetch; remove runtime probing.

  • File: libp2p/bitswap/block_store.py
  • Line(s): 168–174
  • Issue: Non-atomic put_block — crash mid-write_bytes can leave truncated block files.
  • Suggestion: Write to .tmp then os.replace.

  • File: tests (missing file named in PR description)
  • Line(s): N/A
  • Issue: PR description lists test_canonical_dag_pb.py for wire ordering; no such file exists. test_dag_pb.py round-trips encode/decode but does not assert that link tags (0x12) precede data tags (0x0a) on the wire.
  • Suggestion: Add an explicit assertion (e.g. first field tag is 0x12 when links present) to lock the Kubo compatibility fix.

  • File: libp2p/bitswap/chunker.py
  • Line(s): __all__ (module end)
  • Issue: chunk_stream not exported in __all__ (nit from reviewer).
  • Suggestion: Add "chunk_stream" to __all__.

  • File: PR checklist
  • Line(s): To-Do in PR body
  • Issue: Author TODOs still open: clean commit history, update user-facing docs beyond newsfragment, release notes entry (partially done via newsfragment).
  • Suggestion: Complete or tick before merge.

Maintainer feedback status

Reviewer Topic Status
@yashksaini-coder Ed25519-only verify_record ✅ Fixed (_unmarshal_public_key + tests)
@yashksaini-coder Provider local-store vs network ✅ Fixed (provider_store.find_providers in _query_single; tests assert find_providers called)
@yashksaini-coder Signed PUT outbound ✅ Fixed (_store_at_peer + unit tests)
@yashksaini-coder wrap_with_directory=True default Open
@yashksaini-coder Chunk size vs Kubo default Open (undocumented in newsfragment)
@yashksaini-coder Perf / atomic writes / wantlist errors Open (acceptable post-merge per reviewer, except docs CI)
Winter-Soren (AI) Provider + signed PUT ✅ Addressed in later commits

5. Security Review

Risk Impact Mitigation
Signed DHT records now verified with multi-key support Low (improvement) Keep tests for tampered payloads and invalid protobuf authors.
verify_record returns False on any exception (broad except) Low Acceptable for verification API; log at debug if diagnosing interop failures.
Truncated blocks on disk after crash Low–Medium (integrity) Atomic write + optional CID re-hash on read for production stores.
Malicious deep/single-link DAG causing RecursionError in _collect_leaves_local Low Use iterative traversal for untrusted CIDs.
No new obvious RCE or auth bypass in Bitswap message parsing Continue relying on block size limits and existing stream handling.

Overall security impact: Low (incremental hardening on DHT signing; no new high-risk surfaces identified).


6. Documentation and Examples

Present:

  • Module/docstrings on new APIs (BlockService, FilesystemBlockStore, ProviderQueryManager, verify_record).
  • Updated examples/bitswap/bitswap.py.
  • newsfragments/1321.feature.rst.

Gaps:

  • Newsfragment overclaims full ipfs add CID parity (chunk size caveat missing).
  • No dedicated user guide for Kubo interop (chunk size, wrap_with_directory, BlockService wiring) — PR TODO acknowledges this.
  • Sphinx build broken on verify_record docstring (blocks docs CI).
  • PR-listed test_canonical_dag_pb.py documentation does not match repo (see §4 Minor).

7. Newsfragment Requirement

Check Result
File exists newsfragments/1321.feature.rst
Naming 1321.feature.rst
User-facing content ✅ Bullet list
Newline at EOF ✅ (assumed from lint pass)
Type appropriateness ⚠️ Consider .breaking.rst if wrap_with_directory=True default is kept
Issue link in PR ⚠️ Issue #1321 exists; add Fixes #1321 to PR body

Not a blocker on newsfragment presence alone; blocker if breaking default is shipped without .breaking.rst or default revert.


8. Tests and Validation

Linting (make lint)

  • Exit code: 0
  • Result: All hooks passed (ruff, mypy-local, pyrefly-local, mdformat, etc.).
  • Log: downloads/AI-PR-REVIEWS/1321/lint_output.log

Type checking (make typecheck)

  • Exit code: 0
  • Result: mypy + pyrefly passed.
  • Log: downloads/AI-PR-REVIEWS/1321/typecheck_output.log

Tests (make test)

  • Exit code: 0
  • Summary: 2842 passed, 16 skipped, ~118 s
  • Bitswap subset: 238 passed in ~8.4 s
  • Log: downloads/AI-PR-REVIEWS/1321/test_output.log

Documentation (make linux-docs)

  • Exit code: 2 (failed)
  • Errors:
    • libp2p/records/utils.py:docstring of libp2p.records.utils.verify_record:12: ERROR: Unexpected indentation.
    • libp2p/records/utils.py:docstring ...:13: WARNING: Block quote ends without a blank line
  • Sphinx treats warnings as errors (-W).
  • Log: downloads/AI-PR-REVIEWS/1321/docs_output.log

CI (GitHub Actions, PR head)

Check Status
tox (3.10–3.13, core) ✅ pass
tox (*, lint) ✅ pass
tox (3.10, docs) fail
Read the Docs fail
Windows core ✅ pass

9. Recommendations for Improvement

  1. Unblock CI: Fix verify_record docstring RST formatting; re-run docs tox.
  2. Resolve reviewer blockers: Revert wrap_with_directory default to False or ship .breaking.rst + migration text; document chunk-size vs Kubo in newsfragment and chunker.py module docstring.
  3. Performance (quick wins): bytearray in fetch_file and encode_dag_pb; atomic writes in FilesystemBlockStore.
  4. Reliability: Propagate or surface errors from _send_wantlist_to_peer; remove MagicMock-oriented fallback in _get_blocks_batch.
  5. Tests: Add explicit canonical wire-order test (0x12 before 0x0a); optional integration test with Kubo/ipfs add for same chunker.
  6. Process: Add Fixes #1321 to PR body; squash/clean 26 commits if maintainers require linear history.

10. Questions for the Author

  1. Is wrap_with_directory=True as default intentional for IPFS parity, and are you willing to call it breaking in release notes?
  2. Should the project standardize on 63 KiB chunks permanently, or align with Kubo’s 256 KiB default over time?
  3. For ProviderQueryManager, is provider_store.find_providers (network DHT walk) sufficient, or should callers use KadDHT.find_providers at the host API level for consistency?
  4. Do you plan Kubo interop integration tests (dockerized ipfs add / ipfs cat) in a follow-up PR?
  5. Will you fix the Sphinx docstring in this PR or a fast follow-up?

11. Overall Assessment

Metric Rating
Quality Good — strong technical core; remaining issues are mostly API contract, docs CI, and performance hygiene
Security impact Low
Merge readiness Needs fixes — docs CI failure + unresolved blocking review on wrap_with_directory default and CID/chunk-size messaging
Confidence High (full local test run + code review on PR branch; CI docs failure reproduced locally)

Summary: This PR materially fixes real Kubo Bitswap/DHT interoperability gaps and adds valuable infrastructure (BlockService, persistent store, batch wants). Earlier major review findings (signed PUT propagation, network provider lookup, multi-key verification) appear addressed. Before merge: fix docs build, resolve wrap_with_directory default / breaking disclosure, and correct newsfragment claims about chunk size and root CID parity with default Kubo ipfs add.

- Added `payment_client_1_3.py` for handling in-band payment messages, including:
  - Processing PaymentTerms and sending PaymentAuthorization.
  - Handling PaymentReceipts and PaymentRejections.
  - Validating payment amounts and signing EIP-3009 transactions.

- Introduced `payment_ledger.py` for tracking payments at the root CID level:
  - Supports registration of DAG structures for child blocks.
  - Implements nonce deduplication to prevent replay attacks.
  - Provides methods to check payment status and record payments.

- Updated protobuf definitions in `bitswap_1_3_0_pb2.pyi` to reflect new payment structures:
  - Added PaymentTerms, PaymentAuthorization, and PaymentReceipt messages.
  - Introduced TxReceipt for transaction details.

- Created `pricing_engine.py` to compute block pricing based on configurable strategies:
  - Supports free, fixed, size-based, and custom pricing strategies.
  - Allows marking specific CIDs as free and setting per-CID prices.
- Added PaymentExtension class to handle payment-related protobuf fields and wantlists in Bitswap 1.3.0.
- Integrated payment terms, receipts, and rejections processing for client-side and server-side.
- Enhanced PaymentLedger to track root CID payments and manage payment records.
- Updated pricing engine to support configurable pricing strategies.
- Refactored tests to accommodate changes in block storage and encoding, ensuring raw blocks are used for leaves.
- Improved type hints and documentation across the codebase for better clarity and maintainability.
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