Concrete examples of creating, seeding, downloading, and interop-testing BitTorrent v2 (BEP 52) torrents with SpawnDev.WebTorrent.
Background: BEP 52 introduces SHA-256 piece hashing with a Merkle tree over 16 KiB leaves instead of v1's flat SHA-1 per-piece list. Benefits: file-level integrity, per-block verification during streaming, larger pieces without loss of safety. Hybrid v1+v2 torrents carry both hash sets in one info dict so legacy (libtorrent 1.2 / qBittorrent 4.x) and modern (libtorrent 2.0+) clients can seed the same swarm. Pure v2 torrents carry only the v2 tree and require a v2-aware client.
| Flavor | xt= form |
Parser sees | Info-hash | Use when |
|---|---|---|---|---|
| v1-only | urn:btih:<40-hex-SHA1> |
pieces (flat SHA-1s) |
v1 only | Back-compat with legacy ecosystem |
| Pure v2 | urn:btmh:1220<64-hex-SHA256> |
file tree + piece layers + meta version=2 |
v2 only | You control both ends, want per-block integrity |
| Hybrid | Both xt=urn:btih:...&xt=urn:btmh:1220... |
Both sets in one info dict (plus pad files) | v1 AND v2 | You want everyone to be able to download |
Single-file torrent from in-memory bytes — same API shape, different options:
using SpawnDev.WebTorrent;
var data = File.ReadAllBytes("model.onnx");
var trackers = new[] { "wss://hub.spawndev.com:44365/announce", "wss://tracker.openwebtorrent.com" };
// v1-only (classic SHA-1)
var (v1Bytes, v1Meta) = TorrentCreator.CreateFromBytes("model.onnx", data,
new TorrentCreatorOptions
{
PieceLength = 65536,
Trackers = trackers,
HashAlgorithm = "SHA-1",
});
// Pure v2 (BEP 52 Merkle only)
var (v2Bytes, v2Meta) = TorrentCreator.CreateFromBytes("model.onnx", data,
new TorrentCreatorOptions
{
PieceLength = 65536,
Trackers = trackers,
MetaVersion = 2,
Hybrid = false,
});
// Hybrid v1+v2 - recommended for general distribution
var (hybridBytes, hybridMeta) = TorrentCreator.CreateFromBytes("model.onnx", data,
new TorrentCreatorOptions
{
PieceLength = 65536,
Trackers = trackers,
MetaVersion = 2,
Hybrid = true,
});
Console.WriteLine($"v1 infoHash = {v1Meta.InfoHash}");
Console.WriteLine($"v2 infoHash = {v2Meta.V2InfoHash}");
Console.WriteLine($"hybrid v1 = {hybridMeta.InfoHash}");
Console.WriteLine($"hybrid v2 = {hybridMeta.V2InfoHash}");Multi-file works the same way via TorrentCreator.CreateFromMultipleFiles or, for streams larger than RAM (ML model shards, etc.), TorrentCreator.CreateFromMultipleStreamsAsync which is memory-bounded at ~1 piece + incremental Merkle state regardless of input size.
Pass the torrent bytes to the client and it'll serve whatever the peer asks for — v1 clients get v1 pieces, v2 clients get v2 pieces, hybrid clients pick whichever mode they negotiated via BEP 10 extension handshake:
var client = new WebTorrentClient();
var torrent = await client.SeedAsync("model.onnx", data,
new TorrentCreatorOptions
{
MetaVersion = 2,
Hybrid = true, // Reach every peer in the ecosystem
Trackers = trackers,
});
// Share the magnet URI (includes both v1 and v2 xt= for hybrid torrents).
Console.WriteLine(torrent.ComputedMagnetUri);
// magnet:?xt=urn:btih:<v1hex>&xt=urn:btmh:1220<v2hex>&dn=model.onnx&tr=...Downloader side is even simpler - the client handles flavor detection automatically:
var client = new WebTorrentClient();
// Any of these work - v1, pure-v2, or hybrid magnet
var magnet = "magnet:?xt=urn:btmh:1220abc...&dn=pure-v2-model";
var torrent = client.Add(magnet);
// Wait for metadata if starting from magnet (pieces are SHA-256-verified automatically
// via VerifyPieceHash → MerkleHasher.ComputePieceLayer for v2 / hybrid)
while (!torrent.HasMetadata) await Task.Delay(100);
Console.WriteLine($"MetaVersion: {torrent.MetaVersion}"); // 0 for v1-only, 2 for v2 / hybrid
Console.WriteLine($"WireInfoHashHex: {torrent.WireInfoHashHex}"); // What goes on the tracker wire
Console.WriteLine($"Display name: {torrent.DisplayName}"); // Name ?? WireInfoHashHex ?? "unknown"
// Wait for completion
while (!torrent.Done) await Task.Delay(1000);Internally Torrent.VerifyPieceHash(index, buf) branches on MetaVersion:
MetaVersion == 2(pure v2 or hybrid): the stored hash is the piece-layer Merkle root over the 16 KiB leaves inside the piece.MerkleHasher.ComputePieceLayer(buf, PieceLength)recomputes the single root for this piece from its leaves; matches against the stored 32-byte root. Not a flat SHA-256 of the whole piece - that would always mismatch.MetaVersion == 0(classic v1 or Phase 1 SHA-256): flat SHA-1 (20-byte stored hash) or flat SHA-256 (32-byte stored hash) over the whole piece. Auto-detected from the stored hash length.
Missing piece-layer hashes mid-download? The client fetches them via BEP 52 peer-wire messages 21 (hash_request), 22 (hashes), 23 (hash_reject). V2HashRequestCoordinator handles the state machine; MerkleProofBuilder on the seed side serves them.
Pure-v2 torrents have Torrent.InfoHash == "" (no v1 hash). The client and trackers still need a 20-byte identity for the wire handshake + tracker announce. SpawnDev follows the libtorrent / qBittorrent / rqbit convention: use the first 20 bytes of the v2 SHA-256 hash. Torrent.WireInfoHashHex returns this automatically:
- v1 / hybrid →
WireInfoHashHex == InfoHash - pure-v2 →
WireInfoHashHex == V2InfoHash[..40].ToLowerInvariant()
All SpawnDev storage (OPFS directory names, service-worker stream URLs, tracker RoomKey) keys on WireInfoHashHex, so pure-v2 torrents get a consistent 40-char identity end-to-end.
Automated qBittorrent Web UI interop driver: interop_test/qbittorrent_interop.cs. Generates SpawnDev v1 + pure-v2 + hybrid torrents, adds them to a running qBittorrent instance via its REST API, force-rechecks pieces against a known payload, asserts 100% verification.
Run against qBittorrent 5.x (libtorrent 2.0):
cd interop_test
dotnet run gen_qbittorrent_test.cs # produces output/*.torrent + payload.bin
dotnet run qbittorrent_interop.cs --host localhost --port 8080 --user admin --pass <yours>Expected output:
qBittorrent 5.1.4 / libtorrent 2.0.10.0 / BEP 52 v2 capable: YES
spawndev_v1: PASS (v1-only SHA-1)
spawndev_v2: PASS (pure v2 SHA-256 Merkle)
spawndev_hybrid: PASS (hybrid v1+v2)
Against qBittorrent 4.x (libtorrent 1.2) the script auto-skips pure-v2 (parser can't handle v2-only info dicts) and flags hybrid's missing v2 hash as an expected (libtorrent 1.2 v1-only parse) result rather than a mismatch. The v1 and hybrid-back-compat paths still pass.
- BEP 52 spec
- libtorrent 2.0 release notes
Docs/bep52.md— full end-state overviewDocs/bep-support.md— BEP-by-BEP implementation statusPlans/PLAN-BEP52-External-Interop.md— interop runbook + manual test stepsPlans/bep52-phase2-execution.md— Phase 2 implementation history