Skip to content

Commit a104109

Browse files
Merge pull request #2 from robertpelloni/feature/megatorrent-reference-2716865330907683697
Feature/megatorrent reference 2716865330907683697
2 parents 4025528 + 291ff84 commit a104109

File tree

10 files changed

+409
-111
lines changed

10 files changed

+409
-111
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "qbittorrent"]
2+
path = qbittorrent
3+
url = https://github.com/robertpelloni/qbittorrent

ARCHITECTURE.md

Lines changed: 0 additions & 79 deletions
This file was deleted.

README-monorepo.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Megatorrent Monorepo
2+
3+
This repository contains the reference implementation for the Megatorrent protocol, a decentralized, mutable successor to BitTorrent.
4+
5+
## Structure
6+
7+
* `docs/`: Architecture and Protocol Specifications.
8+
* `tracker/`: Node.js implementation of the Tracker and Reference Client (the root of this repo, historically).
9+
* `qbittorrent/`: C++ Client Fork (Submodule).
10+
11+
## Getting Started
12+
13+
### Node.js Tracker & Reference Client
14+
Run the tracker and client tests:
15+
```bash
16+
npm install
17+
npm test
18+
```
19+
20+
### qBittorrent Client
21+
See `qbittorrent/README.md` for C++ build instructions.

docs/ARCHITECTURE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
## 6. Obfuscated Storage Protocol (Phase 2)
3+
To achieve plausible deniability and distributed redundancy, data is not stored as "files" but as "Blobs" of high-entropy data.
4+
5+
### Concepts
6+
* **Blob**: A fixed-size (or variable) container stored by a Host. To the Host, it is just random bytes.
7+
* **Chunk**: A segment of a user's file.
8+
* **Encryption**: Each Chunk is encrypted with a *unique* random key (ChaCha20-Poly1305) before being placed into a Blob.
9+
* **Muxing**: A Blob may contain multiple Chunks (potentially from different files) or padding.
10+
11+
### Data Structure: The `FileEntry`
12+
This structure replaces the simple "magnet link" in the Megatorrent Manifest.
13+
14+
```json
15+
{
16+
"name": "episode1.mkv",
17+
"mime": "video/x-matroska",
18+
"size": 104857600,
19+
"chunks": [
20+
{
21+
"blobId": "<SHA256 Hash of the Blob>",
22+
"offset": 0, // Byte offset in the Blob
23+
"length": 1048576, // Length of the encrypted chunk
24+
"key": "<32-byte Hex Key>",
25+
"nonce": "<12-byte Hex Nonce>",
26+
"authTag": "<16-byte Hex Tag>"
27+
},
28+
...
29+
]
30+
}
31+
```
32+
33+
### Process
34+
1. **Ingest**:
35+
* File is split into N chunks.
36+
* For each chunk:
37+
* Generate random Key & Nonce.
38+
* Encrypt Chunk -> EncryptedChunk.
39+
* (Simplification for Ref Impl) EncryptedChunk becomes a "Blob" directly (or is wrapped).
40+
* Blob ID = SHA256(Blob).
41+
* Result: A list of Blobs (to be uploaded) and a `FileEntry` (to be put in the Manifest).
42+
43+
2. **Access**:
44+
* Subscriber receives Manifest.
45+
* Parses `FileEntry`.
46+
* Requests Blob(ID) from the network (simulated via local dir or tracker relay).
47+
* Extracts bytes at `offset` for `length`.
48+
* Decrypts using `key` and `nonce`.
49+
* Reassembles file.

