diff --git a/codex/bittorrent/bencoding.nim b/codex/bittorrent/bencoding.nim new file mode 100644 index 00000000..2932b18d --- /dev/null +++ b/codex/bittorrent/bencoding.nim @@ -0,0 +1,18 @@ +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 diff --git a/codex/bittorrent/manifest/manifest.nim b/codex/bittorrent/manifest/manifest.nim index da20e8ba..83a31ec2 100644 --- a/codex/bittorrent/manifest/manifest.nim +++ b/codex/bittorrent/manifest/manifest.nim @@ -2,6 +2,10 @@ import pkg/libp2p import pkg/questionable import pkg/questionable/results +import ../../errors +import ../../codextypes +import ../bencoding + type BitTorrentPiece* = MultiHash BitTorrentInfo* = ref object @@ -21,6 +25,19 @@ proc newBitTorrentManifest*( ): BitTorrentManifest = BitTorrentManifest(info: info, codexManifestCid: codexManifestCid) +func bencode*(info: BitTorrentInfo): seq[byte] = + # flatten pieces + var pieces: seq[byte] + for mh in info.pieces: + pieces.add(mh.data.buffer.toOpenArray(mh.dpos, mh.dpos + mh.size - 1)) + result = @['d'.byte] + result.add(bencode("length") & bencode(info.length)) + if name =? info.name: + result.add(bencode("name") & bencode(name)) + result.add(bencode("piece length") & bencode(info.pieceLength)) + result.add(bencode("pieces") & bencode(pieces)) + result.add('e'.byte) + func validate*(self: BitTorrentManifest, cid: Cid): ?!bool = # First stage of validation: # (1) bencode the info dictionary from the torrent manifest @@ -32,4 +49,9 @@ func validate*(self: BitTorrentManifest, cid: Cid): ?!bool = # points to genuine content. This validation will be done while fetching blocks # where we will be able to detect that the aggregated pieces do not match # the hashes in the info dictionary from the torrent manifest. - return success true + let infoBencoded = bencode(self.info) + without infoHash =? MultiHash.digest($Sha1HashCodec, infoBencoded).mapFailure, err: + return failure(err.msg) + without cidInfoHash =? cid.mhash.mapFailure, err: + return failure(err.msg) + return success(infoHash == cidInfoHash) diff --git a/codex/node.nim b/codex/node.nim index f5773a24..55e6cee0 100644 --- a/codex/node.nim +++ b/codex/node.nim @@ -182,7 +182,11 @@ proc fetchTorrentManifest*( trace "Decoded torrent manifest", cid - if err =? torrentManifest.validate(cid).errorOption: + without isValid =? torrentManifest.validate(cid), err: + trace "Error validating torrent manifest", cid, err = err.msg + return failure(err.msg) + + if not isValid: trace "Torrent manifest does not match torrent info hash", cid return failure "Torrent manifest does not match torrent info hash {$cid}" diff --git a/tests/codex/bittorrent/testbencoding.nim b/tests/codex/bittorrent/testbencoding.nim new file mode 100644 index 00000000..ad3b9057 --- /dev/null +++ b/tests/codex/bittorrent/testbencoding.nim @@ -0,0 +1,116 @@ +import std/unittest +import std/strformat +import std/sequtils + +import pkg/nimcrypto +import pkg/stew/byteutils +import pkg/questionable + +import ../../examples +import ../../../codex/bittorrent/bencoding + +type ExampleObject* = ref object + length*: uint64 + pieceLength*: uint32 + pieces*: seq[seq[byte]] + name*: ?string + +func bencode(obj: ExampleObject): seq[byte] = + # flatten pieces + var pieces: seq[byte] + for piece in obj.pieces: + pieces.add(piece) + result = @['d'.byte] + result.add(bencode("length") & bencode(obj.length)) + if name =? obj.name: + result.add(bencode("name") & bencode(name)) + result.add(bencode("piece length") & bencode(obj.pieceLength)) + result.add(bencode("pieces") & bencode(pieces)) + result.add('e'.byte) + +proc toString(bytes: seq[byte]): string = + result = newStringOfCap(len(bytes)) + for b in bytes: + add(result, b.char) + +proc checkEncoding(actual: seq[byte], expected: string) = + check actual.toString == expected + +suite "b-encoding": + test "int": + checkEncoding(bencode(1'i8), "i1e") + checkEncoding(bencode(-1'i8), "i-1e") + checkEncoding(bencode(int8.low), fmt"i{int8.low}e") + checkEncoding(bencode(int8.high), fmt"i{int8.high}e") + checkEncoding(bencode(uint8.low), fmt"i{uint8.low}e") + checkEncoding(bencode(uint8.high), fmt"i{uint8.high}e") + checkEncoding(bencode(int16.low), fmt"i{int16.low}e") + checkEncoding(bencode(int16.high), fmt"i{int16.high}e") + checkEncoding(bencode(uint16.low), fmt"i{uint16.low}e") + checkEncoding(bencode(uint16.high), fmt"i{uint16.high}e") + checkEncoding(bencode(int32.low), fmt"i{int32.low}e") + checkEncoding(bencode(int32.high), fmt"i{int32.high}e") + checkEncoding(bencode(uint32.low), fmt"i{uint32.low}e") + checkEncoding(bencode(uint32.high), fmt"i{uint32.high}e") + checkEncoding(bencode(uint.high), fmt"i{uint.high}e") + checkEncoding(bencode(int64.low), fmt"i{int64.low}e") + checkEncoding(bencode(int64.high), fmt"i{int64.high}e") + checkEncoding(bencode(uint64.low), fmt"i{uint64.low}e") + checkEncoding(bencode(uint64.high), fmt"i{uint64.high}e") + checkEncoding(bencode(int.low), fmt"i{int.low}e") + checkEncoding(bencode(int.high), fmt"i{int.high}e") + + test "empty buffer": + let input: array[0, byte] = [] + check bencode(input) == "0:".toBytes + + test "buffer": + let input = [1.byte, 2, 3] + check bencode(input) == fmt"{input.len}:".toBytes() & @input + + test "longer buffer": + let input = toSeq(1.byte .. 127.byte) + check bencode(input) == fmt"{input.len}:".toBytes() & @input + + test "string": + let input = "abc" + check bencode(input) == "3:abc".toBytes + + test "longer string": + let input = exampleString(127) + check bencode(input) == fmt"{input.len}:{input}".toBytes + + test "empty string": + let input = "" + check bencode(input) == "0:".toBytes + + test "empty list": + let input: seq[string] = @[] + check bencode(input) == "le".toBytes + + test "list (of strings)": + let input = ["abc", "def"] + check bencode(input) == "l3:abc3:defe".toBytes + + test "list (of seq[byte])": + let seq1 = toSeq(1.byte .. 127.byte) + let seq2 = toSeq(128.byte .. 150.byte) + let input = [seq1, seq2] + check bencode(input) == + fmt"l{seq1.len}:".toBytes & seq1 & fmt"{seq2.len}:".toBytes & seq2 & @['e'.byte] + + test "list (of integers)": + let input = [1, -2, 3, 0x7f, -0x80, 0xff] + check bencode(input) == "li1ei-2ei3ei127ei-128ei255ee".toBytes + + test "custom type": + let piece = "1cc46da027e7ff6f1970a2e58880dbc6a08992a0".hexToSeqByte + let obj = ExampleObject( + length: 40960, pieceLength: 65536, pieces: @[piece], name: "data40k.bin".some + ) + let encoded = bencode(obj) + check encoded == + "d6:lengthi40960e4:name11:data40k.bin12:piece lengthi65536e6:pieces20:".toBytes & + piece & @['e'.byte] + let expectedInfoHash = "1902d602db8c350f4f6d809ed01eff32f030da95" + check $sha1.digest(encoded) == expectedInfoHash.toUpperAscii diff --git a/tests/codex/bittorrent/testmanifest.nim b/tests/codex/bittorrent/testmanifest.nim new file mode 100644 index 00000000..50e98e8d --- /dev/null +++ b/tests/codex/bittorrent/testmanifest.nim @@ -0,0 +1,51 @@ +import std/unittest + +import pkg/libp2p/[cid, multicodec, multihash] +import pkg/stew/byteutils +import pkg/questionable + +import ../../examples +import ../../../codex/bittorrent/manifest + +suite "BitTorrent manifest": + # In the tests below, we use an example info dictionary + # from a valid torrent file (v1 so far). + # { + # "info": { + # "length": 40960, + # "name": "data40k.bin", + # "piece length": 65536, + # "pieces": [ + # "1cc46da027e7ff6f1970a2e58880dbc6a08992a0" + # ] + # } + # } + let examplePieceHash = "1cc46da027e7ff6f1970a2e58880dbc6a08992a0".hexToSeqByte + let examplePieceMultihash = MultiHash.init($Sha1HashCodec, examplePieceHash).tryGet + let exampleInfo = BitTorrentInfo( + length: 40960, + pieceLength: 65536, + pieces: @[examplePieceMultihash], + name: "data40k.bin".some, + ) + let dummyCodexManifestCid = Cid.init( + CIDv1, ManifestCodec, MultiHash.digest($Sha256HashCodec, seq[byte].example()).tryGet + ).tryGet + + test "b-encoding info dictionary": + let infoEncoded = bencode(exampleInfo) + check infoEncoded == + "d6:lengthi40960e4:name11:data40k.bin12:piece lengthi65536e6:pieces20:".toBytes & + examplePieceHash & @['e'.byte] + let expectedInfoHash = "1902d602db8c350f4f6d809ed01eff32f030da95" + check $sha1.digest(infoEncoded) == expectedInfoHash.toUpperAscii + + test "validating against info hash Cid": + let infoHash = "1902d602db8c350f4f6d809ed01eff32f030da95".hexToSeqByte + let infoMultiHash = MultiHash.init($Sha1HashCodec, infoHash).tryGet + let infoHashCid = Cid.init(CIDv1, InfoHashV1Codec, infoMultiHash).tryGet + let bitTorrentManifest = newBitTorrentManifest( + info = exampleInfo, codexManifestCid = dummyCodexManifestCid + ) + + check bitTorrentManifest.validate(cid = infoHashCid).tryGet == true diff --git a/tests/codex/testbittorrent.nim b/tests/codex/testbittorrent.nim new file mode 100644 index 00000000..f36b4045 --- /dev/null +++ b/tests/codex/testbittorrent.nim @@ -0,0 +1,4 @@ +import ./bittorrent/testbencoding +import ./bittorrent/testmanifest + +{.warning[UnusedImport]: off.} diff --git a/tests/testCodex.nim b/tests/testCodex.nim index 6a9b107e..7967b45c 100644 --- a/tests/testCodex.nim +++ b/tests/testCodex.nim @@ -1,3 +1,4 @@ +import ./codex/testbittorrent import ./codex/teststores import ./codex/testblockexchange import ./codex/testasyncheapqueue