Skip to content

Latest commit

 

History

34 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UNFED AI — Testnet Guide

How to run, join, and operate on the UNFED AI test network.

For contributor onboarding and PR expectations, see CONTRIBUTING.md.

Developer Shortcuts

# Show common tasks
make help

# Local contributor checks
make check

# Public testnet lifecycle helpers
make public-testnet-start
make public-testnet-status
make public-testnet-stop

Prerequisites

  • Python 3.12+
  • tmux (recommended for multi-process startup on VPS)
  • Model weights: run python -m tools.cli split Qwen/Qwen2.5-0.5B -o shards --text-shards 4 or download pre-split shards

Install dependencies:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

System Requirements by Node Type

Suggested minimums for public testnet reliability:

Node Type CPU RAM GPU Storage Notes
Registry 1 vCPU 1 GB No 10 GB SSD Budget profile; for higher node counts use 2 vCPU / 2-4 GB
Daemon 2 vCPU 4 GB No 20 GB SSD SQLite share-chain + telemetry; use persistent disk
MPC Node A 4 vCPU 8-16 GB Recommended 30 GB SSD Entry MPC node; should be the stronger of the pair
MPC Node B 4 vCPU 8-16 GB Recommended 30 GB SSD MPC peer; keep network latency low to Node A
Compute (text shard) 4+ vCPU 8-16 GB Recommended 40+ GB SSD One process per shard index
Vision shard 4+ vCPU 12-24 GB Yes (preferred) 40+ GB SSD Needed for multimodal image paths
Web UI 1 vCPU 1 GB No 10 GB SSD Usually co-located with registry; bind to 127.0.0.1

Quick Start (Local)

Start a full local stack with on-chain admission + MPC-required policy:

# 0) Prepare local chain + contracts (writes deployed.env)
export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
./scripts/start_local_chain.sh
source deployed.env

# 1) Create cluster config from deployed addresses
cat > cluster_config.json <<EOF
{
  "name": "Local UNFED Pool",
  "chain_rpc_url": "${CHAIN_RPC_URL}",
  "escrow_contract_address": "${ESCROW_ADDRESS}",
  "staking_token_address": "${TOKEN_ADDRESS}",
  "operator_private_key": "${OPERATOR_PRIVATE_KEY}",
  "daemon_required_count": 1
}
EOF

# 2) Fund + stake daemon/MPC/compute identities (required in on-chain mode)
FUND_WEI=200000000000000000000
STAKE_WEI=100000000000000000000
for A in \
  0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc \
  0x976EA74026E726554dB657fA54763abd0C3a0aa9 \
  0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 \
  0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
  0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
  0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC \
  0x90F79bf6EB2c4f870365E785982E1f101E93b906; do
  cast send "$TOKEN_ADDRESS" "transfer(address,uint256)" "$A" "$FUND_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key "$OPERATOR_PRIVATE_KEY"
done

# approve + stake (daemon, MPC-B, MPC-A, shard0-3)
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

# 3) Start services in strict order (one shell each, or use tmux)
# 3a Registry
python -m network.registry_server --port 50050 --cluster-config cluster_config.json

# 3b Daemon (staked identity)
export UNFED_STAKE_EVM_PRIVATE_KEY=0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
python -m network.daemon_node --port 50070 --registry localhost:50050 --eth-address 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc

# 3c MPC B first, then MPC A
export UNFED_STAKE_EVM_PRIVATE_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
python -m network.mpc_shard0 --role B --port 50063 --peer localhost:50061 --registry localhost:50050 --eth-address 0x976EA74026E726554dB657fA54763abd0C3a0aa9 --shards-dir shards

export UNFED_STAKE_EVM_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
python -m network.mpc_shard0 --role A --port 50061 --peer localhost:50063 --registry localhost:50050 --eth-address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 --shards-dir shards

# 3d Compute shards
export UNFED_STAKE_EVM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
python -m node.server --shard-index 0 --port 50051 --shards-dir shards --registry localhost:50050 --eth-address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

export UNFED_STAKE_EVM_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
python -m node.server --shard-index 1 --port 50052 --shards-dir shards --registry localhost:50050 --eth-address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8

export UNFED_STAKE_EVM_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
python -m node.server --shard-index 2 --port 50053 --shards-dir shards --registry localhost:50050 --eth-address 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC

export UNFED_STAKE_EVM_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
python -m node.server --shard-index 3 --port 50054 --shards-dir shards --registry localhost:50050 --eth-address 0x90F79bf6EB2c4f870365E785982E1f101E93b906

