Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
76c9384
adds multicodec for SHA1 and for torrent info hash
marcinczenko Feb 28, 2025
6938ba2
adds TorrentInfoHashNamespace
marcinczenko Feb 28, 2025
e4a04f3
adds Torrent BlockType
marcinczenko Feb 28, 2025
d85b146
advertising torrent info hash on DHT
marcinczenko Feb 28, 2025
494765b
general idea of BitTorrent integration
marcinczenko Feb 28, 2025
e423662
makes sure that torrent info_hash cids are not validated in block con…
marcinczenko Feb 28, 2025
4981d5b
temporarily reuse existing torrent-info multicodec
marcinczenko Feb 28, 2025
cd8d2b1
moves BitTorrent stuff around a bit to de-clutter node.nim
marcinczenko Mar 1, 2025
0d9bad6
adds torrent streaming and piece validation
marcinczenko Mar 2, 2025
bcf622e
formatting
marcinczenko Mar 2, 2025
8a8bab3
adds info hash validation
marcinczenko Mar 4, 2025
d592898
adds API for streaming torrents
marcinczenko Mar 4, 2025
6c58165
simplifies torrent streaming API
marcinczenko Mar 4, 2025
2ab59f6
makes torrent API ready for torrents v2, closes torrent streaming loop
marcinczenko Mar 5, 2025
fece905
adds torrent uploading API
marcinczenko Mar 5, 2025
3a7f18d
fix logging
marcinczenko Mar 5, 2025
ddc8e58
fixes some basic decoding and padding mistakes
marcinczenko Mar 10, 2025
096dff0
makes cleaner streaming API and updates return type of retrieveInfoHa…
marcinczenko Mar 11, 2025
f0a9306
adds BitTorrent specific constants
marcinczenko Mar 12, 2025
fe1cc15
adds JSON serialization to BitTorrent manifest
marcinczenko Mar 12, 2025
ff04de5
moves initializer around to prevent circ deps
marcinczenko Mar 12, 2025
249b799
adds BitTorrent REST API to fetch torrent manifest only
marcinczenko Mar 12, 2025
008b895
adds integration tests for BitTorrent
marcinczenko Mar 12, 2025
95daf37
removes redundant echos
marcinczenko Mar 12, 2025
ecb435b
adds bittorrent integration tests to to CI
marcinczenko Mar 12, 2025
040341b
removes duplication while streaming torrent content
marcinczenko Mar 12, 2025
6e18aef
makes integration tests using two client nodes
marcinczenko Mar 12, 2025
93befc9
fine tunes logging while aggregating pieces
marcinczenko Mar 14, 2025
42f4aa2
sets the default piece length to be 256KiB
marcinczenko Mar 14, 2025
5846fbc
improves exception handling
marcinczenko Mar 16, 2025
96d28e8
adds "==" operator to torrent manifest
marcinczenko Mar 17, 2025
0748a34
makes torrent decode func from openArray data public
marcinczenko Mar 17, 2025
45ab5ee
adds torrent piece validator abstraction to keep streaming in sync wi…
marcinczenko Mar 20, 2025
9903473
updates pieceValidator with better internal state management
marcinczenko Mar 20, 2025
1d47cf4
Updates client streaming to use new piece validator interface
marcinczenko Mar 20, 2025
927e67e
updates torrent streaming to take advantage of the new interface of t…
marcinczenko Mar 20, 2025
9707a44
adds piece validator tests to codex tests
marcinczenko Mar 20, 2025
1b4f045
updates bittorrent tests to use unittest2
marcinczenko Mar 20, 2025
b0ec507
ignore "./data*" folders from git
marcinczenko Mar 22, 2025
cbaace3
fixing problems after rebasing
marcinczenko Mar 23, 2025
42b74cc
uses MultiHash as a type for piece hashes (instead alias)
marcinczenko Mar 24, 2025
774f6d2
fixes checking block results
marcinczenko Mar 24, 2025
2e77c4a
removes unused import
marcinczenko Mar 24, 2025
8616619
adds push raises to a couple of new files
marcinczenko Mar 25, 2025
ea3cfe0
refactor piece validator tests
marcinczenko Mar 25, 2025
50ca6da
adds torrent downloader abstraction
marcinczenko Mar 24, 2025
526e3f1
adds testing setup for torrentdownloader
marcinczenko Mar 25, 2025
ee2f403
do not randomize piece fetching sequence for now
marcinczenko Mar 25, 2025
c859250
adds some good weather tests for torrentdownloader
marcinczenko Mar 26, 2025
2aa91e5
adds more tests and ability to retrieve the downloaded blocks
marcinczenko Mar 26, 2025
534cb71
updates and integrates torrentdownloader into api and node
marcinczenko Mar 27, 2025
414906a
updates copyright
marcinczenko Mar 27, 2025
5abce81
checked exceptions in stores
marcinczenko Mar 29, 2025
b0be205
better exception handling
marcinczenko Mar 29, 2025
edf4627
better exception handling in node
marcinczenko Mar 29, 2025
4d5dfb1
uses SafeAsyncIter in "listBlocks" and in "getBlockExpirations"
marcinczenko Mar 30, 2025
6000ae5
uses SafeAsyncIter to stream the blocks in api
marcinczenko Mar 31, 2025
e3fd2fa
fix failing test in torrentdownloader
marcinczenko Mar 31, 2025
6c32f6b
gets rid of ugly casts
marcinczenko Mar 31, 2025
f9d2b21
adds more tests to torrentdownloader
marcinczenko Mar 31, 2025
85e0e58
gets rid of future casting in torrent downloader
marcinczenko Mar 31, 2025
912479e
Uploading directories - first draft.
marcinczenko Apr 17, 2025
6852c1f
moves tarball-related stuff out of node + adds explicit padding argum…
marcinczenko Apr 26, 2025
6aad595
Cleans up directory manifest and tartballs
marcinczenko Apr 26, 2025
2e6d7d0
adds directory downloader
marcinczenko Apr 26, 2025
108370e
adds API for retrieving directory
marcinczenko Apr 26, 2025
8ac8f94
convenience feature - download content using magnet links
marcinczenko May 19, 2025
03ff25b
Updates magnet link tests
marcinczenko May 28, 2025
0f62624
adds torrent parser to support native torrent files when downloading
marcinczenko May 28, 2025
f9ce889
adds magent link and torrent parser tests to bittorrent tests
marcinczenko May 28, 2025
29bb017
adds integration tests for using magnet links and torrent file while …
marcinczenko Jun 2, 2025
ff40917
removes not needed err from "without" in repostore test
marcinczenko Jun 2, 2025
0935abb
checking if `auto` in the block expiration test has any impact on lib…
marcinczenko Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ docker/prometheus-data
nim.cfg
tests/integration/logs