lib/manifest.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ export function validateManifest (manifest) {
1010
if (!manifest || typeof manifest !== 'object') throw new Error('Invalid manifest')
1111
if (!manifest.publicKey || !manifest.signature) throw new Error('Missing keys')
1212

13+
// Validation
14+
if (typeof manifest.publicKey !== 'string' || !/^[0-9a-fA-F]{64}$/.test(manifest.publicKey)) {
15+
throw new Error('Invalid public key format')
16+
}
17+
if (typeof manifest.signature !== 'string' || !/^[0-9a-fA-F]{128}$/.test(manifest.signature)) {
18+
throw new Error('Invalid signature format')
19+
}
20+
if (typeof manifest.sequence !== 'number') throw new Error('Invalid sequence')
21+
if (typeof manifest.timestamp !== 'number') throw new Error('Invalid timestamp')
22+
if (!Array.isArray(manifest.collections)) throw new Error('Invalid collections')
23+
1324
// Reconstruct the payload to verify (exclude signature)
1425
const payload = {
1526
publicKey: manifest.publicKey,

qbittorrent

Submodule qbittorrent added at 5abf458

reference-client/index.js

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import minimist from 'minimist'
66
import Client from 'bittorrent-tracker'
77
import { generateKeypair } from './lib/crypto.js'
88
import { createManifest, validateManifest } from './lib/manifest.js'
9+
import { ingest, reassemble } from './lib/storage.js'
910

1011
const argv = minimist(process.argv.slice(2), {
1112
alias: {
1213
k: 'keyfile',
1314
t: 'tracker',
1415
i: 'input',
15-
o: 'output'
16+
o: 'output',
17+
d: 'dir'
1618
},
1719
default: {
1820
keyfile: './identity.json',
19-
tracker: 'ws://localhost:8000' // Default to local WS tracker
21+
tracker: 'ws://localhost:8000', // Default to local WS tracker
22+
dir: './storage'
2023
}
2124
})
2225

@@ -25,12 +28,18 @@ const command = argv._[0]
2528
if (!command) {
2629
console.error(`Usage:
2730
gen-key [-k identity.json]
28-
publish [-k identity.json] [-t ws://tracker] -i magnet_list.txt
29-
subscribe [-t ws://tracker] <public_key_hex>
31+
ingest -i <file> [-d ./storage] -> Returns FileEntry JSON
32+
publish [-k identity.json] [-t ws://tracker] -i <file_entry.json>
33+
subscribe [-t ws://tracker] <public_key_hex> [-d ./storage]
3034
`)
3135
process.exit(1)
3236
}
3337

38+
// Ensure storage dir exists
39+
if (!fs.existsSync(argv.dir)) {
40+
fs.mkdirSync(argv.dir, { recursive: true })
41+
}
42+
3443
// 1. Generate Key
3544
if (command === 'gen-key') {
3645
const keypair = generateKeypair()
@@ -44,7 +53,27 @@ if (command === 'gen-key') {
4453
process.exit(0)
4554
}
4655

47-
// 2. Publish
56+
// 2. Ingest
57+
if (command === 'ingest') {
58+
if (!argv.input) {
59+
console.error('Please specify input file with -i')
60+
process.exit(1)
61+
}
62+
const fileBuf = fs.readFileSync(argv.input)
63+
const result = ingest(fileBuf, path.basename(argv.input))
64+
65+
// Save Blobs
66+
result.blobs.forEach(blob => {
67+
fs.writeFileSync(path.join(argv.dir, blob.id), blob.buffer)
68+
})
69+
70+
console.log(`Ingested ${result.blobs.length} blobs to ${argv.dir}`)
71+
console.log('FileEntry JSON (save this to a file to publish it):')
72+
console.log(JSON.stringify(result.fileEntry, null, 2))
73+
process.exit(0)
74+
}
75+
76+
// 3. Publish
4877
if (command === 'publish') {
4978
if (!fs.existsSync(argv.keyfile)) {
5079
console.error('Keyfile not found. Run gen-key first.')
@@ -56,23 +85,32 @@ if (command === 'publish') {
5685
secretKey: Buffer.from(keyData.secretKey, 'hex')
5786
}
5887

59-
// Read magnet links
88+
// Read Input
6089
if (!argv.input) {
61-
console.error('Please specify input file with -i (list of magnet links)')
90+
console.error('Please specify input file with -i (json file entry or text list)')
6291
process.exit(1)
6392
}
93+
6494
const content = fs.readFileSync(argv.input, 'utf-8')
65-
const lines = content.split('\n').map(l => l.trim()).filter(l => l.length > 0)
95+
let items
96+
try {
97+
// Try parsing as JSON (FileEntry)
98+
const json = JSON.parse(content)
99+
// Wrap in our "Items" list.
100+
// In the future, "Items" can be Magnet Links OR FileEntries.
101+
items = [json]
102+
} catch (e) {
103+
// Fallback: Line-separated magnet links
104+
items = content.split('\n').map(l => l.trim()).filter(l => l.length > 0)
105+
}
66106

67-
// Create Collections structure (flat for now)
107+
// Create Collections structure
68108
const collections = [{
69109
title: 'Default Collection',
70-
items: lines
110+
items
71111
}]
72112

73113
// Create Manifest
74-
// TODO: Retrieve last sequence from somewhere or store it?
75-
// For now, we use timestamp as sequence to be simple and monotonic
76114
const sequence = Date.now()
77115
const manifest = createManifest(keypair, sequence, collections)
78116

@@ -123,7 +161,7 @@ if (command === 'publish') {
123161
}, 1000)
124162
}
125163

126-
// 3. Subscribe
164+
// 4. Subscribe
127165
if (command === 'subscribe') {
128166
const pubKeyHex = argv._[1]
129167
if (!pubKeyHex) {
@@ -166,7 +204,37 @@ if (command === 'subscribe') {
166204
if (valid && data.manifest.publicKey === pubKeyHex) {
167205
console.log('VERIFIED UPDATE from ' + pubKeyHex)
168206
console.log('Sequence:', data.manifest.sequence)
169-
console.log('Items:', data.manifest.collections[0].items)
207+
208+
const items = data.manifest.collections[0].items
209+
console.log(`Received ${items.length} items.`)
210+
211+
// Auto-Download Logic (Prototype)
212+
items.forEach(async (item, idx) => {
213+
if (typeof item === 'object' && item.chunks) {
214+
console.log(`Item ${idx}: Detected Megatorrent FileEntry: ${item.name}`)
215+
try {
216+
const fileBuf = await reassemble(item, async (blobId) => {
217+
const p = path.join(argv.dir, blobId)
218+
if (fs.existsSync(p)) {
219+
return fs.readFileSync(p)
220+
}
221+
// TODO: Network fetch
222+
console.log(`Blob ${blobId} not found locally.`)
223+
return null
224+
})
225+
226+
if (fileBuf) {
227+
const outPath = path.join(argv.dir, 'downloaded_' + item.name)
228+
fs.writeFileSync(outPath, fileBuf)
229+
console.log(`SUCCESS: Reassembled to ${outPath}`)
230+
}
231+
} catch (err) {
232+
console.error('Failed to reassemble:', err.message)
233+
}
234+
} else {
235+
console.log(`Item ${idx}: Standard Magnet/Text: ${item}`)
236+
}
237+
})
170238
} else {
171239
console.error('Invalid signature or wrong key!')
172240
}

0 commit comments

Comments
 (0)