# 4) Verify model readiness
python -m client.client --list-models --registry localhost:50050

# 5) Run client query
python -m client.client --registry localhost:50050 --prompt "Hello, world"

# 6) Optional web dashboard
python -m web.server --host 127.0.0.1 --port 8080 --registry localhost:50050

By default, the dashboard binds to 127.0.0.1. Use --host 0.0.0.0 only when you intend to expose it behind network controls.

One-shot tmux launcher (same startup order)

If you prefer a single copy/paste launch path, run this after chain deploy + staking:

# from repo root
source .venv/bin/activate

# clean old sessions (safe if missing)
tmux kill-session -t registry 2>/dev/null || true
tmux kill-session -t daemon 2>/dev/null || true
tmux kill-session -t mpcb 2>/dev/null || true
tmux kill-session -t mpca 2>/dev/null || true
tmux kill-session -t node0 2>/dev/null || true
tmux kill-session -t node1 2>/dev/null || true
tmux kill-session -t node2 2>/dev/null || true
tmux kill-session -t node3 2>/dev/null || true
tmux kill-session -t web 2>/dev/null || true

# 1) registry
tmux new -s registry -d 'cd '"$PWD"' && source .venv/bin/activate && python -m network.registry_server --port 50050 --cluster-config cluster_config.json'

# 2) daemon
tmux new -s daemon -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba python -m network.daemon_node --port 50070 --registry localhost:50050 --eth-address 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'

# 3) MPC B then MPC A
tmux new -s mpcb -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e python -m network.mpc_shard0 --role B --port 50063 --peer localhost:50061 --registry localhost:50050 --eth-address 0x976EA74026E726554dB657fA54763abd0C3a0aa9 --shards-dir shards'
tmux new -s mpca -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a python -m network.mpc_shard0 --role A --port 50061 --peer localhost:50063 --registry localhost:50050 --eth-address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 --shards-dir shards'

# 4) compute shards
tmux new -s node0 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 python -m node.server --shard-index 0 --port 50051 --shards-dir shards --registry localhost:50050 --eth-address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
tmux new -s node1 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d python -m node.server --shard-index 1 --port 50052 --shards-dir shards --registry localhost:50050 --eth-address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
tmux new -s node2 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a python -m node.server --shard-index 2 --port 50053 --shards-dir shards --registry localhost:50050 --eth-address 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'
tmux new -s node3 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 python -m node.server --shard-index 3 --port 50054 --shards-dir shards --registry localhost:50050 --eth-address 0x90F79bf6EB2c4f870365E785982E1f101E93b906'

# optional web
tmux new -s web -d 'cd '"$PWD"' && source .venv/bin/activate && python -m web.server --host 127.0.0.1 --port 8080 --registry localhost:50050'

# check sessions
tmux ls

Useful follow-ups:

# verify readiness
python -m client.client --list-models --registry localhost:50050

# tail one service
tmux attach -t mpca

# stop all local sessions
tmux kill-session -t registry; tmux kill-session -t daemon; tmux kill-session -t mpcb; tmux kill-session -t mpca; tmux kill-session -t node0; tmux kill-session -t node1; tmux kill-session -t node2; tmux kill-session -t node3; tmux kill-session -t web

Run preflight before exposing services:

python -m scripts.testnet_preflight web --host 127.0.0.1
python -m scripts.testnet_preflight node --advertise "203.0.113.42:50051" --tls-cert /path/to/server.crt --tls-key /path/to/server.key
python -m scripts.testnet_preflight runtime --web-url http://127.0.0.1:8080

Or run the bundled one-command checklist against an env profile:

cp .env.public-testnet.example .env.testnet
./scripts/run_public_testnet_checklist.sh

One-command launch + readiness check:

./scripts/start_public_testnet.sh
# optionally reuse already running services
./scripts/start_public_testnet.sh --reuse-running

Using GGUF / Ollama Models

UNFED helper scripts now support GGUF inspection and conversion. This is useful when you downloaded a model with Ollama and want to validate compatibility before testnet rollout.

Install/update dependencies:

source .venv/bin/activate
pip install -r requirements.txt

Inspect a GGUF file:

python -m tools.cli inspect /path/to/model.gguf

Convert GGUF to a local safetensors file:

python -m tools.cli convert /path/to/model.gguf -o ./model.safetensors

Split directly from GGUF (supported subset):

python -m tools.cli split /path/to/model.gguf -o ./shards --text-shards 4