data/
data*/
29 changes: 29 additions & 0 deletions codex/bittorrent/bencoding.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Nim-Codex
## Copyright (c) 2025 Status Research & Development GmbH
## Licensed under either of
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
## at your option.
## This file may not be copied, modified, or distributed except according to
## those terms.

{.push raises: [].}

import std/strformat

import pkg/stew/byteutils

func bencode*(value: uint64): seq[byte] =
fmt"i{value}e".toBytes

func bencode*(value: int64): seq[byte] =
fmt"i{value}e".toBytes

func bencode*(value: openArray[byte]): seq[byte] =
fmt"{value.len}:".toBytes & @value

func bencode*(value: string): seq[byte] =
bencode(value.toBytes)

proc bencode*[T: not byte](value: openArray[T]): seq[byte] =
fmt"l{value.mapIt(bencode(it).toString).join}e".toBytes
107 changes: 107 additions & 0 deletions codex/bittorrent/magnetlink.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import std/strutils
import std/sequtils

import pkg/stew/byteutils
import pkg/libp2p/[multicodec, multihash]
import pkg/questionable
import pkg/questionable/results

import ../errors
import ./manifest/manifest

type
TorrentVersion* = enum
v1
v2
hybrid

MagnetLink* = ref object
version: TorrentVersion
infoHashV1: ?MultiHash
infoHashV2: ?MultiHash

