Run a Mix network of storage nodes
Stand up a local Mix network and download a file through it, with the content lookup anonymized.
This procedure stands up a small local Mix network using logoscore: six Logos Storage Module nodes on one machine — four Mix relays wired around a bootstrap node, plus two storage nodes that route their DHT lookups through the relays. At the end, one storage node uploads a file and the other downloads it with the lookup tunnelled over Mix.
Before you start, make sure you have the following:
-
Linux or macOS
-
Nix with flakes enabled. Install from nixos.org, then enable flakes:
mkdir -p ~/.config/nixecho 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf -
logoscore, andlgpminstalled. To install these tools, use theinstall-node-tools.shhelper script:curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-node-tools.sh | shexport PATH="$PWD/bin:$PATH"The script installs the tools into
./binof the current directory: run it in the same working directory as the rest of this procedure. -
jqon yourPATH— used to read each node's identity out of thedebugJSON. Verify:jq --version
What to expect
- You can run several
logoscoredaemons side by side with separate--config-dirs. - You can set up a private Mix network and configure storage nodes to anonymize their lookups through it.
- You can exchange a file between two storage nodes and verify the content lookup was tunnelled over Mix.
Build and install the storage module
-
Build the module package with Nix:
nix build 'github:logos-co/logos-storage-module/v2.0.1#lgx-portable' -o storage-lgx- This produces a
logos-storage_module-module-lib.lgxpackage in./storage-lgx/.
infoThe initial Nix build takes 15–20 minutes on first run. Subsequent builds use the Nix cache and complete in seconds. Use the
#lgx-portableoutput: it declares the standard platform variant (e.g.linux-amd64) that the release build oflgpmaccepts. - This produces a
-
Install the package into a local modules directory using
lgpm. The package is a local build and is unsigned, so pass--allow-unsigned. All six daemons share this one modules directory:mkdir -p moduleslgpm --modules-dir ./modules --allow-unsigned install --file storage-lgx/*.lgx -
Confirm the module landed:
lgpm --modules-dir ./modules list# storage_module appears in the listing
Launch the bootstrap Mix node (node 1)
The first node is the bootstrap node: the other nodes use it to join the Mix network.
-
Write node 1's configuration. It uses
"nat": "none"to avoid port mapping attempts on the local network,"mix-enabled": true, and"no-bootstrap-node": truebecause it is the bootstrap node. Paths are absolute ($(pwd)) because each module runs in its own process:mkdir -p "$(pwd)/storage-data/node-1"cat > config-1.json <<EOF{"log-level": "DEBUG","data-dir": "$(pwd)/storage-data/node-1","log-file": "$(pwd)/storage-data/node-1/storage.log","disc-port": 9091,"listen-port": 8081,"nat": "none","mix-enabled": true,"no-bootstrap-node": true}EOF -
Start a
logoscoredaemon for node 1 in the background, with its own config directory and its output captured:logoscore --config-dir=./logoscore-1 -D -m ./modules > logs-1.txt 2>&1 &# Wait a few seconds for the daemon to come up -
Load the module, initialise it, and start the node:
logoscore --config-dir=./logoscore-1 load-module storage_modulelogoscore --config-dir=./logoscore-1 call storage_module init @config-1.jsonlogoscore --config-dir=./logoscore-1 call storage_module start# Wait a few seconds for the node to start -
Read node 1's SPR out of
debugand save it: the other nodes use this value as theirbootstrap-node(see Connectivity):logoscore --config-dir=./logoscore-1 call storage_module debug \| jq -er '.result.value.spr' > bootstrap-spr.txt
Launch the remaining Mix relays (nodes 2–4)
Nodes 2, 3 and 4 are identical to node 1, except that they join through node 1's SPR as their bootstrap-node.
-
Write the configs for nodes 2–4, with per-node ports and data dirs:
BOOTSTRAP=$(cat bootstrap-spr.txt)for id in 2 3 4; domkdir -p "$(pwd)/storage-data/node-$id"cat > "config-$id.json" <<EOF{"log-level": "DEBUG","data-dir": "$(pwd)/storage-data/node-$id","log-file": "$(pwd)/storage-data/node-$id/storage.log","disc-port": $((9090 + id)),"listen-port": $((8080 + id)),"nat": "none","mix-enabled": true,"bootstrap-node": ["$BOOTSTRAP"]}EOFdone -
Start one daemon per node, each with its own
--config-dir:for id in 2 3 4; dologoscore --config-dir=./logoscore-$id -D -m ./modules > logs-$id.txt 2>&1 &done# Wait a few seconds for the daemons to come up -
Load the module, initialise from each config, and start each node:
for id in 2 3 4; dologoscore --config-dir=./logoscore-$id load-module storage_modulelogoscore --config-dir=./logoscore-$id call storage_module init @config-$id.jsonlogoscore --config-dir=./logoscore-$id call storage_module startdone# Wait a few seconds for the nodes to start -
Verify the network is up: every node must report a non-empty identity (
idandspr) throughdebug:for id in 1 2 3 4; dologoscore --config-dir=./logoscore-$id call storage_module debug \| jq -e '(.result.value.id // "") != "" and (.result.value.spr // "") != ""'done
Build the Mix relay pool
The storage nodes need two files to use the Mix relays:
mix-pool.json: the relays' peer IDs, multiaddrs, mix and libp2p public keys.mix-proxies.json: the relays' TCP SPRs.
Since this is a local network, every relay is reachable at 127.0.0.1 on its fixed listen-port (808<id>), so the pool multiAddr is built from that rather than from the announced addresses.
-
Save the debug output of each relay to
debug-<id>.jsonto make the data extraction easier:for id in 1 2 3 4; dologoscore --config-dir=./logoscore-$id call storage_module debug > debug-$id.jsondone -
Assemble
mix-pool.json.debugalready returnspeerId,mixPubKeyandlibp2pPubKeyin exactly the form the pool wants:for id in 1 2 3 4; doADDR="/ip4/127.0.0.1/tcp/$((8080 + id))"jq --arg ma "$ADDR" '{peerId: .result.value.id,multiAddr: $ma,mixPubKey: .result.value.mixPubKey,libp2pPubKey: .result.value.libp2pPubKey}' debug-$id.jsondone | jq -s '{version: 1, relays: .}' > mix-pool.json -
Collect the relays' proxy SPRs (
providerRecord) into a JSON array:jq -s -c '[.[].result.value.providerRecord]' debug-*.json > mix-proxies.json
Start the storage nodes (5 and 6)
The four nodes so far are the Mix relays. Now add the storage nodes that actually use them: their config is the same, plus the dht-mix-proxy list and the mix-pool path built in the previous section.
-
Write the storage node configs:
BOOTSTRAP=$(cat bootstrap-spr.txt)PROXIES=$(cat mix-proxies.json)for id in 5 6; domkdir -p "$(pwd)/storage-data/node-$id"cat > "config-$id.json" <<EOF{"log-level": "DEBUG","data-dir": "$(pwd)/storage-data/node-$id","log-file": "$(pwd)/storage-data/node-$id/storage.log","disc-port": $((9090 + id)),"listen-port": $((8080 + id)),"nat": "none","mix-enabled": true,"bootstrap-node": ["$BOOTSTRAP"],"dht-mix-proxy": $PROXIES,"mix-pool": "$(pwd)/mix-pool.json"}EOFdone -
Start one daemon per storage node:
for id in 5 6; dologoscore --config-dir=./logoscore-$id -D -m ./modules > logs-$id.txt 2>&1 &done# Wait a few seconds for the daemons to come up -
Load the module, initialise from each config, and start each node:
for id in 5 6; dologoscore --config-dir=./logoscore-$id load-module storage_modulelogoscore --config-dir=./logoscore-$id call storage_module init @config-$id.jsonlogoscore --config-dir=./logoscore-$id call storage_module startdone# Wait a few seconds for the nodes to start -
Verify the storage nodes are up:
for id in 5 6; dologoscore --config-dir=./logoscore-$id call storage_module debug \| jq -e '(.result.value.id // "") != "" and (.result.value.spr // "") != ""'done
Upload from one node, download through Mix
Node 5 seeds a file, and node 6 downloads it with local=false to force a network lookup — the lookup that Mix hides.
-
Create a small file and upload it through node 5:
echo "Hello through Mix from the storage doc-test." > hello.txtlogoscore --config-dir=./logoscore-5 call storage_module uploadUrl "$(pwd)/hello.txt" 65536 -
The upload runs in the background; give it a moment, then read the CID of the stored manifest from node 5:
logoscore --config-dir=./logoscore-5 call storage_module manifests \| jq -er '.result.value[0].cid' > cid.txt -
Download the CID through node 6:
logoscore --config-dir=./logoscore-6 call storage_module downloadToUrl "$(cat cid.txt)" "$(pwd)/downloaded.txt" false 65536# Wait a few seconds for the download to complete -
Confirm the lookup was tunnelled through Mix: node 6's log records the relay selection (SURB):
grep "Selected mix node for surbs" storage-data/node-6/storage.log logs-6.txt -
Verify the round-trip: the downloaded file matches what node 5 uploaded:
cat downloaded.txt# Hello through Mix from the storage doc-test.
Shut the network down
-
For each node: stop the libp2p node, destroy the storage context, and stop the daemon. The
|| truelets the loop continue past a node that is already gone:for id in 1 2 3 4 5 6; dologoscore --config-dir=./logoscore-$id call storage_module stop || truelogoscore --config-dir=./logoscore-$id call storage_module destroy || truelogoscore --config-dir=./logoscore-$id stop || truedone -
Confirm the daemons have stopped:
logoscore --config-dir=./logoscore-1 status# reports "status":"not_running"