Current limitations:

  • Split from GGUF is text-only and expects transformer-style tensor names (model.layers.<idx>.*)
  • Quantized GGUF tensors (for example Q4/Q5/Q8 packed tensors) are rejected
  • For unsupported GGUF key layouts, use original Hugging Face weights for splitting

Troubleshooting:

  • Unsupported GGUF tensor dtype: your file is quantized; use non-quantized export or HF weights
  • supported transformer layer key names: GGUF uses llama.cpp naming; split from HF source instead
  • GGUF support requires the 'gguf' package: run pip install gguf

Hosting on the Internet

To expose your node to other participants over the internet:

1. Open firewall ports

Each service needs its port accessible from the internet:

Service Default Port Protocol
Registry 50050 TCP/gRPC
Node 50051+ TCP/gRPC
Web UI 8080 TCP/HTTP
Daemon 50070 TCP/gRPC

Example (ufw):

sudo ufw allow 50051/tcp

2. Use --advertise with your public IP

Nodes register with the registry using their advertised address. By default this is localhost:<port>, which only works locally.

python -m node.server \
  --shard-index 0 \
  --port 50051 \
  --advertise "203.0.113.42:50051" \
  --registry "registry.unfed.ai:50050" \
  --shards-dir shards

Or via config file:

python -m node.run --config node_config.json

See node_config.testnet.json.example for a template.

3. Configure seed registries

Edit network/seeds.json (or set UNFED_SEEDS env var) to point at the public registry:

export UNFED_SEEDS="registry.unfed.ai:50050,registry2.unfed.ai:50050"

Or copy network/seeds.testnet.json.example to network/seeds.json and fill in real addresses.

4. TLS (recommended for public networks)

Generate or obtain a TLS certificate, then start with:

python -m network.registry_server \
  --port 50050 \
  --tls-cert /path/to/server.crt \
  --tls-key /path/to/server.key

python -m node.server \
  --shard-index 0 --port 50051 \
  --tls-cert /path/to/server.crt \
  --tls-key /path/to/server.key \
  --registry "registry.unfed.ai:50050"

Publicly advertised compute nodes now require TLS by default. If a node advertises a non-local endpoint and no TLS cert/key is provided, startup fails closed.

5. Output privacy mode (full output-stage 2PC)

Output privacy now supports full output-stage 2PC without the retired server_sample path:

  • off: standard plaintext sampling
  • decode_client_sample: final node returns encrypted top-k artifact for client-side sampling
  • full_output_2pc: output stage uses MPC A/B roles for sampled-token artifact return

Set it via env or node config:

export UNFED_HE_COMPUTE_MODE=full_output_2pc

server_sample is rejected at runtime and should not be used. When MPC policy is enabled, model readiness requires one healthy input MPC pair and one healthy output MPC pair (the same A/B pair may advertise both capabilities).

Performance-oriented transport paths for compute/MPC nodes are now always-on:

  • daemon share submission uses async queue + batching + compact blob transport
  • output-stage 2PC uses binary artifact transport with FP16 hidden-vector codec
  • same-next-hop forwarding uses short-window micro-batching (BatchForward)

These are no longer controlled by runtime environment toggles.

Clients connect with:

python -m client.client --tls-ca /path/to/ca.crt

For testing, generate a self-signed cert:

openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt \
  -days 365 -nodes -subj "/CN=your.domain.com" \
  -addext "subjectAltName=DNS:your.domain.com,IP:203.0.113.42"

On-Chain Escrow and Token

The registry can enforce staking, escrow, and settlement on-chain via an ERC-20 token and the UnfedEscrow contract. This is optional — without it the registry runs in-memory simulation mode (--no-chain).

Deploy contracts to a testnet

You need Foundry installed and a wallet funded with testnet ETH (get Sepolia ETH from sepoliafaucet.com).

export CHAIN_RPC_URL="https://rpc.sepolia.org"
export OPERATOR_PRIVATE_KEY="0x<your-deployer-key>"
export OPERATOR_ADDRESS="0x<your-deployer-address>"

./scripts/deploy_testnet.sh

This deploys UnfedToken (1M test UNFED) and UnfedEscrow, then writes the addresses to deployed.env at the project root.

For local development with Anvil instead:

export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

./scripts/start_local_chain.sh

Start the registry with on-chain escrow

Create a cluster_config.json:

{
  "name": "My UNFED Pool",
  "chain_rpc_url": "https://rpc.sepolia.org",
  "escrow_contract_address": "0x<from deployed.env>",
  "staking_token_address": "0x<from deployed.env>",
  "operator_private_key": "0x<your-operator-key>"
}