proc version*(self: MagnetLink): TorrentVersion =
## Get the version of the magnet link
##
## returns: the version of the magnet link
result = self.version

proc infoHashV1*(self: MagnetLink): ?MultiHash =
## Get the info hash of the magnet link
##
## returns: the info hash of the magnet link
result = self.infoHashV1

proc infoHashV2*(self: MagnetLink): ?MultiHash =
## Get the info hash of the magnet link
##
## returns: the info hash of the magnet link
result = self.infoHashV2

proc parseMagnetLink(link: string): ?!MagnetLink =
let prefix = "magnet:?"
if not link.startsWith(prefix):
return failure("Invalid magnet link format (missing 'magnet:?' prefix)")
let infoHashParts = link[prefix.len .. ^1].split("&").filterIt(it.startsWith("xt="))
if infoHashParts.len < 1:
return
failure("Invalid magnet link format (at least one info hash part is required)")
let v1Prefix = "xt=urn:btih:"
let v2Prefix = "xt=urn:btmh:"
var infoHashV1 = none(MultiHash)
var infoHashV2 = none(MultiHash)
for infoHashPart in infoHashParts:
# var a = infoHashPart[v1Prefix.len .. ^1]
if infoHashPart.startsWith(v1Prefix):
without infoHash =? BitTorrentInfo.buildMultiHash(
infoHashPart[v1Prefix.len .. ^1]
), err:
return failure("Error parsing info hash: " & err.msg)
infoHashV1 = some(infoHash)
elif infoHashPart.startsWith(v2Prefix):
without infoHash =? BitTorrentInfo.buildMultiHash(
infoHashPart[v2Prefix.len .. ^1]
), err:
return failure("Error parsing info hash: " & err.msg)
infoHashV2 = some(infoHash)

if infoHashV1.isNone and infoHashV2.isNone:
return failure("Invalid magnet link format (missing info hash part)")

var version: TorrentVersion
if infoHashV1.isSome and infoHashV2.isSome:
version = TorrentVersion.hybrid
elif infoHashV1.isSome:
version = TorrentVersion.v1
else:
version = TorrentVersion.v2

let magnetLink =
MagnetLink(version: version, infoHashV1: infoHashV1, infoHashV2: infoHashV2)
return success(magnetLink)

proc getHashHex(multiHash: MultiHash): string =
## Get the info hash of the magnet link as a hex string
result = byteutils.toHex(multiHash.data.buffer[multiHash.dpos .. ^1]).toUpperAscii()

proc `$`*(self: MagnetLink): string =
## Convert the magnet link to a string
##
## returns: the magnet link as a string
if self.version == TorrentVersion.hybrid:
result =
"magnet:?xt=urn:btih:" & (!self.infoHashV1).getHashHex() & "&xt=urn:btmh:" &
(!self.infoHashV2).hex
elif self.version == v1:
result = "magnet:?xt=urn:btih:" & (!self.infoHashV1).getHashHex()
else:
result = "magnet:?xt=urn:btmh:" & (!self.infoHashV2).hex

proc newMagnetLink*(magnetLinkString: string): ?!MagnetLink =
## Create a new magnet link
##
## version: the version of the magnet link
## magnetLinkString: text containing the magnet link
##
## returns: a Result containing a magnet link object or a failure
parseMagnetLink(magnetLinkString)
5 changes: 5 additions & 0 deletions codex/bittorrent/manifest.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ./manifest/manifest
import ./manifest/encoding
import ./manifest/decoding

export manifest, encoding, decoding
90 changes: 90 additions & 0 deletions codex/bittorrent/manifest/decoding.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Nim-Codex
## Copyright (c) 2021 Status Research & Development GmbH
## Licensed under either of
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
## at your option.
## This file may not be copied, modified, or distributed except according to
## those terms.

{.push raises: [].}

import pkg/libp2p/cid
import pkg/libp2p/multihash
import pkg/libp2p/protobuf/minprotobuf

import pkg/questionable/results

import ../../blocktype
import ./manifest

func decode*(_: type BitTorrentManifest, data: openArray[byte]): ?!BitTorrentManifest =
# ```protobuf
# Message BitTorrentManifest {
# Message Piece {
# bytes data = 1;
# }
#
# Message BitTorrentInfo {
# uint32 length = 1;
# uint32 pieceLength = 2;
# repeated Piece pieces = 3;
# optional string name = 4;
# }
#
# BitTorrentInfo info = 1;
# bytes codexManifestCid = 2;
# ```

var
pbNode = initProtoBuffer(data)
pbInfo: ProtoBuffer
length: uint64
pieceLength: uint32
pieces: seq[MultiHash]
piecesBytes: seq[seq[byte]]
name: string
cidBuf = newSeq[byte]()
codexManifestCid: Cid

if pbNode.getField(1, pbInfo).isErr:
return failure("Unable to decode `info` from BitTorrentManifest")

if pbInfo.getField(1, length).isErr:
return failure("Unable to decode `length` from BitTorrentInfo")

if pbInfo.getField(2, pieceLength).isErr:
return failure("Unable to decode `pieceLength` from BitTorrentInfo")

if ?pbInfo.getRepeatedField(3, piecesBytes).mapFailure:
for piece in piecesBytes:
var pbPiece = initProtoBuffer(piece)
var dataBuf = newSeq[byte]()
if pbPiece.getField(1, dataBuf).isErr:
return failure("Unable to decode piece `data` to MultiHash")
without mhash =? MultiHash.init(dataBuf).mapFailure, err:
return failure(err.msg)
pieces.add(mhash)
discard ?pbInfo.getField(4, name).mapFailure

if ?pbNode.getField(2, cidBuf).mapFailure:
without cid =? Cid.init(cidBuf).mapFailure, err:
return failure(err.msg)
codexManifestCid = cid

let info = BitTorrentInfo(
length: length,
pieceLength: pieceLength,
pieces: pieces,
name: if name.len > 0: name.some else: string.none,
)
BitTorrentManifest(info: info, codexManifestCid: codexManifestCid).success

func decode*(_: type BitTorrentManifest, blk: Block): ?!BitTorrentManifest =
## Decode a manifest using `decoder`
##

if not ?blk.cid.isTorrentInfoHash:
return failure "Cid not a torrent info hash codec"

BitTorrentManifest.decode(blk.data)
59 changes: 59 additions & 0 deletions codex/bittorrent/manifest/encoding.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Nim-Codex
## Copyright (c) 2021 Status Research & Development GmbH
## Licensed under either of
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
## at your option.
## This file may not be copied, modified, or distributed except according to
## those terms.

{.push raises: [].}

import pkg/libp2p/cid
import pkg/libp2p/multihash
import pkg/libp2p/protobuf/minprotobuf

import pkg/questionable/results

import ./manifest

proc write(pb: var ProtoBuffer, field: int, value: MultiHash) =
var ipb = initProtoBuffer()
ipb.write(1, value.data.buffer)
ipb.finish()
pb.write(field, ipb)

proc write(pb: var ProtoBuffer, field: int, value: BitTorrentInfo) =
var ipb = initProtoBuffer()
ipb.write(1, value.length)
ipb.write(2, value.pieceLength)
for piece in value.pieces:
ipb.write(3, piece)
if name =? value.name:
ipb.write(4, name)
ipb.finish()
pb.write(field, ipb)

proc encode*(manifest: BitTorrentManifest): seq[byte] =
# ```protobuf
# Message BitTorrentManifest {
# Message Piece {
# bytes data = 1;
# }
#
# Message BitTorrentInfo {
# uint32 length = 1;
# uint32 pieceLength = 2;
# repeated Piece pieces = 3;
# optional string name = 4;
# }
#
# BitTorrentInfo info = 1;
# bytes codexManifestCid = 2;
# ```

var ipb = initProtoBuffer()
ipb.write(1, manifest.info)
ipb.write(2, manifest.codexManifestCid.data.buffer)
ipb.finish()
ipb.buffer
Loading
Loading