You do not need to provide a token name in config; the registry resolves token metadata from the ERC-20 contract at staking_token_address.

Then start the registry:

python -m network.registry_server --port 50050 --cluster-config cluster_config.json

If you also want the browser UI for easier querying, start the web server separately (registry does not launch it automatically):

python -m web.server --port 8080 --registry localhost:50050

The registry will verify node stake eligibility on-chain, post settlements, and handle slashing.

Winner bonus and payout split

Racing still uses first-valid-wins for request serving, but settlement payouts are not winner-only. The registry accepts race-winner reports and signs winner receipts, then applies a winner-share bonus during settlement weighting:

  • all validated shares still participate in payout split
  • winner nodes receive extra share weight for reported winning hops
  • total settlement payout is unchanged (bonus is reweighted within the same pool)

Winner-bonus policy is cluster-config driven:

  • winner_bonus_per_report (default 0.25)
  • winner_bonus_cap_ratio (default 0.5)
  • winner_receipt_store_path (default ~/.unfed/registry_winner_receipts.jsonl)
  • winner_receipt_store_max_entries (default 200000)

Racing clients now report winners in batches per decode step to reduce control-plane RPC overhead (ReportRaceWinners), with automatic fallback to single-report mode when connected to older registries.

Docs Site

The static docs are in site/ and include:

  • Getting started: site/getting-started.html
  • Architecture and protocol flow: site/architecture.html, site/protocol.html
  • Economics and staking/slashing model: site/economics.html
  • Full config reference by node type: site/config-reference.html

Community links:

Using your own ERC-20 token

The deploy script creates a test UnfedToken, but the escrow contract accepts any ERC-20. To use your own token:

  1. Deploy UnfedEscrow with your token's address (edit Deploy.s.sol or pass it as an env var)
  2. Set staking_token_address in cluster_config.json to your token's address
  3. Nodes must hold and stake your token to be eligible

How staking works

  • Nodes call stake() on the escrow contract with the required minimum (default: 100 tokens)
  • The registry checks isEligible() on-chain before accepting a node
  • Clients deposit tokens into escrow; inference fees are deducted per-request
  • The operator posts settlement batches via postSettlement(); after the challenge window, finalizeSettlement() unlocks claimable payouts
  • Misbehaving nodes can be slashed (50% of stake, configurable)

Test token faucet

On a testnet the operator holds all minted tokens. Clients can request free test tokens from the web dashboard:

  1. Open the dashboard and enter your wallet address in the Settings sidebar.
  2. Click Get Test Tokens.
  3. The server calls depositFor(address, amount) on the escrow contract, which transfers tokens from the operator and credits them directly to the client's escrow balance.

Rate limit: one drip (100 tokens) per address per hour. The amount and cooldown are configurable via OnChainEscrow.FAUCET_DRIP_AMOUNT and OnChainEscrow.FAUCET_COOLDOWN.

Faucet is disabled by default and must be explicitly enabled with:

export UNFED_FAUCET_ENABLED=1

By default, faucet requests also require a wallet-authenticated session (UNFED_FAUCET_REQUIRE_AUTH=1). For local test-only automation, you can disable that check with UNFED_FAUCET_REQUIRE_AUTH=0.

API: POST /api/faucet with body {"address": "0x...", "session_token": "<optional when auth disabled>"}. Returns {"success": true, "amount": 100, "tx_hash": "0x...", "balance": 200.0} on success, or an error with the appropriate HTTP status.

Address validation is strict: invalid/non-EVM addresses are rejected with HTTP 400. Faucet cooldown is keyed on normalized checksum addresses, so case changes do not bypass rate limits.

Strict token billing

Billing is tokenizer-based and model-specific (no character-length heuristics):

  • Text requests: input tokens are counted with the selected model tokenizer.
  • Vision-language requests: input tokens are counted from the same model processor/tokenizer path used to build multimodal inputs (including image placeholder tokens).
  • Output tokens are counted from streamed generation events.

Cost is reported as:

(input_tokens * price_per_input_token) + (output_tokens * price_per_output_token)

Strict mode behavior:

  • If model tokenizer/processor cannot be loaded, the request fails (no fallback estimate and no usage report).
  • Usage reports with invalid token counts are rejected by the registry.

Environment Variables

Variable Description Default
UNFED_REGISTRY Registry address localhost:50050
UNFED_SEEDS Comma-separated seed registries from network/seeds.json
UNFED_WIRE_DTYPE Wire format for activations float16
UNFED_COMPRESS_THRESHOLD Compression threshold (bytes) 16384
UNFED_PREFILL_MIN Min tokens for pipelined prefill 64
UNFED_REQUIRE_MPC Require MPC shard-0 for admission 1
UNFED_STAKE_EVM_PRIVATE_KEY Staked node wallet key (direct env)
UNFED_STAKE_EVM_PRIVATE_KEY_FILE File path containing staked node wallet key
UNFED_AUTH_MAX_SKEW_MS Max timestamp skew for signed control/auth payloads 300000
UNFED_AUTH_NONCE_MAX_ENTRIES Max nonce cache entries before LRU-style eviction 200000
UNFED_AUTH_RATE_LIMIT_PER_MINUTE Per-node/per-peer auth RPC rate cap 120
UNFED_STAKE_REVALIDATE_TTL_SECONDS Eligibility cache TTL before on-chain recheck 30
CHAIN_RPC_URL Ethereum RPC endpoint
OPERATOR_PRIVATE_KEY Operator wallet key (deploy/settlement)
OPERATOR_ADDRESS Operator wallet address

UNFED_REQUIRE_MPC=1 is the default fail-closed policy across web, client, and pipeline entrypoints. Set UNFED_REQUIRE_MPC=0 only for local debugging.

For OPSEC, prefer file-based secret loading for staked node keys:

umask 077
printf '%s' "0xYOUR_STAKED_NODE_PRIVATE_KEY" > ~/.unfed/node_stake.key
export UNFED_STAKE_EVM_PRIVATE_KEY_FILE=~/.unfed/node_stake.key

Manual MPC Local Setup Script

Use the orchestration script to launch chain/funding/registry/daemon/nodes/web in separate gnome-terminal windows:

export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
./scripts/manual_setup_mpc_testnet.sh

Useful flags:

  • --dry-run: print commands without starting processes
  • --yes-reuse-ports: continue even if expected ports are already occupied

Running as a Node Operator

  1. Split or download the model shards
  2. Pick a shard index (coordinate with other operators or use --auto)
  3. Open firewall ports
  4. Start your node with --advertise pointing to your public IP
  5. Verify registration via the web dashboard or python -m client.client --list-models

Running the Client

CLI:

python -m client.client \
  --registry "registry.unfed.ai:50050" \
  --prompt "Explain quantum computing" \
  --max-tokens 100

Add --tls-ca ca.crt if the network uses TLS.

Web interface: Every registry hosts a web dashboard. Open the registry's HTTP address in a browser to chat, inspect the network, and explore the share-chain — no separate client install needed.

Web Dashboard

The registry operator starts the dashboard with:

python -m web.server --host 127.0.0.1 --port 8080 --registry "localhost:50050"

Open http://<registry-ip>:8080 — tabs for Chat, Network topology, and Chain explorer. Clients visiting the URL can send inference requests directly from the browser, get test tokens via the faucet, and monitor node activity.

Model selection is strict and registry-driven:

  • The dashboard loads model options from GET /api/models only (no frontend hardcoded list).
  • A request is accepted only for a model that passes admission preflight:
    • full text shard coverage
    • MPC shard-0 entry available (default policy)
    • full vision shard coverage for multimodal requests
  • If preflight fails, the request is rejected with a model-scoped error.

Security restrictions:

  • Websocket wallet and faucet address inputs must be valid EVM addresses.
  • cluster_endpoint overrides are blocked by default; only the co-located registry (or entries from UNFED_ALLOWED_CLUSTER_ENDPOINTS) are accepted.

Troubleshooting:

  • No models discovered: ensure compute nodes are registered with the intended model_id and shard coverage is complete.
  • Model rejected by policy (MPC missing): register an MPC shard-0 node, or set UNFED_REQUIRE_MPC=0 for local debugging only.
  • Model rejected by policy (coverage incomplete): verify all required shards are registered for that model_id.

Known Limitations

  • Persistent identity required in on-chain mode: compute/vision/MPC/daemon should use a staked EVM identity (--eth-address) and authenticated registration/control-plane signatures
  • Testnet economics: on-chain escrow works but uses test tokens — no real value at stake
  • Single model: the testnet runs one model (Qwen2.5-0.5B); multi-model pools are implemented but untested at scale
  • No NAT traversal: nodes behind NAT must configure port forwarding manually
  • TLS is optional: without it, traffic is plaintext — do not send sensitive data

About

Decentralized AI inference network — privacy-preserving, peer-to-peer LLM serving with onion routing, MPC, and share-chain economics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages